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 @@ -36,6 +36,12 @@ 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.
- **One-click profiles [522].** YouTube 1080p60, YouTube 4K60 (H.265), Reels / Shorts / TikTok
1080×1920, Instagram square, a Twitch / OBS overlay in WebM with alpha, a ProRes 422 HQ 4K
master and a 720p GIF loop. A profile sets the format, size, frame rate and quality; the GPU or
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.
- **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
4 changes: 2 additions & 2 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-3132%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-3138%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 @@ -1715,7 +1715,7 @@ npm test
npm start -- --smoke
```

**3132 unit tests, all passing.** They are written to check answers, not to exercise lines:
**3138 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
4 changes: 2 additions & 2 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-3132%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-3138%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 @@ -1718,7 +1718,7 @@ npm test
npm start -- --smoke
```

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

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

### 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.
- **What a profile sets.** Each one fixes format, resolution, frame rate and quality. Colour is BT.709 for every format already. The encoder (GPU/CPU) and the speed balance remain the user's choice.
- **No stored field.** The panel derives the profile from the current settings (`matchProfile`). Choosing one fills the fields, and changing any of them by hand shows "Custom". In the panel, changing the frame rate first left the old profile showing, because only the format select rebuilt the card. Resolution, frame rate and quality now rebuild it too.
- **Availability.** Only profiles whose format works on this machine are listed.
- **MCP.** `profile` supplies defaults, explicit arguments override them, and the profile's format decides the allowed extension.

### 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.
Expand Down
36 changes: 31 additions & 5 deletions src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,13 +847,16 @@
}

