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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ CaYaDev presets and the audio bridge came after the 3.1.6-beta build; the ones a
- **Default windows** (40–90 Hz, 150–400 Hz, 1.5–4 kHz, 6–14 kHz) come from levels measured on four released tracks: the floor at about the 10th percentile, the ceiling at about the 95th. The spectrum's scale stops at -18 dB, so a loudly mastered kick often reaches the ceiling.
- **Checked** in the running app in Turkish and English: the panel, saving, the source list and the live meters. New tests cover the byte scale, power averaging, narrow and reversed ranges, the dB window, frame-rate independence, invalid settings, and that an unrouted band leaves the configuration untouched.

### Export loop, panel and bridge fixes

- **Seamless loop and Spotify Canvas exports work again [840].** ffmpeg stopped with exit code 234 ("xfade ... constant frame rate; current rate of 1/0 is invalid"): after `trim` and `setpts` the two branches had no frame rate, and `xfade` needs one. Each branch now ends in `fps=<fps>`. Checked with a real ffmpeg run (2 s at 30 fps: 60 frames, yuv420p, exit 0); the same command failed before the change.
- **Bar placement.** Switching "Placement" to Bottom now rebuilds the panel, so the Floor Reflection control appears at once; "Reflection Length" no longer waits for the reflection slider to be above zero.
- **Blend mode "Screen".** In an English UI the blend option showed "Display" because it shared a label with the display section. It has its own label now; the Turkish text is unchanged.
- **Audio bridge badge.** The bridge window showed "Starting" forever when no pairing secret was set. It now says "Secret missing", like the settings page.
- **Studio presets.** A long preset name that is cut off in the list shows in full on hover.
- **Layout import, by design.** A layout file with a text element adds a second visualizer-type layer (text is a sub-type of the visualizer layer): a fresh scene ends up with [background, visualizer, visualizer]. Importing the same file again updates those layers instead of adding more. Covered by a test.

### Layout Editor and theme card fixes

