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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#build--distribution)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)
[![Downloads](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=downloads)](https://github.com/CaYatur/SoundVisualizer/releases)
[![Tests](https://img.shields.io/badge/tests-2561%20passing-2ea043.svg)](#tests)
[![Tests](https://img.shields.io/badge/tests-2577%20passing-2ea043.svg)](#tests)
[![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com)

</div>
Expand Down Expand Up @@ -813,7 +813,7 @@ The styles the application shipped with, still one click away.
The sections above show what the application looks like. This one lists what is
actually in it, by name.

### Backgrounds — 31 types
### Backgrounds — 41 types

**Fluid** — **Fluid Gradient** (an audio-reactive mesh gradient in a WebGL shader, in *Soft* and
*Plasma* styles, with flow speed, wander, orbit, swirl, warp, scale, grain, vignette, audio burst
Expand Down Expand Up @@ -848,7 +848,7 @@ Five colour stops, **58 built-in palettes** in seven groups (Classics, Warm, Coo
Dark, Light, Monochrome Families) and your own saved palettes apply to every background type, to the
Studio engine and to the 3D engine.

### Visualizer — 48 modes
### Visualizer — 60 modes

**Basic** — **Bars** · **Center** · **Segments** (LED equaliser) · **Dot Matrix** · **Skyline**
(buildings with lit windows)
Expand Down Expand Up @@ -1341,7 +1341,7 @@ npm test
npm start -- --smoke
```

**2561 unit tests, all passing.** They are written to check answers, not to exercise lines:
**2577 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
Expand Down
4 changes: 2 additions & 2 deletions README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-111997.svg)](#paketleme--dağıtım)
[![Electron](https://img.shields.io/badge/Electron-43-47848F.svg)](https://www.electronjs.org/)
[![İndirme](https://img.shields.io/github/downloads/CaYatur/SoundVisualizer/total?label=indirme)](https://github.com/CaYatur/SoundVisualizer/releases)
[![Test](https://img.shields.io/badge/test-2561%20geçiyor-2ea043.svg)](#testler)
[![Test](https://img.shields.io/badge/test-2577%20geçiyor-2ea043.svg)](#testler)
[![cayadev.com](https://img.shields.io/badge/cayadev.com-e11d2a.svg)](https://cayadev.com)

</div>
Expand Down Expand Up @@ -1341,7 +1341,7 @@ npm test
npm start -- --smoke
```

**2561 birim testi, hepsi geçiyor.** Satır çalıştırmak için değil, cevap denetlemek için yazıldılar:
**2577 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ı
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ npm test
npm start -- --smoke
```

- **2561 unit tests, all passing** on `main`. 703 of those shipped in v3.1.0;
- **2577 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, and 878
on `main` since.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "soundvisualizer",
"version": "3.1.5-beta",
"description": "CAYADEV — çoklu ekran, OBS yayın çıkışı ve Studio shader editörü olan ses görselleştirici (Windows + macOS)",
"description": "CAYADEV — çoklu ekran, OBS yayın çıkışı ve Studio shader editörü olan ses görselleştirici (Windows + macOS + Linux)",
"main": "src/main/main.js",
"author": {
"name": "CAYADEV",
Expand Down
165 changes: 154 additions & 11 deletions src/admin/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -542,10 +542,17 @@
return window.SVNowPlayingPanel ? window.SVNowPlayingPanel.panel() : null;
case 'grouppanel':
return window.SVGroupPanel ? window.SVGroupPanel.panel() : null;
case 'note':
case 'note': {
// Metin işlev olabilir: içerik çizim anında hesaplanır (ör. ekran hızı)
return el('div', { class: 'ctrl settings-io-note',
text: typeof def.text === 'function' ? def.text() : def.text });
const note = el('div', {
class: 'ctrl settings-io-note' + (def.warn ? ' warn' : ''),
text: typeof def.text === 'function' ? def.text() : def.text,
});
if (def.attrs) {
for (const k in def.attrs) note.setAttribute(k, def.attrs[k]);
}
return note;
}
case 'scenes':
return scenesCtrl(def);
case 'displaypicker':
Expand Down Expand Up @@ -2097,6 +2104,12 @@
const isGradient = () => cfg.background.type === 'gradient';
// Renk paleti gradyan dışındaki 2D arkaplan modlarında da kullanılır
const usesPalette = () => cfg.background.type !== 'solid';
/* K1: WebGL gradient ctor failed → silent 2D solid. Informational only. */
const isGradientWebGLFallback = () => !!(
window.SVLayers
&& typeof window.SVLayers.isGradientWebGLFallback === 'function'
&& window.SVLayers.isGradientWebGLFallback()
);
// Frekans bandı okuyan ön modlar (bar sayısı / frekans aralığı anlamlı)
const isBandMode = () => MC().is('visualizer', v.type, 'bands');
// Bar benzeri geometriye sahip modlar (aralarındaki boşluk anlamlı)
Expand Down Expand Up @@ -2183,6 +2196,13 @@
type: 'segment', path: 'background.type', label: 'Tür', rebuild: true, grouped: true,
options: MC().options('background'),
},
{
type: 'note',
warn: true,
attrs: { 'data-sv-webgl-fallback-note': '1' },
text: 'WebGL yok / desteklenmiyor: gradyan düz renge düştü',
show: () => isGradient() && isGradientWebGLFallback(),
},
{ type: 'custompicker', kind: 'background', show: () => cfg.background.type === 'custom' },
{
type: 'segment',
Expand Down Expand Up @@ -3257,6 +3277,8 @@
/* Masonry sınıfı bir önceki çizimden kalırsa yeni kartlar 4 px satıra
doğar. Linux o boyu içerik sanıp bütün kartları üst üste bindiriyor. */
root.classList.remove('masonry');
// Preview first so K1 WebGL-fallback flag is current for note show().
if (window.SVPreview) window.SVPreview.setConfig(cfg);
root.innerHTML = '';

const cat = CATEGORIES.find((c) => c.id === activeCategory) || CATEGORIES[0];
Expand Down Expand Up @@ -3290,7 +3312,6 @@
restoreSectionsScroll();
requestAnimationFrame(restoreSectionsScroll);
});
if (window.SVPreview) window.SVPreview.setConfig(cfg);
}

/* KART DÜZENİ (#622).
Expand Down Expand Up @@ -3977,13 +3998,84 @@
updateFloatingToggles();
}

/* audioUiKind tracks how the Ses Seviyesi status was last set:
idle (waiting / diagnosing), capture (source started or levels flowing),
devices (enumerate success), err (capture/diagnose failure).
Capture may already be running before the admin window subscribes to
audio-source-status, so meters can move while the label still says
"Ses bekleniyor…". Levels refresh promotes idle/devices -> capture. */
let audioUiKind = 'idle';
let audioCaptureDevice = null;
let audioCaptureFromStatus = false; // true after onAudioSourceStatus started

function setAudioState(text, cls, icon) {
const a = $('audioState');
window.SVIcons.set(a, icon || '', text);
a.title = text; // kısaltılan uzun aygıt adları için tam metin
a.className = 'audio-state' + (cls ? ' ' + cls : '');
}

/* Mirror loopback-helper resolveDevice + CAPTURE-START naming:
'default' -> pickDefault device name; apps -> label; else device name.
Joined with ' + ' so the status matches onAudioSourceStatus started. */
function pickDefaultDeviceName() {
const all = audioDevices || [];
const d =
all.find((x) => x.loopback && x.isDefault) ||
all.find((x) => x.loopback) ||
all.find((x) => x.kind === 'output' && x.isDefault) ||
all.find((x) => x.kind === 'output') ||
all.find((x) => x.isDefault) ||
all[0] ||
null;
return d && d.name ? d.name : null;
}

function resolveSelectedCaptureLabel() {
const audio = cfg && cfg.audio;
const sources = (audio && Array.isArray(audio.sources) && audio.sources.length)
? audio.sources
: (audio && audio.source ? [audio.source] : ['default']);
const AA = window.SVAppAudio;
const names = [];
const seen = new Set();
for (const s of sources) {
let name = null;
if (AA && AA.isAppSource(s)) name = s.label || s.match || null;
else if (s === 'default') name = pickDefaultDeviceName();
else if (s != null && s !== '') name = String(s);
if (!name || seen.has(name)) continue;
seen.add(name);
names.push(name);
}
return names.length ? names.join(' + ') : null;
}

function markAudioCapturing(device) {
if (device) audioCaptureDevice = device;
const label = audioCaptureDevice || resolveSelectedCaptureLabel() || 'çıkış';
audioCaptureDevice = label;
audioCaptureFromStatus = true;
audioUiKind = 'capture';
setAudioState('Yakalanıyor: ' + label, 'ok', 'record');
}

/* Promote waiting/device-list status when live meters show signal.
Resolves the selected Ses Kaynakları label so we do not need a
re-select (started event) just to learn the device name. */
function ensureAudioListeningFromLevels(d) {
if (!d || audioUiKind === 'err') return;
if (audioCaptureFromStatus && audioUiKind === 'capture') return;
const signal = Math.max(+d.level || 0, +d.bass || 0, +d.mid || 0, +d.treble || 0);
if (!(signal > 0.015)) return;
const label = resolveSelectedCaptureLabel() || audioCaptureDevice;
if (audioUiKind === 'capture' && audioCaptureDevice && audioCaptureDevice === label) return;
audioCaptureDevice = label;
audioUiKind = 'capture';
if (label) setAudioState('Yakalanıyor: ' + label, 'ok', 'record');
else setAudioState('Yakalanıyor: ' + (resolveSelectedCaptureLabel() || 'çıkış'), 'ok', 'record');
}

/* Ölçek olarak yazılıyor, genişlik değil (bkz. admin.css .meter .bar i,
#621). Aynı değer yeniden yazılmıyor: stil bildirimi her yazımda
değişmiş sayılıyor. */
Expand All @@ -4002,14 +4094,28 @@
if (window.SVI18n?.locale !== 'tr') {
return `${english} [${code}]${result?.retried ? ' Automatic retry was unsuccessful.' : ''}`;
}
/* Platforma gore TR metin: Windows Ses ayarlari Linux'ta yaniltir.
loopbackAdvice / noDevicesMessage ile ayni yonlendirme. */
const tr = {
NODE_NOT_FOUND: 'Node.js bulunamadı. Node.js LTS kurun veya PATH ayarını onarın.',
NODE_NOT_FOUND: 'Node.js bulunamadı. Node.js LTS kurun veya PATH ayarınızı onarın.',
HELPER_MISSING: 'Ses yardımcı dosyaları kurulumda eksik. Uygulamayı yeniden kurun veya onarın.',
AUDIFY_MISSING: 'Native ses modülü eksik. Uygulamayı yeniden kurun veya onarın.',
NATIVE_ABI_MISMATCH: 'Native ses modülü bu Node.js sürümüyle uyumsuz. Node.js LTS ve uygulamayı yeniden kurun.',
ACCESS_DENIED: 'Windows ses sistemine erişimi engelledi. Ses gizlilik/güvenlik ayarlarını kontrol edip uygulamayı yeniden başlatın.',
DEVICE_ENUM_TIMEOUT: 'Ses aygıtı algılama zaman aşımına uğradı. Windows Ses hizmetini ve bağlı aygıtları kontrol edin.',
NO_DEVICES: 'Etkin ses aygıtı bulunamadı. Windows Ses ayarlarını kontrol edin ve aygıtı yeniden bağlayın.',
ACCESS_DENIED: PLATFORM.isLinux
? 'Ses alt sistemine erişim engellendi. PulseAudio/PipeWire izinlerini kontrol edip uygulamayı yeniden başlatın.'
: PLATFORM.isMac
? 'macOS ses alt sistemine erişimi engelledi. Ses ve Gizlilik ayarlarını kontrol edip uygulamayı yeniden başlatın.'
: 'Windows ses sistemine erişimi engelledi. Ses gizlilik/güvenlik ayarlarını kontrol edip uygulamayı yeniden başlatın.',
DEVICE_ENUM_TIMEOUT: PLATFORM.isLinux
? 'Ses aygıtı algılama zaman aşımına uğradı. PulseAudio veya PipeWire servisini ve bağlı aygıtları kontrol edin.'
: PLATFORM.isMac
? 'Ses aygıtı algılama zaman aşımına uğradı. macOS Ses ayarlarını ve bağlı aygıtları kontrol edin.'
: 'Ses aygıtı algılama zaman aşımına uğradı. Windows Ses hizmetini ve bağlı aygıtları kontrol edin.',
NO_DEVICES: PLATFORM.isLinux
? 'Etkin ses aygıtı bulunamadı. PulseAudio veya PipeWire çalışıyor mu ve bir monitor kaynağı görünüyor mu kontrol edin.'
: PLATFORM.isMac
? 'Etkin ses aygıtı bulunamadı. macOS Ses ayarlarını kontrol edin ve aygıtı yeniden bağlayın.'
: 'Etkin ses aygıtı bulunamadı. Windows Ses ayarlarını kontrol edin ve aygıtı yeniden bağlayın.',
INVALID_HELPER_OUTPUT: 'Ses yardımcı süreci geçersiz veri döndürdü.',
HELPER_EXITED: 'Ses yardımcı süreci beklenmedik şekilde kapandı.',
PROCESS_START_FAILED: 'Ses yardımcı süreci başlatılamadı.',
Expand All @@ -4021,10 +4127,22 @@
function applyAudioDiagnostic(result, showSuccess = false) {
audioDevices = result?.devices || [];
if (result?.ok) {
if (showSuccess) setAudioState(`${audioDevices.length} ses aygıtı bulundu`, 'ok', 'check');
// Keep an active capture label; enumerate success must not hide it.
if (showSuccess && audioUiKind !== 'capture') {
audioUiKind = 'devices';
setAudioState(`${audioDevices.length} ses aygıtı bulundu`, 'ok', 'check');
} else if (audioUiKind === 'capture' && !audioCaptureFromStatus) {
// Device list just refreshed — resolve default -> real name for status.
const label = resolveSelectedCaptureLabel();
if (label && label !== audioCaptureDevice) {
audioCaptureDevice = label;
setAudioState('Yakalanıyor: ' + label, 'ok', 'record');
}
}
$('banner').classList.add('hidden');
return;
}
audioUiKind = 'err';
setAudioState('Ses aygıtı tanılaması başarısız', 'err', 'warning');
$('bannerDetail').textContent = diagnosticText(result);
$('banner').classList.remove('hidden');
Expand Down Expand Up @@ -4125,7 +4243,12 @@
actions.floatingSnapTl = () => { if (window.api && window.api.floatingSnap) window.api.floatingSnap('tl'); };

actions.refreshDevices = async () => {
setAudioState(window.SVI18n?.locale === 'tr' ? 'Ses aygıtları tanılanıyor…' : 'Diagnosing audio devices…');
// Do not clobber an active capture status while re-enumerating devices
// (started handler may call refresh when a device name is missing).
if (audioUiKind !== 'capture') {
audioUiKind = 'idle';
setAudioState(window.SVI18n?.locale === 'tr' ? 'Ses aygıtları tanılanıyor…' : 'Diagnosing audio devices…');
}
const result = await window.api.diagnoseAudio();
applyAudioDiagnostic(result, true);
await refreshAudioApps();
Expand Down Expand Up @@ -4279,6 +4402,7 @@
setMeter('mBass', l.bass);
setMeter('mMid', l.mid);
setMeter('mTreble', l.treble);
ensureAudioListeningFromLevels(l);
}, 50);

const btn = $('previewToggle');
Expand Down Expand Up @@ -4765,6 +4889,8 @@
}
const audioDiagnostic = await window.api.diagnoseAudio();
audioDevices = audioDiagnostic?.devices || [];
// Prefetch capture label from selected sources so meter promote has a name.
audioCaptureDevice = resolveSelectedCaptureLabel();
await refreshAudioApps();
try {
lightingIdentity = await window.api.getLightingIdentityStatus();
Expand Down Expand Up @@ -5004,6 +5130,20 @@
}
window.api.onVisualizerStatus((d) => setStatus(d.open, d.displayIds, d.floating));

/* K1 admin notice: refresh when preview stack flips WebGL→solid fallback. */
let fallbackUiRaf = 0;
window.addEventListener('sv-gradient-webgl-fallback', () => {
if (fallbackUiRaf) return;
fallbackUiRaf = requestAnimationFrame(() => {
fallbackUiRaf = 0;
const want = !!(window.SVLayers
&& typeof window.SVLayers.isGradientWebGLFallback === 'function'
&& window.SVLayers.isGradientWebGLFallback());
const has = !!document.querySelector('[data-sv-webgl-fallback-note]');
if (want !== has) render();
});
});

// Farklı kontrol sistemi (Heartbeat / Durum Güvencesi):
// Kaza korumasıyla pencere geri açıldığında, çökme anında veya IPC gecikmelerinde
// buton durumunun ve görselleştirici durumunun gerçekle %100 uyuşmasını garanti eder.
Expand Down Expand Up @@ -5067,10 +5207,11 @@
setMeter('mBass', d.bass);
setMeter('mMid', d.mid);
setMeter('mTreble', d.treble);
ensureAudioListeningFromLevels(d);
});
window.api.onAudioSourceStatus((s) => {
if (s.type === 'started') {
setAudioState('Yakalanıyor: ' + (s.device || 'çıkış'), 'ok', 'record');
markAudioCapturing(s.device || 'çıkış');
$('banner').classList.add('hidden');
// başlatılan aygıtlardan herhangi biri listede yoksa listeyi tazele
if (s.device) {
Expand All @@ -5084,10 +5225,12 @@
kullanıcının BlackHole gibi sanal bir aygıt kurması gerekir.
Sessiz kalmak, kullanıcının neden hiçbir şey görmediğini
anlamaması demek olurdu. */
audioUiKind = 'err';
setAudioState('Sistem sesi yakalanamıyor', 'err', 'warning');
$('bannerDetail').textContent = s.message || 'Bu sistemde sistem sesini veren bir aygıt bulunamadı.';
$('banner').classList.remove('hidden');
} else if (s.type === 'error') {
audioUiKind = 'err';
setAudioState('Ses yakalanamadı', 'err', 'warning');
$('bannerDetail').textContent = s.message || 'Çıkış aygıtı yakalanamadı.';
$('banner').classList.remove('hidden');
Expand Down
13 changes: 13 additions & 0 deletions src/admin/scene-panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,19 @@
]),
]));

/* K1: when layer stack hides the Background card, surface the same
WebGL→solid info notice here (non-blocking). */
if (window.SVLayers
&& typeof window.SVLayers.isGradientWebGLFallback === 'function'
&& window.SVLayers.isGradientWebGLFallback()) {
const note = el('div', {
class: 'studio-note dim-hint',
text: 'WebGL yok / desteklenmiyor: gradyan düz renge düştü',
});
note.setAttribute('data-sv-webgl-fallback-note', '1');
nodes.push(note);
}

if (on) {
/* Arkaplan kartı yığın açıkken gizli; şeffaflık burada, listenin
en üstünde tek bir anahtar. Pencere / yayın / Spout aynı ayarı
Expand Down
Loading
Loading