Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ GitHub. The full reasoning and the measurements behind each change are in [ROADM
CPU choice and the speed balance stay yours. The profile is read from the settings themselves, so
changing any of them by hand shows "Custom", and every setting stays available underneath. MCP's
`sv_start_export` takes a `profile` too, with explicit arguments taking precedence.
- **A batch queue [526].** Queue the current song with the current settings, or several audio
files at once into one folder, and start the queue. Each job keeps the scene and settings from
the moment it was added, so you can change the scene and queue the next variation; one song at
three resolutions gets three distinctly named files. A failing job is marked and the next one
runs. Each row shows its status, failed or cancelled jobs can be retried, and Stop cancels the
running job without leaving a half-written file.
- **Split a long show at the timeline markers [527].** One render is cut at forced keyframes into
a folder of files named after the markers ("01 - Intro.mp4", "02 - Drop.mp4"), so feedback
layers like MilkDrop run continuously across the cuts instead of restarting cold in each part.
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ layers and **40 GPU effects**, an **OBS overlay**, **Spout / Syphon**, **project
[![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-e11d2a.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#download)
[![Tests](https://img.shields.io/badge/tests-3145%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-3151%20passing-2ea043.svg)](#tests)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)

<a href="https://github.com/CaYatur/SoundVisualizer/releases/latest"><img src="https://img.shields.io/badge/Download-Windows-0a84ff?style=for-the-badge" alt="Download for Windows" /></a>
Expand Down Expand Up @@ -937,7 +937,9 @@ the fixtures on the left take the colours on the left of the screen.
time, the timeline or a number of bars; the file is named and tagged with the track, scene and
BPM, and a cancelled export leaves no half-written file. One-click profiles cover YouTube,
Reels / Shorts, Instagram, a Twitch overlay with alpha and a ProRes master, and a long show can be
split at the timeline's markers into files named after them, from a single render.
split at the timeline's markers into files named after them, from a single render. A batch
queue renders one scene across many songs, or one song at several sizes, and keeps going past
a failed job.
- **Live recording** of the output exactly as it appears — with the live audio, modulation,
transitions and effects — to MP4 or WebM, on one key.
- **GIF export** with two-pass palette generation, because one pass bands visibly.
Expand Down Expand Up @@ -1717,7 +1719,7 @@ npm test
npm start -- --smoke
```

**3145 unit tests, all passing.** They are written to check answers, not to exercise lines:
**3151 unit tests, all passing.** They are written to check answers, not to exercise lines:

- **Formulas** are checked against values derived by hand from their definitions — Viviani's curve
staying on its sphere, the torus tube radius, Chladni's m↔n antisymmetry, every attractor staying
Expand Down
7 changes: 4 additions & 3 deletions README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ destesi**, ve **MCP üzerinden yapay zekâ kontrolü**.
[![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases)
[![Lisans: MIT](https://img.shields.io/badge/Lisans-MIT-e11d2a.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#hızlı-başlangıç)
[![Test](https://img.shields.io/badge/test-3145%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-3151%20geçiyor-2ea043.svg)](#testler)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)

<a href="https://github.com/CaYatur/SoundVisualizer/releases/latest"><img src="https://img.shields.io/badge/%C4%B0ndir-Windows-0a84ff?style=for-the-badge" alt="Windows için indir" /></a>
Expand Down Expand Up @@ -937,7 +937,8 @@ ekranın solundaki renkleri alıyor.
etiketleniyor, iptal edilen dışa aktarım yarım dosya bırakmıyor. Tek tıklık profiller YouTube,
Reels / Shorts, Instagram, alfalı Twitch bindirmesi ve ProRes ana kopyayı kapsıyor; uzun bir
gösteri tek render'dan zaman çizelgesinin işaretçilerinde, onların adlarını taşıyan dosyalara
bölünebiliyor.
bölünebiliyor. Toplu kuyruk bir sahneyi birçok şarkıya ya da bir şarkıyı birkaç boyuta
çıkarıyor ve hata veren bir işte durmuyor.
- Çıkışın göründüğü gibi **canlı kaydı** — canlı ses, modülasyon, geçişler ve efektlerle — MP4 ya da
WebM'e, tek tuşla.
- Tek geçiş gözle görülür bantlandığı için iki geçişli palet üretimli **GIF dışa aktarma**.
Expand Down Expand Up @@ -1721,7 +1722,7 @@ npm test
npm start -- --smoke
```

**3145 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
**3151 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:

- **Formüller**, tanımlarından elle türetilmiş değerlerle sınanıyor — Viviani eğrisinin küre üzerinde
kalması, simidin boru yarıçapı, Chladni'nin m↔n antisimetrisi, her çekicinin sınırlı kalması ve görüş
Expand Down
25 changes: 23 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ hands, and a row only moves up once it ships.
|---|:--:|---|
| Per-app audio capture on macOS and Linux | v3.1.6 | On `main` (#514, #517). Linux verified in WSLg with PulseAudio; macOS written on ScreenCaptureKit and compiled in CI, never run on a Mac. Windows shipped in v3.1.3 |
| Live level meter per application | v3.1.6 | Done on `main` (#516): a moving dB-scale bar per application, streamed only while the picker is on screen |
| Comprehensive video export | v3.1.6 | Formats, alpha, frame sequences (#521, #525, #529), about 3× faster export (#531), duration sources, metadata and clean cancel (#523, #524, #528, #532), profiles (#522), chapters (#527) done; queue and the two render modes in progress |
| Comprehensive video export | v3.1.6 | Formats, alpha, frame sequences (#521, #525, #529), about 3× faster export (#531), duration sources, metadata and clean cancel (#523, #524, #528, #532), profiles (#522), chapters (#527), batch queue (#526) done; the two render modes in progress |
| Broadcast layout editor | v3.1.7 | Not started |
| NDI | v3.1.8 | Deferred for licence reasons, not difficulty — see "Not done, and why" |
| Redundancy / genlock | v3.2.0 | Not started |
Expand Down Expand Up @@ -131,7 +131,7 @@ npm test
npm start -- --smoke
```

- **3145 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
- **3151 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
105 came with v3.1.1; 163 came with v3.1.2; 157 came with v3.1.3 — 1128 at
that tag — 469 more with v3.1.4, most of them from the MilkDrop work — 1597 at
that tag — 1402 more with v3.1.5 — 2999 at that tag — and 27
Expand Down Expand Up @@ -3352,6 +3352,27 @@ Export is about three times faster for MP4 and WebM, with no change to the pictu
- A saved "GPU" preference is no longer rewritten to "CPU" on a machine without one: the export runs on the CPU and the box says so.
- The bench-only options, a raw frame dump and a forced RGBA path, are a separate argument that neither the panel nor MCP can set, because the dump writes a file.

### Batch queue (#526) · done

- **Where it lives.** The queue is in the main process and runs jobs one after another. Each job carries a copy of the scene configuration taken when it was added, and `startExportJob` renders that copy rather than the live config. You can therefore change the scene and queue the next variation.
- **Adding jobs.**
- "Add to Queue" adds the chosen song with the current settings.
- "Add Several Audio Files" adds one job per file into a chosen folder.
- Names follow "Track - Scene - 1080p". A name already on disk or in the queue gets " (2)", so one song at three resolutions, or ten songs into one folder, never overwrite each other.
- The path rule is the same as for a single export.
- The queue is capped at 200 jobs.
- **Failure handling.**
- A job that fails is marked with its message, and the queue moves on.
- A job that never started gives back its completion waiter. Otherwise the next job's result would have been reported against it.
- Failed or cancelled jobs can be retried.
- **Stopping.** Stop cancels the running job and leaves the rest waiting. Quitting the app stops the queue before cancelling the running job, so the next job does not start during shutdown.
- **Single exports.** A single export is refused while the queue runs.
- **Measured end to end:**
- **Bench queue mode.** Two jobs with the same name came out as `q1.mp4` and `q1 (2).mp4`. A job with a missing audio file failed with "Ses dosyası bulunamadı." and the WebM and ProRes jobs after it ran. The scene was changed after the jobs were added, and the first two videos still decode to identical frames (`framemd5`).
- **Stopping mid-run.** The running job was cancelled, the rest stayed waiting, and the folder was empty. A `.partial` first seemed to remain, but only because the bench exited with `app.exit()`, which skips the delayed delete; the driver now flushes pending deletes before it exits.
- **Panel.** Driven by its own buttons over CDP, the rows went Waiting → Running → Done, and the button switched between Start and Stop. After the queue ended, "Export to Video" stayed disabled: a queue update arriving after the last job's done event re-marked the panel as busy. The panel now releases the state it set when the queue stops.
- **Not in MCP yet.** `sv_start_export` remains one job at a time.

### Chapters at the timeline markers (#527) · done

- **One render, cut afterwards.** The markers inside the export become forced keyframes, and ffmpeg's segment muxer cuts the single render at them. Rendering each chapter on its own would start MilkDrop and other feedback layers cold at every boundary, so the cut would show.
Expand Down
12 changes: 12 additions & 0 deletions src/admin/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -1559,3 +1559,15 @@ input[type="number"]::-webkit-inner-spin-button {
font-weight: 800;
letter-spacing: .01em;
}

/* Dışa aktarma kuyruğu (#526) */
.export-queue { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; }
.export-queue-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--card2); }
.export-queue-row .q-main { flex: 1; min-width: 0; }
.export-queue-row .q-name { font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.export-queue-row .q-meta { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.export-queue-row .q-status { font-size: 12px; white-space: nowrap; }
.export-queue-row .q-actions { display: flex; gap: 4px; }
.export-queue-row.q-running { border-color: var(--accent); }
.export-queue-row.q-done .q-status { color: var(--ok, #4caf50); }
.export-queue-row.q-error .q-status { color: var(--err, #ff6b6b); }
143 changes: 143 additions & 0 deletions src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@
return audioFileCtrl(def);
case 'exportpanel':
return exportPanelCtrl(def);
case 'exportqueue':
return exportQueueCtrl(def);
case 'lightingpanel':
return lightingPanelCtrl(def);
case 'lightinggeneralpanel':
Expand Down Expand Up @@ -1687,6 +1689,133 @@
return el('div', { class: 'ctrl' }, [btn, name]);
}

// --- Video dışa aktarma kuyruğu (#526) ---
/* Kuyruk ana süreçte; panel yalnız durumu çiziyor ve komut gönderiyor.
Liste kendi kutusunda yeniden çiziliyor, kartın geri kalanı değil. */
let exportQueueSt = { running: false, items: [] };
let exportQueueDir = '';
let exportByQueue = false; // panelin "çalışıyor" hâlini kuyruk mu açtı
const QUEUE_STATUS = {
waiting: ['Bekliyor', ''], running: ['Çalışıyor', 'play'], done: ['Bitti', 'check'],
error: ['Hata', 'warning'], cancelled: ['İptal edildi', 'stop'],
};

/* Kuyruk işinin önerilen adı: "Parça - Sahne - 1080p.mp4". Çözünürlük
adda: aynı şarkı üç çözünürlükte aynı klasöre gidebilsin. */
function queueItemName(audioPath, fmt) {
const FX = window.SVExportFormats;
const title = String(audioPath || '').split(/[\\/]/).pop().replace(/\.[^.]+$/, '');
const e = cfg.export || {};
const base = FX ? FX.outputName({ title, scene: activeSceneName() }, '') : title;
const name = base + ' - ' + (e.resolution || '1080p');
return (FX ? FX.sanitizeFileName(name, 'export') : name) + '.' + (fmt ? fmt.ext : 'mp4');
}

function queueItemOpts() {
const e = cfg.export || {};
const dur = exportDuration();
const fmt = exportFormat();
return {
format: fmt ? fmt.id : 'mp4-h264',
resolution: e.resolution, fps: e.fps, quality: e.quality, encoder: e.encoder, speed: e.speed,
duration: { mode: dur.mode, seconds: dur.seconds, bars: dur.bars, bpm: dur.bpm },
chapters: !!e.chapters && (!fmt || fmt.kind === 'video'),
sceneName: activeSceneName(),
};
}

async function queueAddFiles(files) {
if (!files || !files.length) return;
const dir = await window.api.pickExportFolder(exportQueueDir || undefined);
if (!dir) return;
exportQueueDir = dir;
push(true); // eklenen işler sahnenin şu anki hâlini dondursun
const fmt = exportFormat();
const sep = dir.indexOf('\\') >= 0 ? '\\' : '/';
const items = files.map((a) => ({
audioPath: a, outputPath: dir.replace(/[\\/]+$/, '') + sep + queueItemName(a, fmt), opts: queueItemOpts(),
}));
const r = await window.api.exportQueueAdd(items);
if (r && r.added && r.added.length < items.length) {
P().toast(tr('Bazı işler eklenemedi (en çok 200 iş, geçerli bir yol gerekir).'), 'warn');
}
}

function renderExportQueue() {
const box = $('exportQueueList');
if (!box) return;
box.innerHTML = '';
const st = exportQueueSt || { items: [] };
if (!st.items.length) {
box.appendChild(el('div', { class: 'studio-note dim-hint', text: 'Kuyruk boş.' }));
}
for (const it of st.items) {
const [label, icon] = QUEUE_STATUS[it.status] || [it.status, ''];
const meta = [it.format, it.resolution, it.fps ? it.fps + ' fps' : '', it.audioName].filter(Boolean).join(' · ');
const actions = [];
if (it.status === 'error' || it.status === 'cancelled') {
actions.push(el('button', { class: 'btn ghost small', type: 'button', icon: 'refresh', text: 'Yeniden Dene',
onclick: () => window.api.exportQueueRetry(it.id) }));
}
if (it.status !== 'running') {
actions.push(el('button', { class: 'btn ghost small', type: 'button', icon: 'trash', text: 'Kaldır',
onclick: () => window.api.exportQueueRemove(it.id) }));
}
const row = el('div', { class: 'export-queue-row q-' + it.status, title: it.message || it.output || it.outputPath }, [
el('div', { class: 'q-main' }, [
el('div', { class: 'q-name', text: it.name }),
el('div', { class: 'q-meta', text: meta }),
]),
el('div', { class: 'q-status', icon, text: label }),
el('div', { class: 'q-actions' }, actions),
]);
box.appendChild(row);
}
const run = $('exportQueueRun');
if (run) {
const waiting = st.items.some((x) => x.status === 'waiting');
if (window.SVIcons) window.SVIcons.set(run, st.running ? 'stop' : 'play', st.running ? 'Kuyruğu Durdur' : 'Kuyruğu Başlat');
run.className = 'btn ' + (st.running ? 'danger' : 'primary') + ' small';
run.disabled = !st.running && !waiting;
}
}

function exportQueueCtrl() {
const add = el('button', {
class: 'btn ghost small', type: 'button', icon: 'plus', text: 'Kuyruğa Ekle',
onclick: async () => {
if (!exportAudioPath) { setExportStatus('Önce bir ses dosyası seçin.', 'err'); return; }
await queueAddFiles([exportAudioPath]);
},
});
const addMany = el('button', {
class: 'btn ghost small', type: 'button', icon: 'music', text: 'Birden Çok Ses Ekle',
onclick: async () => queueAddFiles(await window.api.pickExportAudioMulti()),
});
const run = el('button', {
class: 'btn primary small', type: 'button', id: 'exportQueueRun', icon: 'play', text: 'Kuyruğu Başlat',
onclick: async () => {
if (exportQueueSt.running) { await window.api.exportQueueStop(); return; }
push(true);
const r = await window.api.exportQueueStart();
if (r && r.ok === false && r.error && r.error !== 'running') setExportStatus(r.error, 'err', 'warning');
},
});
const clear = el('button', {
class: 'btn ghost small', type: 'button', icon: 'check-circle', text: 'Bitenleri Temizle',
onclick: () => window.api.exportQueueClear(),
});
const list = el('div', { class: 'export-queue', id: 'exportQueueList' });
const box = el('div', { class: 'ctrl' }, [
el('label', { class: 'lbl', text: 'Kuyruk' }),
el('div', { class: 'studio-note dim-hint', text: 'Her iş eklendiği andaki sahneyi ve ayarları kullanır; biri hata verse de sonraki çalışır. Bir sahneyi birden çok şarkıya ya da bir şarkıyı birden çok çözünürlüğe aynı klasöre çıkarmak için.' }),
el('div', { class: 'row', style: 'flex-wrap:wrap;gap:6px' }, [add, addMany, run, clear]),
list,
]);
setTimeout(renderExportQueue, 0);
return box;
}

// --- Video dışa aktarma: çalıştır düğmesi + ilerleme ---
function exportPanelCtrl() {
const runBtn = el('button', {
Expand Down Expand Up @@ -3484,6 +3613,7 @@
],
},
{ type: 'exportpanel', tail: true },
{ type: 'exportqueue', tail: true },
],
},
];
Expand Down Expand Up @@ -6395,6 +6525,19 @@
if (d.eta != null) line += ' · kalan ' + fmtClock(d.eta);
setExportStatus(line);
});
// Kuyruk durumu (#526): liste kendi kutusunda tazeleniyor
try { exportQueueSt = (await window.api.exportQueueState()) || exportQueueSt; } catch { /* eski ana süreç */ }
if (window.api.onExportQueue) {
window.api.onExportQueue((d) => {
exportQueueSt = d || { running: false, items: [] };
/* Kuyruk koşarken ilerleme çubuğu ve İptal (o anki işi iptal eder)
görünür. Kuyruk bitince açtığı durumu kapatıyor: son işin bitiş
olayından SONRA gelen bildirim düğmeyi kilitli bırakıyordu. */
if (exportQueueSt.running && !exporting) { exporting = true; exportByQueue = true; setExportUI(true); }
else if (!exportQueueSt.running && exportByQueue) { exporting = false; exportByQueue = false; setExportUI(false); }
renderExportQueue();
});
}
window.api.onExportDone((d) => {
exporting = false;
setExportUI(false);
Expand Down
Loading
Loading