function selectCtrl(def) {
const cur = getPath(cfg, def.path);
/* `value`/`onChange`: ayarda kendi alanı olmayan, başka alanlardan
türeyen seçimler için (dışa aktarma profili). */
const cur = typeof def.value === 'function' ? def.value() : getPath(cfg, def.path);
const opts = typeof def.options === 'function' ? def.options() : def.options;
const sel = el('select', {
onchange: (e) => {
let v = e.target.value;
if (def.numeric) v = parseFloat(v);
setPath(cfg, def.path, v);
if (typeof def.onChange === 'function') def.onChange(v);
else setPath(cfg, def.path, v);
if (def.rebuild) render();
push(true);
},
Expand Down Expand Up @@ -3381,6 +3384,19 @@
desc: 'Bir ses dosyası seçin; mevcut sahne ayarlarıyla kayıpsız videoya dönüştürülür. Ekran kaydı değildir — her kare birebir render edilir.',
controls: [
{ type: 'audiofile' },
{
/* Hazır profil (#522): ayarların kendisinden türüyor, ayrı bir
alanı yok. Seçmek alanları dolduruyor; bir alan elle
değişince "Özel"e dönüyor. */
type: 'select', path: 'export.profile', label: 'Hazır Profil', rebuild: true,
value: () => (window.SVExportFormats ? window.SVExportFormats.matchProfile(cfg.export) : 'custom'),
options: () => exportProfileOptions(),
onChange: (id) => {
const FX = window.SVExportFormats;
if (!FX || id === 'custom') return;
cfg.export = FX.applyProfile(cfg.export, id);
},
},
{
type: 'select', path: 'export.format', label: 'Biçim', rebuild: true,
options: () => exportFormatOptions(),
Expand Down Expand Up @@ -3410,7 +3426,7 @@
show: () => exportDuration().mode === 'bars',
},
{
type: 'select', path: 'export.resolution', label: 'Çözünürlük',
type: 'select', path: 'export.resolution', label: 'Çözünürlük', rebuild: true, // profil seçimi güncellensin (#522)
options: [
{ value: '720p', label: '720p — 1280×720' },
{ value: '1080p', label: '1080p — 1920×1080 (Full HD)' },
Expand All @@ -3421,7 +3437,7 @@
],
},
{
type: 'select', path: 'export.fps', label: 'Kare Hızı (FPS)', numeric: true,
type: 'select', path: 'export.fps', label: 'Kare Hızı (FPS)', numeric: true, rebuild: true,
options: () => {
const f = exportFormat();
const max = f && f.fpsMax ? f.fpsMax : 60;
Expand All @@ -3444,7 +3460,7 @@
fallback: () => 'cpu',
},
{
type: 'select', path: 'export.quality', label: 'Kalite',
type: 'select', path: 'export.quality', label: 'Kalite', rebuild: true,
group: 'Kodlama', advanced: true,
options: [
{ value: 'visually-lossless', label: 'Görsel Kayıpsız (en yüksek)' },
Expand All @@ -3467,6 +3483,16 @@
];
}

/* Profiller: yalnız biçimi bu makinede çalışanlar. */
function exportProfileOptions() {
const FX = window.SVExportFormats;
if (!FX) return [{ value: 'custom', label: 'Özel' }];
const list = (exportCaps && exportCaps.formats) || [];
const ok = (fid) => !list.length || list.some((f) => f.id === fid && f.available);
return [{ value: 'custom', label: 'Özel' }]
.concat(FX.PROFILES.filter((p) => ok(p.set.format)).map((p) => ({ value: p.id, label: p.label[0] })));
}

/* Süre kaynağı (#523); eski kayıtlarda alan yok. */
function exportDuration() {
const e = cfg.export || (cfg.export = {});
Expand Down
44 changes: 44 additions & 0 deletions src/shared/export-formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,49 @@
return args;
}

// ---------------------------------------------------------- profiller (#522)

/* Tek tıkla ayar. Her profil biçimi, çözünürlüğü, kare hızını ve kaliteyi
sabitliyor; renk her biçimde zaten BT.709. Kodlayıcı (GPU/CPU) ve hız
dengesi kullanıcının tercihi olarak kalıyor. Ayrıntılı ayarlar profilin
altında duruyor: bir alan değişince seçim kendiliğinden "Özel" oluyor. */
const PROFILES = [
{ id: 'youtube-1080p60', label: ['YouTube 1080p60', 'YouTube 1080p60'],
set: { format: 'mp4-h264', resolution: '1080p', fps: 60, quality: 'high' } },
{ id: 'youtube-4k60', label: ['YouTube 4K60 (H.265)', 'YouTube 4K60 (H.265)'],
set: { format: 'mp4-hevc', resolution: '2160p', fps: 60, quality: 'high' } },
{ id: 'reels-shorts', label: ['Reels / Shorts / TikTok — 1080×1920', 'Reels / Shorts / TikTok — 1080×1920'],
set: { format: 'mp4-h264', resolution: '1080x1920', fps: 30, quality: 'high' } },
{ id: 'instagram-square', label: ['Instagram kare — 1080×1080', 'Instagram square — 1080×1080'],
set: { format: 'mp4-h264', resolution: '1080x1080', fps: 30, quality: 'high' } },
{ id: 'twitch-overlay', label: ['Twitch / OBS bindirme — WebM alfa', 'Twitch / OBS overlay — WebM alpha'],
set: { format: 'webm-vp9-alpha', resolution: '1080p', fps: 60, quality: 'high' } },
{ id: 'master-4k', label: ['Ana kopya 4K — ProRes 422 HQ', 'Master 4K — ProRes 422 HQ'],
set: { format: 'mov-prores', resolution: '2160p', fps: 60, quality: 'visually-lossless' } },
{ id: 'gif-loop', label: ['GIF döngü — 720p', 'GIF loop — 720p'],
set: { format: 'gif', resolution: '720p', fps: 24, quality: 'high' } },
];

function profile(id) {
for (let i = 0; i < PROFILES.length; i++) if (PROFILES[i].id === id) return PROFILES[i];
return null;
}

/* Şu anki ayarlara birebir uyan profil, yoksa 'custom'. */
function matchProfile(exp) {
const e = exp || {};
for (const p of PROFILES) {
if (Object.keys(p.set).every((k) => String(e[k]) === String(p.set[k]))) return p.id;
}
return 'custom';
}

/* Profili ayarlara uygular (yeni nesne); bilinmeyen profilde aynı ayar. */
function applyProfile(exp, id) {
const p = profile(id);
return Object.assign({}, exp || {}, p ? p.set : {});
}

// ------------------------------------------------------------ süre (#523)

const DURATION_MODES = ['audio', 'seconds', 'timeline', 'bars'];
Expand Down Expand Up @@ -427,6 +470,7 @@
RESOLUTIONS, FPS, QUALITIES, SPEEDS, ENCODERS, FORMATS, DEFAULT_FORMAT,
format, extensions, encodersFor, pickEncoder, available,
videoArgs, audioArgs, buildArgs, probeArgs, sequencePattern, takesYuv420,
PROFILES, profile, matchProfile, applyProfile,
DURATION_MODES, MAX_SECONDS, resolveDuration, sanitizeFileName, outputName, partialPath, progressStats,
};

Expand Down
4 changes: 4 additions & 0 deletions src/shared/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -2965,6 +2965,10 @@
'Video Dışa Aktar (MP3 → Video)': 'Video Export (MP3 → Video)',
/* Dışa aktarma biçimleri (#521); export-formats.js etiketleriyle aynı */
'Dikey — 1080×1920 (Reels / Shorts)': 'Vertical — 1080×1920 (Reels / Shorts)',
/* Dışa aktarma profilleri (#522); export-formats.js etiketleriyle aynı */
'Hazır Profil': 'Preset Profile', 'Instagram kare — 1080×1080': 'Instagram square — 1080×1080',
'Twitch / OBS bindirme — WebM alfa': 'Twitch / OBS overlay — WebM alpha',
'Ana kopya 4K — ProRes 422 HQ': 'Master 4K — ProRes 422 HQ', 'GIF döngü — 720p': 'GIF loop — 720p',
'Ses dosyasının süresi': 'Length of the audio file', 'Sabit süre': 'Fixed length',
'Zaman çizelgesinin uzunluğu': 'Length of the timeline', 'Ölçü sayısı (BPM\'den)': 'Number of bars (from the BPM)',
'Süre (saniye)': 'Duration (seconds)', 'Zaman çizelgesi': 'Timeline',
Expand Down
10 changes: 10 additions & 0 deletions src/shared/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@
const EXPORT_FORMAT_IDS = EXPORT_FX ? EXPORT_FX.FORMATS.map(function (f) { return f.id; }) : ['mp4-h264'];
const EXPORT_RESOLUTIONS = EXPORT_FX ? Object.keys(EXPORT_FX.RESOLUTIONS) : ['720p', '1080p', '1440p', '2160p'];
const EXPORT_FPS = EXPORT_FX ? EXPORT_FX.FPS.slice() : [30, 60];
const EXPORT_PROFILE_IDS = EXPORT_FX ? EXPORT_FX.PROFILES.map(function (p) { return p.id; }) : [];
const OVERWRITE = sp('boolean', 'Replace the file if it already exists. Default false.');
const MAPPING_KEYS = {
enabled: sp('boolean', 'Turn mapping on for this display.'),
Expand Down Expand Up @@ -1077,6 +1078,7 @@
props: {
audioPath: sp('string', 'Absolute path of the audio file.'),
outputPath: sp('string', 'Absolute path ending with the extension of the format (.mp4, .mov, .webm, .gif, .png, .exr).'),
profile: sp('string', 'One-click preset for format, resolution, fps and quality; explicit arguments override it.', { enum: EXPORT_PROFILE_IDS }),
format: sp('string', 'Container and codec. Default mp4-h264. Alpha: mov-prores4444, webm-vp9-alpha, apng, png-seq, exr-seq.', { enum: EXPORT_FORMAT_IDS }),
resolution: sp('string', 'Video size. Default 1080p.', { enum: EXPORT_RESOLUTIONS }),
fps: sp('integer', 'Frame rate. Default 60. GIF and APNG cap at 30.', { enum: EXPORT_FPS }),
Expand Down Expand Up @@ -2055,6 +2057,14 @@
tool('sv_start_export', 'export', 'Start an offline video export. Requires audioPath and outputPath. format picks the container and codec; outputPath must end with the extension of that format. A frame sequence writes into a new folder named after outputPath.', function (args, ctx) {
if (!ctx.startExport) return fail('Export is not available in this process.');
const FX = formatsApi();
/* Profil (#522): biçim, çözünürlük, kare hızı ve kalite için varsayılan;
açıkça verilen argüman profilin üstüne yazılır. */
if (args && args.profile != null) {
const prof = FX && FX.profile(args.profile);
if (!prof) return fail('Unknown profile: ' + args.profile);
args = Object.assign({}, prof.set, args);
delete args.profile;
}
const fmt = FX ? FX.format((args && args.format) || FX.DEFAULT_FORMAT) : null;
if (FX && !fmt) return fail('Unknown format: ' + (args && args.format));
const outBad = outPathError(ctx, args && args.outputPath, [fmt ? fmt.ext : 'mp4'], args && args.overwrite);
Expand Down
Loading
Loading