- **Fresh profile no longer breaks on the first drag.** The Visualizer fields showed 0, 0, 0, 0 and the first drag wrote those zeros back, collapsing the bars into a small icon. Empty bar placement now resolves to the real defaults (position-aware, same as the renderer).
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-3577%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-3582%20passing-2ea043.svg)](#tests)
[![Electron](https://img.shields.io/badge/Electron-44-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 @@ -1818,7 +1818,7 @@ npm test
npm start -- --smoke
```

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

_The count is the runner's total (`# tests`). A few tests depend on the environment (a git checkout, Node 22's built-in WebSocket, the electron-builder schema); where that is missing they are reported as skipped, not passed, so `pass + skipped` always equals this number and the skipped count may be 0 on one machine and 1–4 on another._

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-3577%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-3582%20geçiyor-2ea043.svg)](#testler)
[![Electron](https://img.shields.io/badge/Electron-44-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 @@ -1822,7 +1822,7 @@ npm test
npm start -- --smoke
```

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

_Sayı koşunun toplamıdır (`# tests`). Birkaç test ortama bağlı (git çalışma kopyası, Node 22'nin yerleşik WebSocket'i, electron-builder şeması); bunlar eksikse geçti değil atlandı sayılır. Yani `geçen + atlanan` hep bu sayıya eşittir; atlanan sayısı bir makinede 0, başka birinde 1–4 olabilir._

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ npm test
npm start -- --smoke
```

- **3577 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
- **3582 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
8 changes: 5 additions & 3 deletions src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2904,7 +2904,8 @@
// Spektrogram kendi ısı haritasını çizer, parlama uygulanmaz
{ type: 'slider', path: 'visualizer.glow', label: 'Parlama (Glow)', min: 0, max: 1, step: 0.02, percent: true, show: () => v.type !== 'none' && v.type !== 'spectrogram' },
{
type: 'segment', path: 'visualizer.position', label: 'Yerleşim',
// rebuild: Zemin Yansıması denetimi yerleşime bağlı (Orta'da gizli), panel yeniden kurulmazsa Alt'a geçince görünmüyordu
type: 'segment', path: 'visualizer.position', label: 'Yerleşim', rebuild: true,
options: [{ value: 'bottom', label: 'Alt' }, { value: 'center', label: 'Orta' }, { value: 'full', label: 'Tam' }],
show: () => v.type === 'bars',
},
Expand All @@ -2923,7 +2924,8 @@
{ type: 'slider', path: 'visualizer.baseline', label: 'Taban Çizgisi', min: 0, max: 1, step: 0.01, percent: true, nullable: 1, show: () => v.type === 'bars', group: 'Bar Yerleşimi', advanced: true },
/* Zemin yansıması (#842): taban kadranın içindeyken görünür */
{ type: 'slider', path: 'visualizer.reflection', label: 'Zemin Yansıması', min: 0, max: 1, step: 0.02, percent: true, show: () => v.type === 'bars' && v.position !== 'center', group: 'Bar Yerleşimi', advanced: true },
{ type: 'slider', path: 'visualizer.reflectionLength', label: 'Yansıma Boyu', min: 0.05, max: 1, step: 0.01, percent: true, show: () => v.type === 'bars' && v.position !== 'center' && (Number(v.reflection) || 0) > 0, group: 'Bar Yerleşimi', advanced: true },
// Boy, Yansıma 0 iken de görünür: kaydırıcı yeniden çizim tetiklemediği için koşula bağlıyken ilk kaydırışta çıkmıyordu
{ type: 'slider', path: 'visualizer.reflectionLength', label: 'Yansıma Boyu', min: 0.05, max: 1, step: 0.01, percent: true, show: () => v.type === 'bars' && v.position !== 'center', group: 'Bar Yerleşimi', advanced: true },

// --- Dalga / çizgi biçimi (gelişmiş) ---
{ type: 'slider', path: 'visualizer.lineWidth', label: 'Çizgi Kalınlığı', min: 1, max: 12, step: 0.5, show: isWaveMode, group: 'Dalga Biçimi', advanced: true },
Expand Down Expand Up @@ -3420,7 +3422,7 @@
{ type: 'slider', path: 'logo.saturate', label: 'Doygunluk', min: 0, max: 2, step: 0.01, show: () => cfg.logo.enabled && isGifLogo(cfg.logo) },
{
type: 'segment', path: 'logo.blend', label: 'Karışım',
options: [{ value: 'normal', label: 'Normal' }, { value: 'screen', label: 'Ekran' }, { value: 'add', label: 'Ekle' }],
options: [{ value: 'normal', label: 'Normal' }, { value: 'screen', label: 'Ekran\u200B' }, { value: 'add', label: 'Ekle' }],
show: () => cfg.logo.enabled && isGifLogo(cfg.logo),
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/admin/media-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
nodes.push(
P().segment('Karışım', 'media.blend', [
{ value: 'normal', label: 'Normal' },
{ value: 'screen', label: 'Ekran' },
{ value: 'screen', label: 'Ekran\u200B' },
{ value: 'add', label: 'Toplama' },
{ value: 'multiply', label: 'Çarpma' },
])
Expand Down
4 changes: 2 additions & 2 deletions src/admin/scene-panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@
];

const BLEND_LABELS = [
['normal', 'Normal'], ['add', 'Toplama'], ['screen', 'Ekran'], ['multiply', 'Çarpma'],
['normal', 'Normal'], ['add', 'Toplama'], ['screen', 'Ekran\u200B'], ['multiply', 'Çarpma'],
['overlay', 'Kaplama'], ['darken', 'Koyulaştır'], ['lighten', 'Açıklaştır'],
['color-dodge', 'Renk Soldurma'], ['color-burn', 'Renk Yakma'],
['hard-light', 'Sert Işık'], ['soft-light', 'Yumuşak Işık'],
Expand Down Expand Up @@ -1028,7 +1028,7 @@
out.push(miniSlider('Parlaklık', () => getL('brightness', 1), (v) => setL('brightness', v), { min: 0.2, max: 2, step: 0.01, def: 1}));
out.push(miniSlider('Renk Kayması', () => getL('hue', 0), (v) => setL('hue', v), { min: 0, max: 1, step: 0.01, percent: true, def: 0}));
out.push(miniSlider('Doygunluk', () => getL('saturate', 1), (v) => setL('saturate', v), { min: 0, max: 2, step: 0.01, def: 1}));
out.push(miniSelect('Karışım', [['normal', 'Normal'], ['screen', 'Ekran'], ['add', 'Ekle']],
out.push(miniSelect('Karışım', [['normal', 'Normal'], ['screen', 'Ekran\u200B'], ['add', 'Ekle']],
() => lg.blend || 'normal', (v) => { lg.blend = v; }, undefined, 'normal'));
out.push(miniSelect('Ses Bandı', [['bass', 'Bas'], ['mid', 'Orta'], ['treble', 'Tiz'], ['level', 'Seviye']],
() => lg.audioBand || 'bass', (v) => { lg.audioBand = v; }, undefined, 'bass'));
Expand Down
3 changes: 2 additions & 1 deletion src/admin/studio.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,8 @@
onclick: () => guardedSelect(p.id),
},
[
el('span', { class: 'si-name', text: p.builtin ? trStudio(p.name) : p.name, ...(p.builtin ? {} : { 'data-user-text': '' }) }),
// title: dar listede kısaltılan uzun adın tamamı fare üstünde görünsün
el('span', { class: 'si-name', text: p.builtin ? trStudio(p.name) : p.name, ...(p.builtin ? {} : { 'data-user-text': '' }), title: p.builtin ? trStudio(p.name) : p.name }),
el('span', { class: 'si-tag', text: trStudio(p.builtin ? 'yerleşik' : p.engine === 'shader' ? 'shader' : 'varyasyon') }),
]
)
Expand Down
4 changes: 3 additions & 1 deletion src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1449,7 +1449,9 @@ function bridgeStatus() {
const c = bridge.cfg;
const now = Date.now();
return {
only: BRIDGE_ONLY, flag: BRIDGE_FLAG, ownProfile: BRIDGE_OWN_PROFILE, problem: audioBridge.problem(c), wire: audioBridge.WIRE_VERSION,
only: BRIDGE_ONLY, flag: BRIDGE_FLAG, ownProfile: BRIDGE_OWN_PROFILE,
// Köprü uygulamasında ayar send:false olabilir; eksik parola yine 'Parola eksik' görünsün (yoksa rozet hep 'Başlıyor' kalıyordu)
problem: audioBridge.problem(Object.assign({}, c, { send: c.send || BRIDGE_ONLY })), wire: audioBridge.WIRE_VERSION,
send: c.send || BRIDGE_ONLY, receive: c.receive && !BRIDGE_ONLY,
sender: bridge.sender ? Object.assign({ name: bridgeName(c) }, bridge.sender.status()) : null,
receiver: bridge.receiver ? bridge.receiver.status() : null,
Expand Down
6 changes: 4 additions & 2 deletions src/shared/export-formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,11 @@
if (frames < 4) return null;
const want = Math.round((Number(loop.cross) > 0 ? Number(loop.cross) : 0.5) * f);
const nx = Math.max(1, Math.min(want, Math.floor(frames / 2), Math.round(2 * f)));
/* trim + setpts sonrası akışın kare hızı bilinmiyor (1/0); xfade sabit kare
hızı istiyor ve ffmpeg 234 ile çıkıyordu. Her dala fps ekleniyor. */
const graph = '[0:v]split=2[lpa][lpb];' +
'[lpa]trim=start_frame=' + nx + ',setpts=PTS-STARTPTS[lpm];' +
'[lpb]trim=end_frame=' + nx + ',setpts=PTS-STARTPTS[lph];' +
'[lpa]trim=start_frame=' + nx + ',setpts=PTS-STARTPTS,fps=' + f + '[lpm];' +
'[lpb]trim=end_frame=' + nx + ',setpts=PTS-STARTPTS,fps=' + f + '[lph];' +
'[lpm][lph]xfade=transition=fade:duration=' + (nx / f).toFixed(6) + ':offset=' + ((frames - nx) / f).toFixed(6);
return { graph, frames, cross: nx / f, render: (frames + nx) / f };
}
Expand Down
3 changes: 3 additions & 0 deletions src/shared/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -3316,6 +3316,9 @@
'Ekran seçilmedi': 'No display selected',
'Önce görselleştirmenin açılacağı ekranı seçin.': 'Choose a display for the visualizer first.',
'Ekran': 'Display',
// Karışım kipi "Ekran" (screen blend), ekran/monitör değil. Anahtarın sonundaki
// görünmez U+200B iki anlamı ayırıyor; Türkçede ikisi de "Ekran" görünür.
'Ekran\u200B': 'Screen',
'Açık': 'On',
'Kapalı': 'Off',
'ızgara izleniyor': 'following the grid',
Expand Down
14 changes: 14 additions & 0 deletions tests/export-loop.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ test('plan: L saniye çıktı, L + X çizim; geçiş sınırlı', () => {
assert.strictEqual(F.loopPlan(null, 30, mp4), null);
});

test('plan: xfade sabit kare hızı ister; her setpts\'ten sonra fps gelir (ffmpeg 234, 1/0)', () => {
for (const fps of [24, 30, 60, 29.97]) {
const g = F.loopPlan({ seconds: 8, cross: 0.5 }, fps, mp4).graph;
const n = (g.match(/setpts=PTS-STARTPTS/g) || []).length;
assert.strictEqual(n, 2, 'iki dal');
// Her setpts hemen fps ile devam ediyor ve değer çıktı kare hızı
assert.strictEqual((g.match(new RegExp('setpts=PTS-STARTPTS,fps=' + fps + '\\[', 'g')) || []).length, 2, fps + ' fps');
// fps xfade'den önce: iki dal da etiketlenmeden önce sabit hıza geçiyor
assert.ok(g.indexOf('fps=') < g.indexOf('xfade='));
}
// Varsayılan 30 (geçersiz kare hızında da boş değer yazılmıyor)
assert.match(F.loopPlan({ seconds: 8, cross: 0.5 }, 0, mp4).graph, /setpts=PTS-STARTPTS,fps=30\[lpm\]/);
});

test('argümanlar: süzgeç zinciri, ses yok, bölüm kesimi yok, süre L', () => {
const a = F.buildArgs(Object.assign({}, base, { durationSec: 8, loop: { seconds: 8, cross: 0.5 }, chapters: [3] })).join(' ');
assert.match(a, /-filter_complex \[0:v\]split=2\[lpa\]\[lpb\];.*xfade=[^ ]*\[lp\];\[lp\]scale=[^ ]*format=yuv420p\[v\] -map \[v\]/);
Expand Down
75 changes: 75 additions & 0 deletions tests/export-ui-polish.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';
/* Dışa aktarma ve arayüz düzeltmeleri: karışım kipi çevirisi, Yerleşim
* yeniden çizimi, düzen içe aktarımı ve köprü parola rozeti. */
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');

const read = (rel) => fs.readFileSync(path.join(__dirname, '..', rel), 'utf-8');

function loadEnglish() {
const win = {
navigator: { languages: ['en-US'], language: 'en-US' },
localStorage: { getItem: () => 'en', setItem: () => {} },
alert: () => {}, confirm: () => {},
document: { documentElement: { lang: '' }, readyState: 'complete', title: '', body: {}, addEventListener: () => {} },
MutationObserver: function () { this.observe = () => {}; },
Node: { TEXT_NODE: 3, ELEMENT_NODE: 1 },
};
win.window = win;
vm.runInContext(read('src/shared/i18n.js'), vm.createContext(win), { filename: 'i18n.js' });
return win.SVI18n;
}

test('karışım kipi "Ekran" İngilizcede Screen, ekran/monitör Display kalır', () => {
const t = loadEnglish().t;
assert.strictEqual(t('Ekran'), 'Display');
assert.strictEqual(t('Ekran\u200B'), 'Screen');
// Dört karışım listesi de ayrı anahtarı kullanıyor; Türkçede ikisi de "Ekran" görünür
for (const f of ['src/admin/admin.js', 'src/admin/media-panel.js', 'src/admin/scene-panels.js']) {
assert.match(read(f), /'screen'.{0,20}'Ekran\\u200B'|label: 'Ekran\\u200B'/, f);
assert.ok(!/'screen'[^\n]{0,20}'Ekran'/.test(read(f)), f + ' eski anahtarı kullanıyor');
}
// Ekran bölümünün başlığı ve özeti eski anahtarda kaldı
assert.match(read('src/admin/admin.js'), /title: 'Ekran',/);
});

test('Yerleşim seçimi paneli yeniden kuruyor; yansıma boyu yansıma 0 iken de görünür', () => {
const admin = read('src/admin/admin.js');
assert.match(admin, /path: 'visualizer\.position', label: 'Yerleşim', rebuild: true,/);
const line = admin.split('\n').find((l) => l.includes("path: 'visualizer.reflectionLength'"));
assert.ok(line && !/v\.reflection\)/.test(line), 'boy yansıma değerine bağlı değil');
});

test('düzen içe aktarımı: yeni sahnede metin öğesi ikinci bir görselleştirici katmanı olarak gelir, tekrarında çoğalmaz', () => {
const LY = require('../src/shared/layout.js');
const SL = require('../src/visualizer/layers.js');
const fresh = () => ({ background: { type: 'gradient' }, visualizer: { type: 'bars' }, text: { enabled: false } });
const src = fresh();
SL.setStackEnabled(src, true);
src.layers.push(SL.normalizeLayer(SL.makeTextLayer({ source: 'static', text: 'Merhaba' })));
const file = LY.exportLayout(SL.resolve(src), src, {});
assert.deepStrictEqual(file.elements.map((e) => e.kind), ['bars', 'text']);

const cfg = fresh();
SL.setStackEnabled(cfg, true);
const make = (s) => SL.normalizeLayer(s);
const r = LY.importLayout(cfg, JSON.parse(JSON.stringify(file)), make);
assert.deepStrictEqual(r, { ok: true, updated: 1, added: 1, skipped: 0 });
// Metin ayrı bir tür değil, görselleştirici katmanının alt türü: [arkaplan, bars, text]
assert.deepStrictEqual(cfg.layers.map((l) => l.kind + ':' + l.type), ['background:gradient', 'visualizer:bars', 'visualizer:text']);
const again = LY.importLayout(cfg, JSON.parse(JSON.stringify(file)), make);
assert.deepStrictEqual(again, { ok: true, updated: 2, added: 0, skipped: 0 });
assert.strictEqual(cfg.layers.length, 3);
});

test('köprü uygulaması: parola yokken durum "eksik" bildirir (rozet Başlıyor\'da kalmaz)', () => {
const main = read('src/main/main.js');
assert.match(main, /problem: audioBridge\.problem\(Object\.assign\(\{\}, c, \{ send: c\.send \|\| BRIDGE_ONLY \}\)\), wire/);
const B = require('../src/main/audio-bridge.js');
// Köprü kipinde ayar send:false olsa da gönderen sayılır
assert.strictEqual(B.problem({ send: false, receive: false, secret: '' }), '');
assert.strictEqual(B.problem({ send: true, receive: false, secret: '' }), 'secret');
});
Loading