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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ GitHub. The full reasoning and the measurements behind each change are in [ROADM
31 s to 11 s, MilkDrop at 1080p60 from 31 s to 13 s, and MilkDrop at 4K from 60–67 s to 18–19 s.
The conversion matches ffmpeg's to within one level on luma; the scene itself is drawn exactly as
before.
- **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
audio is cut (a trimmed MP3 is still copied, not re-encoded). An empty timeline is refused with
a reason instead of rendering nothing.
- **Scene and BPM go into the file [528].** The save dialog suggests "Track - Scene - 128 BPM",
with characters Windows forbids and reserved names like CON removed. The file's comment carries
the scene name and BPM next to the title, artist and album.
- **A cancelled export leaves nothing behind [532].** The file is written under a `.partial` name
and moved to its real name only when ffmpeg finishes cleanly, so a cancel, an error or quitting
the app never leaves a half-written file at the chosen path. Quitting during an export used to
leave ffmpeg running; it is now stopped and its partial output removed. The status line shows
frames per second and the time left.
- **The panel opens without waiting for the encoder probe [521].** GPU encoders are tested one at
a time, and a saved "GPU" choice is no longer rewritten to "CPU" on a machine without one; the
export runs on the CPU and the box shows it.
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-3123%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-3132%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 @@ -933,7 +933,9 @@ the fixtures on the left take the colours on the left of the screen.
1080×1080, 24 to 60 fps. The GPU encoder is NVIDIA NVENC, AMD AMF or Intel Quick Sync, whichever
passes a test encode on your machine. Colours are converted and tagged as BT.709, so players
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.
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.
- **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 @@ -1713,7 +1715,7 @@ npm test
npm start -- --smoke
```

**3123 unit tests, all passing.** They are written to check answers, not to exercise lines:
**3132 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
8 changes: 5 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-3123%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-3132%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 @@ -932,7 +932,9 @@ ekranın solundaki renkleri alıyor.
24–60 fps. GPU kodlayıcısı NVIDIA NVENC, AMD AMF ya da Intel Quick Sync; hangisi bilgisayarınızda
deneme kodlamasını geçerse. Renkler BT.709 olarak çevrilip etiketleniyor, oynatıcı ekranda
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.
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.
- Çı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 @@ -1716,7 +1718,7 @@ npm test
npm start -- --smoke
```

**3123 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
**3132 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
33 changes: 31 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) and about 3× faster export (#531) done; profiles, queue, chapters and the rest 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) done; profiles, queue, chapters 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
```

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

### Duration, audio, metadata and clean cancel (#523, #524, #528, #532) · done

- **Duration.** The export lasts the audio file's length (the default), a fixed number of seconds, the timeline's length (latest clip, marker or loop end), or N bars. Bars use a typed BPM or, when it is 0, the timeline's tempo map. The main process measures the audio with ffmpeg and resolves the length before it starts. The renderer draws exactly `ceil(seconds × fps)` frames, and the frames past the end of the audio are drawn with silence.
- **Audio follows the video's length (#524).** If the audio is shorter, it is padded with `apad` and re-encoded, because a copied stream cannot be filtered. If it is longer, it is cut with `-t`, and an MP3 or AAC source is still copied. Each container keeps its codec: AAC or a copy in MP4, PCM s24 in MOV, Opus in WebM, and no audio for GIF, APNG or frame sequences.
- **Metadata (#528).**
- The save dialog suggests "Track - Scene - 128 BPM".
- Characters Windows forbids, reserved names (CON, NUL, COM1…) and trailing dots or spaces are removed, and the name is capped at 150 characters.
- The file's `comment` carries `Scene: … · BPM: …` next to the title, artist and album. MP4 and MOV do not write custom keys, and `comment` is read everywhere.
- Over MCP, an export without a scene name takes the active scene.
- **No partial file at the final path (#532).**
- **Rename on success.** Output goes to `name.partial.ext`, or `folder.partial` for a sequence, and is renamed only after ffmpeg exits with 0. If the rename fails, the job reports an error and points at the partial file instead of losing it.
- **Cancel or error.** The partial output is removed. A renderer crash still keeps what was written.
- **Quitting.** Quitting during an export now stops ffmpeg and removes the partial file. Before, ffmpeg could keep running after the app closed.
- **Status line.** It shows frames per second and the time left. The clock starts at the first frame, so decoding and scene setup do not inflate it.
- **Measured in the app** with 5 s of audio:

| job | frames | video / audio | audio |
|---|---|---|---|
| 8 s fixed | 240 | 8.000 / 8.000 s | AAC; silence after 5 s (−inf dB) |
| 3 s fixed, WAV | 90 | 3.000 / 3.000 s | AAC |
| 3 s fixed, MP3 | 90 | 3.000 / 3.005 s | copied; packet boundary |
| 8 bars at 120 BPM | 480 | 16.000 / 16.000 s | comment "BPM: 120" |
| timeline, marker at 6 s | 180 | 6.000 / 6.000 s | |
| empty timeline | — | refused with a reason | |
| cancel after 2.5 s | — | no file, no `.partial` | |
| quit after 3 s | — | no file, no `.partial`, no ffmpeg left | |

A 6 s PNG sequence over 5 s of audio wrote 180 frames into the final folder.

### Per-application capture on Linux and macOS (#514, #517) · done, macOS unverified

- **Linux** needs no new binary. It lists applications from `pactl list sink-inputs` (always under `LC_ALL=C`; JSON on pactl 16+, text before) and captures each matching stream with `parec --monitor-stream=<idx>` as 48 kHz stereo float32, the format the Windows helper delivers. A clock-driven mixer adds the streams and sends silence when nothing plays.
Expand Down
65 changes: 62 additions & 3 deletions src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3385,6 +3385,30 @@
type: 'select', path: 'export.format', label: 'Biçim', rebuild: true,
options: () => exportFormatOptions(),
},
{
type: 'select', path: 'export.duration.mode', label: 'Süre', rebuild: true,
options: [
{ value: 'audio', label: 'Ses dosyasının süresi' },
{ value: 'seconds', label: 'Sabit süre' },
{ value: 'timeline', label: 'Zaman çizelgesinin uzunluğu' },
{ value: 'bars', label: 'Ölçü sayısı (BPM\'den)' },
],
},
{
type: 'slider', path: 'export.duration.seconds', label: 'Süre (saniye)',
min: 1, max: 1800, step: 1, fmt: (v) => fmtClock(v),
show: () => exportDuration().mode === 'seconds',
},
{
type: 'slider', path: 'export.duration.bars', label: 'Ölçü',
min: 1, max: 512, step: 1,
show: () => exportDuration().mode === 'bars',
},
{
type: 'slider', path: 'export.duration.bpm', label: 'BPM',
min: 0, max: 300, step: 1, fmt: (v) => (Math.round(v) > 0 ? String(Math.round(v)) : 'Zaman çizelgesi'),
show: () => exportDuration().mode === 'bars',
},
{
type: 'select', path: 'export.resolution', label: 'Çözünürlük',
options: [
Expand Down Expand Up @@ -3443,6 +3467,27 @@
];
}

/* Süre kaynağı (#523); eski kayıtlarda alan yok. */
function exportDuration() {
const e = cfg.export || (cfg.export = {});
if (!e.duration || typeof e.duration !== 'object') e.duration = { mode: 'audio', seconds: 30, bars: 16, bpm: 0 };
return e.duration;
}
// 75 -> "1:15", 3725 -> "1:02:05"
function fmtClock(sec) {
const t = Math.max(0, Math.round(Number(sec) || 0));
const h = Math.floor(t / 3600);
const m = Math.floor((t % 3600) / 60);
const s = String(t % 60).padStart(2, '0');
return h ? h + ':' + String(m).padStart(2, '0') + ':' + s : m + ':' + s;
}
/* Dosya adı ve içine yazılan meta veri için o anki sahne (#528). */
function activeSceneName() {
const id = activeSceneId || cfg._activeSceneId;
const sc = id && Array.isArray(cfg.scenes) ? cfg.scenes.find((x) => x && x.id === id) : null;
return sc && sc.name ? String(sc.name) : '';
}

/* Seçili biçimin ana süreçten gelen tanımı; liste henüz gelmediyse null. */
function exportFormat() {
const id = (cfg.export && cfg.export.format) || 'mp4-h264';
Expand Down Expand Up @@ -5742,7 +5787,14 @@
const base = (exportAudioName || 'gorsellestirme').replace(/\.[^.]+$/, '');
const fmt = exportFormat();
const fmtId = fmt ? fmt.id : 'mp4-h264';
const outputPath = await window.api.pickExportOutput(base + '.' + (fmt ? fmt.ext : 'mp4'), fmtId);
const dur = exportDuration();
const sceneName = activeSceneName();
/* Varsayılan ad: "Parça - Sahne - 128 BPM" (#528); kullanıcı değiştirebilir */
const FX = window.SVExportFormats;
const suggested = FX
? FX.outputName({ title: base, scene: sceneName, bpm: dur.mode === 'bars' ? dur.bpm : 0 }, fmt ? fmt.ext : 'mp4')
: base + '.' + (fmt ? fmt.ext : 'mp4');
const outputPath = await window.api.pickExportOutput(suggested, fmtId);
if (!outputPath) return;

push(true); // en güncel görsel ayarları ana sürece gönder
Expand All @@ -5751,6 +5803,8 @@
audioPath: exportAudioPath,
outputPath,
format: fmtId,
duration: { mode: dur.mode, seconds: dur.seconds, bars: dur.bars, bpm: dur.bpm },
sceneName,
resolution: cfg.export.resolution,
fps: cfg.export.fps,
quality: cfg.export.quality,
Expand Down Expand Up @@ -6287,7 +6341,7 @@
let exportEnc = '';
window.api.onExportProgress((d) => {
if (d.phase === 'start') {
exportEnc = d.encoder === 'gpu' ? 'GPU/NVENC' : 'CPU/libx264';
exportEnc = d.encoderLabel || (d.encoder === 'gpu' ? 'GPU' : 'CPU');
return;
}
if (d.phase === 'encode') {
Expand All @@ -6301,7 +6355,12 @@
exportPct = pct;
const fill = $('exportProgressFill');
if (fill) fill.style.width = pct + '%';
setExportStatus('Render ediliyor [' + exportEnc + ']… %' + pct + ' (' + d.done + ' / ' + d.total + ' kare)');
/* Hız ve kalan süre (#532) ' · ' ile ekleniyor: çevirici satırı bu
ayırıcıdan bölüp parçaları ayrı çeviriyor. */
let line = 'Render ediliyor [' + exportEnc + ']… %' + pct + ' (' + d.done + ' / ' + d.total + ' kare)';
if (d.fps > 0) line += ' · ' + (d.fps >= 10 ? Math.round(d.fps) : d.fps) + ' fps';
if (d.eta != null) line += ' · kalan ' + fmtClock(d.eta);
setExportStatus(line);
});
window.api.onExportDone((d) => {
exporting = false;
Expand Down
7 changes: 6 additions & 1 deletion src/exporter/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,12 @@
if (prof) { prof.decodeMs = performance.now() - tp; tp = performance.now(); }
await setup(job);
if (prof) prof.setupMs = performance.now() - tp;
totalFrames = Math.max(1, Math.ceil(duration * fps));
/* Süre ana süreçten gelebilir (#523): sabit saniye, zaman çizelgesi ya
da N ölçü. Ses bittikten sonraki kareler sessizlikle çiziliyor
(analyzeAt dosya dışını sıfır okuyor); ffmpeg sesi de aynı süreye
dolduruyor ya da kesiyor. */
const renderSec = Number(job.durationSec) > 0 ? Number(job.durationSec) : duration;
totalFrames = Math.max(1, Math.ceil(renderSec * fps));
window.exp.ready(totalFrames);

// Boru hattı (pipeline): render, IPC ve ffmpeg kodlaması ÜST ÜSTE çalışsın.
Expand Down
Loading
Loading