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.
- **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.
Measured on H.264 (x264 and NVENC), H.265, VP9 and ProRes: chapters of exactly 60, 45 and 45
frames for markers at 2 s and 3.5 s, each starting on a keyframe. AMD's AMF ignores forced
keyframes, so chapters use the next encoder that honours them.
- **Choose how long the export runs [523].** The length of the audio file (as before), a fixed
number of seconds, the length of the timeline, or N bars at a BPM (or at the timeline's tempo).
Video and audio come out exactly that long: shorter audio is padded with silence, and longer
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-3138%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-3145%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 @@ -935,7 +935,9 @@ the fixtures on the left take the colours on the left of the screen.
show what the screen showed. MP4 and WebM export about three times faster than in v3.1.5: a
1080p60 song renders in about half its own length. The length can be the audio file, a fixed
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.
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.
- **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 @@ -1715,7 +1717,7 @@ npm test
npm start -- --smoke
```

**3138 unit tests, all passing.** They are written to check answers, not to exercise lines:
**3145 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
9 changes: 6 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-3138%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-3145%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 @@ -934,7 +934,10 @@ ekranın solundaki renkleri alıyor.
görüneni gösteriyor. MP4 ve WebM dışa aktarımı v3.1.5'e göre yaklaşık üç kat hızlı: 1080p60
bir şarkı kendi süresinin yarısında çıkıyor. Süre ses dosyasından, sabit bir süreden, zaman
çizelgesinden ya da ölçü sayısından alınabiliyor; dosya parça, sahne ve BPM ile adlandırılıp
etiketleniyor, iptal edilen dışa aktarım yarım dosya bırakmı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.
- Çı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 @@ -1718,7 +1721,7 @@ npm test
npm start -- --smoke
```

**3138 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
**3145 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
22 changes: 20 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) done; queue, chapters 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) done; queue and 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
```

- **3138 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
- **3145 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,24 @@ 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.

### 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.
- **Output.**
- Files go to a folder named after the chosen file, one per chapter, named from the marker: `01 - Intro.mp4`, `02 - Drop.mp4`.
- A marker at 0 names the first chapter. Without one, the first chapter is called "Start".
- Markers that land on the same frame count once.
- The folder is written as `.partial` and renamed when the job succeeds, like a frame sequence.
- If the number of files does not match the plan, the job fails instead of silently missing a chapter.
- **Two measured traps:**
- **Exact split times drop cuts.** Given the exact time, the segment muxer skipped the cut whose packet time rounded a hair below it: x264 produced 2 files instead of 3. Split times are now half a frame early, and the keyframe stays at the exact frame.
- **AMF ignores forced keyframes.** It produced one file, and none of its options enforces an IDR at a given time. Chapters therefore pick the next GPU encoder that honours forced keyframes (NVENC with `-forced-idr 1`, Quick Sync), and fall back to the CPU.
- **Measured.** 5 s of audio, markers at 2.0 s ("Nakarat") and 3.5 s ("Son: Bitiş"), 30 fps.
- x264, NVENC, x265, VP9 and ProRes each give 60 / 45 / 45 frames, and every file starts on a keyframe.
- The names come out as `01 - Başlangıç`, `02 - Nakarat` and `03 - Son Bitiş`.
- Audio is cut on AAC frame boundaries, so it differs from the video by up to a few tens of milliseconds per file: 2.043 / 1.486 / 1.494 s against 2.0 / 1.5 / 1.5 s.
- **Found on the way.** The bench driver's first job sometimes ran on the panel's settings instead of its own, because the panel sends its config while it loads. This is what produced the odd first-job hash noted under #531. The driver now holds panel updates the same way the self-test does.

### Preset profiles (#522) · done

- **The profiles are data** in `export-formats.js`: YouTube 1080p60, YouTube 4K60 (H.265), Reels / Shorts / TikTok 1080×1920 at 30 fps, Instagram square 1080×1080, a Twitch / OBS overlay as 1080p60 WebM VP9 with alpha, a 4K60 ProRes 422 HQ master, and a 720p GIF loop at 24 fps.
Expand Down
7 changes: 7 additions & 0 deletions src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3425,6 +3425,12 @@
min: 0, max: 300, step: 1, fmt: (v) => (Math.round(v) > 0 ? String(Math.round(v)) : 'Zaman çizelgesi'),
show: () => exportDuration().mode === 'bars',
},
{
/* Bölümler (#527): tek render, zaman çizelgesi işaretçilerinde
kesilip işaretçi adlarıyla bir klasöre. Yalnız video biçimleri. */
type: 'toggle', path: 'export.chapters', label: 'İşaretçilerden bölümlere ayır',
show: () => { const f = exportFormat(); return !f || f.kind === 'video'; },
},
{
type: 'select', path: 'export.resolution', label: 'Çözünürlük', rebuild: true, // profil seçimi güncellensin (#522)
options: [
Expand Down Expand Up @@ -5830,6 +5836,7 @@
outputPath,
format: fmtId,
duration: { mode: dur.mode, seconds: dur.seconds, bars: dur.bars, bpm: dur.bpm },
chapters: !!(cfg.export && cfg.export.chapters) && (!fmt || fmt.kind === 'video'),
sceneName,
resolution: cfg.export.resolution,
fps: cfg.export.fps,
Expand Down
65 changes: 64 additions & 1 deletion src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4290,6 +4290,27 @@ function removePartialDir(dir, proc) {
if (proc && proc.exitCode == null && proc.signalCode == null) proc.once('close', () => setTimeout(drop, 50));
else setTimeout(drop, 50);
}
/* Segment yazıcısının 001.mp4, 002.mp4… dosyalarına işaretçi adı verir
(#527). Sayı tutmazsa iş hata sayılıyor: eksik bölüm sessizce geçmesin. */
function nameChapters(dir, names, ext) {
let files;
try {
files = fs.readdirSync(dir).filter((f) => /^\d{3}\./.test(f)).sort();
} catch (err) {
return { ok: false, error: err.message };
}
if (files.length !== names.length) {
return { ok: false, error: trUi('Bölüm sayısı tutmuyor: ', 'Chapter count does not match: ') + files.length + ' / ' + names.length };
}
const word = trUi('Bölüm', 'Chapter');
try {
files.forEach((f, i) => fs.renameSync(path.join(dir, f), path.join(dir, exportFormats.chapterFileName(i, names[i], ext, word))));
} catch (err) {
return { ok: false, error: err.message };
}
return { ok: true };
}

/* Geçici çıktıyı asıl adına taşır. Dosyada hedef varsa üstüne yazılır
(kaydetme penceresi bunu zaten sordu); kare dizisinde klasör adı
doluysa yanına numaralı yenisi seçilir. Windows tutamacı hemen
Expand Down Expand Up @@ -4481,7 +4502,10 @@ async function startExportJob(opts, bench) {
(NVIDIA, AMD, Intel sırasıyla), yoksa CPU. Deneme kodlamasını geçmemiş
bir kodlayıcı hiç seçilmez. */
const okMap = await probeEncoders();
const encoderId = exportFormats.pickEncoder(fmt, opts.encoder === 'gpu' ? 'gpu' : 'cpu', (e) => okMap[e]);
// Bölümleme zorunlu anahtar kare istiyor; AMF onu yok sayıyor (#527)
const wantChapters = opts.chapters === true && fmt.kind === 'video';
const encoderId = exportFormats.pickEncoder(fmt, opts.encoder === 'gpu' ? 'gpu' : 'cpu', (e) => okMap[e],
{ forcedKeys: wantChapters });
if (!encoderId) {
return { ok: false, error: trUi('Bu biçimin kodlayıcısı bu bilgisayarda çalışmıyor: ', 'No working encoder for this format on this computer: ') + fmt.id };
}
Expand Down Expand Up @@ -4558,6 +4582,29 @@ async function startExportJob(opts, bench) {
/* "Sesin süresi" seçiliyken eski kural geçerli (-shortest): ffmpeg'in
ölçtüğü süreyle çözülen süre arasındaki küçük fark sesi kesmesin. */
const durationSec = dur.mode === 'audio' ? 0 : dur.seconds;

/* Bölümler (#527): TEK render, işaretçilerde zorunlu anahtar kare ve
ffmpeg'in segment yazıcısıyla kesim. Her bölümü ayrı çizmek MilkDrop
gibi geri beslemeli katmanları sınırda farklı başlatırdı. Dosyalar bir
klasöre gidiyor ve işaretçinin adını alıyor; kare dizisindeki gibi önce
`.partial` klasörüne. */
let chapters = null;
if (wantChapters) {
let markers = [];
try { markers = TL.makeTimeline(tlCfg).markers; } catch { markers = []; }
chapters = exportFormats.chapterPlan(markers, dur.seconds, fps, trUi('Başlangıç', 'Start'));
if (!chapters.cuts.length) {
return { ok: false, error: trUi('Bölmek için zaman çizelgesinde bu süre içinde bir işaretçi yok.',
'There is no timeline marker within this length to split at.') };
}
const stem = path.basename(outputPath, path.extname(outputPath));
const parent = path.dirname(outputPath);
seqDir = path.join(parent, stem);
for (let n = 2; fs.existsSync(seqDir) && n < 1000; n++) seqDir = path.join(parent, stem + ' (' + n + ')');
workDir = seqDir + '.partial';
for (let n = 2; fs.existsSync(workDir) && n < 1000; n++) workDir = seqDir + '.partial' + n;
target = workDir.replace(/%/g, '%%') + '/%03d.' + fmt.ext;
}
/* Sahne adı meta veriye (#528): panel gönderiyor; MCP vermezse etkin sahne. */
let sceneName = typeof opts.sceneName === 'string' ? opts.sceneName.slice(0, 120) : '';
if (!sceneName && currentConfig && currentConfig._activeSceneId && Array.isArray(currentConfig.scenes)) {
Expand All @@ -4576,6 +4623,7 @@ async function startExportJob(opts, bench) {
fmt, encoder: encoderId, w, h, fps, quality, speed, inputPix: pix,
threads: Math.max(1, Math.min(8, require('os').cpus().length)),
audioPath, audioExt: path.extname(audioPath), audioSec, durationSec, output: target,
chapters: chapters ? chapters.cuts : null,
meta: { title: track.title, artist: track.artist, album: track.album, scene: sceneName, bpm: bpmMeta },
});

Expand All @@ -4596,6 +4644,7 @@ async function startExportJob(opts, bench) {
exportState = {
outputPath: seqDir || outputPath, seqDir, workDir, partial: workDir || target,
format: fmt.id, encoderId, ffErr: '', encoder, finishAt: 0, cancel: false,
chapterNames: chapters ? chapters.names : null, ext: fmt.ext,
startedAt: 0, total: 0,
};
/* Ölçüm (#531): ana süreç tarafındaki yazma/bekleme süreleri ve kare
Expand Down Expand Up @@ -4796,6 +4845,8 @@ function finalizeExport(status, message, opts) {
const seqDir = exportState.seqDir;
const workDir = exportState.workDir;
const partial = exportState.partial;
const chapterNames = exportState.chapterNames;
const ext = exportState.ext;
const encoder = exportState.encoder;
const encoderId = exportState.encoderId;
const prof = exportState.prof;
Expand Down Expand Up @@ -4833,6 +4884,10 @@ function finalizeExport(status, message, opts) {
hatada geçici çıktı siliniyor; asıl yolda hiçbir zaman yarım dosya
olmuyor. */
let finalOut = out;
if (status === 'done' && chapterNames && workDir) {
const named = nameChapters(workDir, chapterNames, ext);
if (!named.ok) { status = 'error'; message = named.error; }
}
if (status === 'done' && partial) {
const moved = promotePartial(partial, seqDir ? workDir : null, seqDir || out);
if (moved.ok) {
Expand All @@ -4850,6 +4905,8 @@ function finalizeExport(status, message, opts) {
finalOut = partial;
}

// Ölçüm kaydı SON durumu taşısın: ad verme ya da taşıma işi hataya çevirebiliyor
if (prof && lastExportProfile) { lastExportProfile.status = status; lastExportProfile.message = message || ''; }
notifyAdmin('export-done', {
status, output: finalOut, message: message || '', encoder,
encoderLabel: encoderId && exportFormats.ENCODERS[encoderId] ? exportFormats.ENCODERS[encoderId].label : '',
Expand Down Expand Up @@ -5072,6 +5129,11 @@ app.whenReady().then(async () => {
async function runExportBench(file) {
const spec = JSON.parse(fs.readFileSync(file, 'utf8'));
const results = [];
/* Panel açılırken kendi ayarlarını gönderiyor (update-config) ve bu,
işin yapılandırmasını DEĞİŞTİRİYORDU: ilk iş bazen panelin ayarıyla
çizildi (farklı kare özeti), bölümleme ilk işte işaretçi göremedi.
Ölçüm boyunca panelin güncellemeleri öz testteki gibi bekletiliyor. */
smokeHoldStart();
const base = currentConfig;
for (const job of spec.jobs || []) {
/* Profilin kayıtlı ayarları MİRAS ALINMIYOR: dışa aktarıcı iş
Expand All @@ -5096,6 +5158,7 @@ async function runExportBench(file) {
results.push(row);
}
currentConfig = base;
smokeHoldEnd();
fs.writeFileSync(spec.result, JSON.stringify(results, null, 2));
app.exit(0);
}
Expand Down
1 change: 1 addition & 0 deletions src/shared/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,7 @@
/* Süre kaynağı (#523): ses dosyası, sabit saniye, zaman çizelgesinin
uzunluğu ya da N ölçü (bpm 0 = zaman çizelgesinin temposu). */
duration: { mode: 'audio', seconds: 30, bars: 16, bpm: 0 },
chapters: false, // zaman çizelgesi işaretçilerinden bölümlere ayır (#527)
resolution: '1080p', // export-formats.js RESOLUTIONS anahtarı
fps: 60, // 24 | 25 | 30 | 50 | 60
quality: 'visually-lossless', // 'visually-lossless' | 'high' | 'balanced'
Expand Down
Loading
Loading