feat(processed-audio-sources): phase 3 - processed audio source model
Replace MultichannelAudioSource with CaptureAudioSource, add ProcessedAudioSource (audio_source_id + audio_processing_template_id), remove MonoAudioSource and BandExtractAudioSource entirely. Update store resolution to walk processed chains collecting template IDs. Update all API schemas, routes, and frontend references.
This commit is contained in:
@@ -335,6 +335,13 @@
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
/* ── Integrations grid ── */
|
||||
.dashboard-integrations-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dashboard-autostart-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
|
||||
@@ -127,8 +127,8 @@ function _buildItems(results: any[], states: any = {}) {
|
||||
}));
|
||||
|
||||
_mapEntities(audioSrc, a => {
|
||||
const section = a.source_type === 'mono' ? 'audio-mono' : a.source_type === 'band_extract' ? 'audio-band-extract' : 'audio-multi';
|
||||
const tab = a.source_type === 'mono' ? 'audio_mono' : a.source_type === 'band_extract' ? 'audio_band_extract' : 'audio_multi';
|
||||
const section = a.source_type === 'processed' ? 'audio-processed' : 'audio-capture';
|
||||
const tab = a.source_type === 'processed' ? 'audio_processed' : 'audio_capture';
|
||||
items.push({
|
||||
name: a.name, detail: a.source_type, group: 'audio', icon: getAudioSourceIcon(a.source_type),
|
||||
nav: ['streams', tab, section, 'data-id', a.id],
|
||||
|
||||
@@ -104,7 +104,7 @@ const SUBTYPE_ICONS = {
|
||||
static: P.layoutDashboard, animated: P.refreshCw, audio: P.music,
|
||||
adaptive_time: P.clock, adaptive_scene: P.cloudSun, daylight: P.sun,
|
||||
},
|
||||
audio_source: { mono: P.mic, multichannel: P.volume2 },
|
||||
audio_source: { capture: P.volume2, processed: P.slidersHorizontal },
|
||||
output_target: { led: P.lightbulb, wled: P.lightbulb, ha_light: P.lightbulb },
|
||||
};
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ const _valueSourceTypeIcons = {
|
||||
system_metrics: _svg(P.cpu),
|
||||
game_event: _svg(P.gamepad2),
|
||||
};
|
||||
const _audioSourceTypeIcons = { mono: _svg(P.mic), multichannel: _svg(P.volume2), band_extract: _svg(P.activity) };
|
||||
const _audioSourceTypeIcons = { capture: _svg(P.volume2), processed: _svg(P.slidersHorizontal) };
|
||||
const _deviceTypeIcons = {
|
||||
wled: _svg(P.wifi), adalight: _svg(P.usb), ambiled: _svg(P.usb),
|
||||
mqtt: _svg(P.send), ws: _svg(P.globe), openrgb: _svg(P.palette),
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* Audio Sources — CRUD for multichannel, mono, and band extract audio sources.
|
||||
* Audio Sources — CRUD for capture and processed audio sources.
|
||||
*
|
||||
* Audio sources are managed entities that encapsulate audio device
|
||||
* configuration. Multichannel sources represent physical audio devices;
|
||||
* mono sources extract a single channel from a multichannel source;
|
||||
* band extract sources filter a parent source to a frequency band.
|
||||
* configuration. Capture sources represent physical audio devices;
|
||||
* processed sources apply audio processing filters to another source.
|
||||
* CSS audio type references an audio source by ID.
|
||||
*
|
||||
* Card rendering is handled by streams.js (Audio tab).
|
||||
@@ -30,8 +29,6 @@ class AudioSourceModal extends Modal {
|
||||
|
||||
onForceClose() {
|
||||
if (_audioSourceTagsInput) { _audioSourceTagsInput.destroy(); _audioSourceTagsInput = null; }
|
||||
if (_asChannelIconSelect) { _asChannelIconSelect.destroy(); _asChannelIconSelect = null; }
|
||||
if (_asBandIconSelect) { _asBandIconSelect.destroy(); _asBandIconSelect = null; }
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
@@ -41,12 +38,8 @@ class AudioSourceModal extends Modal {
|
||||
type: (document.getElementById('audio-source-type') as HTMLSelectElement).value,
|
||||
device: (document.getElementById('audio-source-device') as HTMLSelectElement).value,
|
||||
audioTemplate: (document.getElementById('audio-source-audio-template') as HTMLSelectElement).value,
|
||||
parent: (document.getElementById('audio-source-parent') as HTMLSelectElement).value,
|
||||
channel: (document.getElementById('audio-source-channel') as HTMLSelectElement).value,
|
||||
bandParent: (document.getElementById('audio-source-band-parent') as HTMLSelectElement).value,
|
||||
band: (document.getElementById('audio-source-band') as HTMLSelectElement).value,
|
||||
freqLow: (document.getElementById('audio-source-freq-low') as HTMLInputElement).value,
|
||||
freqHigh: (document.getElementById('audio-source-freq-high') as HTMLInputElement).value,
|
||||
parentSource: (document.getElementById('audio-source-parent') as HTMLSelectElement).value,
|
||||
processingTemplate: (document.getElementById('audio-source-processing-template') as HTMLSelectElement).value,
|
||||
tags: JSON.stringify(_audioSourceTagsInput ? _audioSourceTagsInput.getValue() : []),
|
||||
};
|
||||
}
|
||||
@@ -54,25 +47,14 @@ class AudioSourceModal extends Modal {
|
||||
|
||||
const audioSourceModal = new AudioSourceModal();
|
||||
|
||||
// ── EntitySelect / IconSelect instances for audio source editor ──
|
||||
// ── EntitySelect instances for audio source editor ──
|
||||
let _asTemplateEntitySelect: EntitySelect | null = null;
|
||||
let _asDeviceEntitySelect: EntitySelect | null = null;
|
||||
let _asParentEntitySelect: EntitySelect | null = null;
|
||||
let _asBandParentEntitySelect: EntitySelect | null = null;
|
||||
let _asBandIconSelect: IconSelect | null = null;
|
||||
let _asChannelIconSelect: IconSelect | null = null;
|
||||
let _asProcessingTemplateEntitySelect: EntitySelect | null = null;
|
||||
|
||||
const _svg = (d: string): string => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||
|
||||
function _buildBandItems() {
|
||||
return [
|
||||
{ value: 'bass', icon: _svg(P.volume2), label: t('audio_source.band.bass'), desc: '20–250 Hz' },
|
||||
{ value: 'mid', icon: _svg(P.music), label: t('audio_source.band.mid'), desc: '250–4000 Hz' },
|
||||
{ value: 'treble', icon: _svg(P.zap), label: t('audio_source.band.treble'), desc: '4k–20k Hz' },
|
||||
{ value: 'custom', icon: _svg(P.slidersHorizontal), label: t('audio_source.band.custom') },
|
||||
];
|
||||
}
|
||||
|
||||
// ── Auto-name generation ──────────────────────────────────────
|
||||
|
||||
let _asNameManuallyEdited = false;
|
||||
@@ -82,24 +64,14 @@ function _autoGenerateAudioSourceName() {
|
||||
if ((document.getElementById('audio-source-id') as HTMLInputElement).value) return;
|
||||
const type = (document.getElementById('audio-source-type') as HTMLSelectElement).value;
|
||||
let name = '';
|
||||
if (type === 'multichannel') {
|
||||
if (type === 'capture') {
|
||||
const devSel = document.getElementById('audio-source-device') as HTMLSelectElement | null;
|
||||
const devName = devSel?.selectedOptions[0]?.textContent?.trim();
|
||||
name = devName || t('audio_source.type.multichannel');
|
||||
} else if (type === 'mono') {
|
||||
name = devName || t('audio_source.type.capture');
|
||||
} else if (type === 'processed') {
|
||||
const parentSel = document.getElementById('audio-source-parent') as HTMLSelectElement | null;
|
||||
const parentName = parentSel?.selectedOptions[0]?.textContent?.trim() || '';
|
||||
const ch = (document.getElementById('audio-source-channel') as HTMLSelectElement).value;
|
||||
const chLabel = ch === 'left' ? 'L' : ch === 'right' ? 'R' : 'M';
|
||||
name = parentName ? `${parentName} · ${chLabel}` : t('audio_source.type.mono');
|
||||
} else if (type === 'band_extract') {
|
||||
const parentSel = document.getElementById('audio-source-band-parent') as HTMLSelectElement | null;
|
||||
const parentName = parentSel?.selectedOptions[0]?.textContent?.trim() || '';
|
||||
const band = (document.getElementById('audio-source-band') as HTMLSelectElement).value;
|
||||
const bandLabel = band === 'custom'
|
||||
? `${(document.getElementById('audio-source-freq-low') as HTMLInputElement).value}–${(document.getElementById('audio-source-freq-high') as HTMLInputElement).value} Hz`
|
||||
: t(`audio_source.band.${band}`);
|
||||
name = parentName ? `${parentName} · ${bandLabel}` : bandLabel;
|
||||
name = parentName ? `${parentName} (processed)` : t('audio_source.type.processed');
|
||||
}
|
||||
(document.getElementById('audio-source-name') as HTMLInputElement).value = name;
|
||||
}
|
||||
@@ -107,15 +79,14 @@ function _autoGenerateAudioSourceName() {
|
||||
// ── Modal ─────────────────────────────────────────────────────
|
||||
|
||||
const _titleKeys: Record<string, Record<string, string>> = {
|
||||
multichannel: { add: 'audio_source.add.multichannel', edit: 'audio_source.edit.multichannel' },
|
||||
mono: { add: 'audio_source.add.mono', edit: 'audio_source.edit.mono' },
|
||||
band_extract: { add: 'audio_source.add.band_extract', edit: 'audio_source.edit.band_extract' },
|
||||
capture: { add: 'audio_source.add.capture', edit: 'audio_source.edit.capture' },
|
||||
processed: { add: 'audio_source.add.processed', edit: 'audio_source.edit.processed' },
|
||||
};
|
||||
|
||||
export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
const isEdit = !!editData;
|
||||
const st = isEdit ? editData.source_type : sourceType;
|
||||
const titleKey = _titleKeys[st]?.[isEdit ? 'edit' : 'add'] || _titleKeys.multichannel.add;
|
||||
const titleKey = _titleKeys[st]?.[isEdit ? 'edit' : 'add'] || _titleKeys.capture.add;
|
||||
|
||||
document.getElementById('audio-source-modal-title')!.innerHTML = `${ICON_MUSIC} ${t(titleKey)}`;
|
||||
(document.getElementById('audio-source-id') as HTMLInputElement).value = isEdit ? editData.id : '';
|
||||
@@ -131,41 +102,26 @@ export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
(document.getElementById('audio-source-name') as HTMLInputElement).value = editData.name || '';
|
||||
(document.getElementById('audio-source-description') as HTMLInputElement).value = editData.description || '';
|
||||
|
||||
if (editData.source_type === 'multichannel') {
|
||||
if (editData.source_type === 'capture') {
|
||||
_loadAudioTemplates(editData.audio_template_id);
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = () => { _filterDevicesBySelectedTemplate(); _autoGenerateAudioSourceName(); };
|
||||
await _loadAudioDevices();
|
||||
_selectAudioDevice(editData.device_index, editData.is_loopback);
|
||||
} else if (editData.source_type === 'mono') {
|
||||
_loadMultichannelSources(editData.audio_source_id);
|
||||
(document.getElementById('audio-source-channel') as HTMLSelectElement).value = editData.channel || 'mono';
|
||||
_ensureChannelIconSelect();
|
||||
} else if (editData.source_type === 'band_extract') {
|
||||
_loadBandParentSources(editData.audio_source_id);
|
||||
(document.getElementById('audio-source-band') as HTMLSelectElement).value = editData.band || 'bass';
|
||||
_ensureBandIconSelect();
|
||||
(document.getElementById('audio-source-freq-low') as HTMLInputElement).value = String(editData.freq_low ?? 20);
|
||||
(document.getElementById('audio-source-freq-high') as HTMLInputElement).value = String(editData.freq_high ?? 20000);
|
||||
onBandPresetChange();
|
||||
} else if (editData.source_type === 'processed') {
|
||||
_loadParentSources(editData.audio_source_id);
|
||||
_loadProcessingTemplates(editData.audio_processing_template_id);
|
||||
}
|
||||
} else {
|
||||
(document.getElementById('audio-source-name') as HTMLInputElement).value = '';
|
||||
(document.getElementById('audio-source-description') as HTMLInputElement).value = '';
|
||||
|
||||
if (sourceType === 'multichannel') {
|
||||
if (sourceType === 'capture') {
|
||||
_loadAudioTemplates();
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = () => { _filterDevicesBySelectedTemplate(); _autoGenerateAudioSourceName(); };
|
||||
await _loadAudioDevices();
|
||||
} else if (sourceType === 'mono') {
|
||||
_loadMultichannelSources();
|
||||
_ensureChannelIconSelect();
|
||||
} else if (sourceType === 'band_extract') {
|
||||
_loadBandParentSources();
|
||||
(document.getElementById('audio-source-band') as HTMLSelectElement).value = 'bass';
|
||||
_ensureBandIconSelect();
|
||||
(document.getElementById('audio-source-freq-low') as HTMLInputElement).value = '20';
|
||||
(document.getElementById('audio-source-freq-high') as HTMLInputElement).value = '20000';
|
||||
onBandPresetChange();
|
||||
} else if (sourceType === 'processed') {
|
||||
_loadParentSources();
|
||||
_loadProcessingTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,14 +145,17 @@ export async function closeAudioSourceModal() {
|
||||
|
||||
export function onAudioSourceTypeChange() {
|
||||
const type = (document.getElementById('audio-source-type') as HTMLSelectElement).value;
|
||||
(document.getElementById('audio-source-multichannel-section') as HTMLElement).style.display = type === 'multichannel' ? '' : 'none';
|
||||
(document.getElementById('audio-source-mono-section') as HTMLElement).style.display = type === 'mono' ? '' : 'none';
|
||||
(document.getElementById('audio-source-band-extract-section') as HTMLElement).style.display = type === 'band_extract' ? '' : 'none';
|
||||
}
|
||||
|
||||
export function onBandPresetChange() {
|
||||
const band = (document.getElementById('audio-source-band') as HTMLSelectElement).value;
|
||||
(document.getElementById('audio-source-custom-freq') as HTMLElement).style.display = band === 'custom' ? '' : 'none';
|
||||
const captureSection = document.getElementById('audio-source-capture-section');
|
||||
const processedSection = document.getElementById('audio-source-processed-section');
|
||||
if (captureSection) captureSection.style.display = type === 'capture' ? '' : 'none';
|
||||
if (processedSection) processedSection.style.display = type === 'processed' ? '' : 'none';
|
||||
// Legacy sections — hide if present
|
||||
const legacyMulti = document.getElementById('audio-source-multichannel-section');
|
||||
const legacyMono = document.getElementById('audio-source-mono-section');
|
||||
const legacyBand = document.getElementById('audio-source-band-extract-section');
|
||||
if (legacyMulti) legacyMulti.style.display = 'none';
|
||||
if (legacyMono) legacyMono.style.display = 'none';
|
||||
if (legacyBand) legacyBand.style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────
|
||||
@@ -216,22 +175,15 @@ export async function saveAudioSource() {
|
||||
|
||||
const payload: any = { name, source_type: sourceType, description, tags: _audioSourceTagsInput ? _audioSourceTagsInput.getValue() : [] };
|
||||
|
||||
if (sourceType === 'multichannel') {
|
||||
if (sourceType === 'capture') {
|
||||
const deviceVal = (document.getElementById('audio-source-device') as HTMLSelectElement).value || '-1:1';
|
||||
const [devIdx, devLoop] = deviceVal.split(':');
|
||||
payload.device_index = parseInt(devIdx) || -1;
|
||||
payload.is_loopback = devLoop !== '0';
|
||||
payload.audio_template_id = (document.getElementById('audio-source-audio-template') as HTMLSelectElement).value || null;
|
||||
} else if (sourceType === 'mono') {
|
||||
} else if (sourceType === 'processed') {
|
||||
payload.audio_source_id = (document.getElementById('audio-source-parent') as HTMLSelectElement).value;
|
||||
payload.channel = (document.getElementById('audio-source-channel') as HTMLSelectElement).value;
|
||||
} else if (sourceType === 'band_extract') {
|
||||
payload.audio_source_id = (document.getElementById('audio-source-band-parent') as HTMLSelectElement).value;
|
||||
payload.band = (document.getElementById('audio-source-band') as HTMLSelectElement).value;
|
||||
if (payload.band === 'custom') {
|
||||
payload.freq_low = parseFloat((document.getElementById('audio-source-freq-low') as HTMLInputElement).value) || 20;
|
||||
payload.freq_high = parseFloat((document.getElementById('audio-source-freq-high') as HTMLInputElement).value) || 20000;
|
||||
}
|
||||
payload.audio_processing_template_id = (document.getElementById('audio-source-processing-template') as HTMLSelectElement).value;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -392,77 +344,18 @@ function _selectAudioDevice(deviceIndex: any, isLoopback: any) {
|
||||
if (opt) select.value = val;
|
||||
}
|
||||
|
||||
function _loadMultichannelSources(selectedId?: any) {
|
||||
function _loadParentSources(selectedId?: any) {
|
||||
const select = document.getElementById('audio-source-parent') as HTMLSelectElement | null;
|
||||
if (!select) return;
|
||||
const multichannel = _cachedAudioSources.filter(s => s.source_type === 'multichannel');
|
||||
select.innerHTML = multichannel.map(s =>
|
||||
`<option value="${s.id}"${s.id === selectedId ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
|
||||
).join('');
|
||||
|
||||
if (_asParentEntitySelect) _asParentEntitySelect.destroy();
|
||||
if (multichannel.length > 0) {
|
||||
_asParentEntitySelect = new EntitySelect({
|
||||
target: select,
|
||||
getItems: () => multichannel.map((s: any) => ({
|
||||
value: s.id,
|
||||
label: s.name,
|
||||
icon: getAudioSourceIcon('multichannel'),
|
||||
})),
|
||||
placeholder: t('palette.search'),
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
|
||||
function _ensureBandIconSelect() {
|
||||
const sel = document.getElementById('audio-source-band') as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
if (_asBandIconSelect) {
|
||||
_asBandIconSelect.updateItems(_buildBandItems());
|
||||
return;
|
||||
}
|
||||
_asBandIconSelect = new IconSelect({
|
||||
target: sel,
|
||||
items: _buildBandItems(),
|
||||
columns: 2,
|
||||
onChange: () => { onBandPresetChange(); _autoGenerateAudioSourceName(); },
|
||||
});
|
||||
}
|
||||
|
||||
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
||||
|
||||
function _ensureChannelIconSelect() {
|
||||
const sel = document.getElementById('audio-source-channel') as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
const items = [
|
||||
{ value: 'mono', icon: _icon(P.headphones), label: t('audio_source.channel.mono'), desc: t('audio_source.channel.mono.desc') },
|
||||
{ value: 'left', icon: _icon(P.volume2), label: t('audio_source.channel.left'), desc: t('audio_source.channel.left.desc') },
|
||||
{ value: 'right', icon: _icon(P.volume2), label: t('audio_source.channel.right'), desc: t('audio_source.channel.right.desc') },
|
||||
];
|
||||
if (_asChannelIconSelect) {
|
||||
_asChannelIconSelect.updateItems(items);
|
||||
return;
|
||||
}
|
||||
_asChannelIconSelect = new IconSelect({
|
||||
target: sel,
|
||||
items,
|
||||
columns: 3,
|
||||
onChange: () => _autoGenerateAudioSourceName(),
|
||||
});
|
||||
}
|
||||
|
||||
function _loadBandParentSources(selectedId?: any) {
|
||||
const select = document.getElementById('audio-source-band-parent') as HTMLSelectElement | null;
|
||||
if (!select) return;
|
||||
// Band extract can reference any audio source type
|
||||
// Processed sources can reference any audio source type
|
||||
const sources = _cachedAudioSources;
|
||||
select.innerHTML = sources.map(s =>
|
||||
`<option value="${s.id}"${s.id === selectedId ? ' selected' : ''}>${escapeHtml(s.name)}</option>`
|
||||
).join('');
|
||||
|
||||
if (_asBandParentEntitySelect) _asBandParentEntitySelect.destroy();
|
||||
if (_asParentEntitySelect) _asParentEntitySelect.destroy();
|
||||
if (sources.length > 0) {
|
||||
_asBandParentEntitySelect = new EntitySelect({
|
||||
_asParentEntitySelect = new EntitySelect({
|
||||
target: select,
|
||||
getItems: () => sources.map((s: any) => ({
|
||||
value: s.id,
|
||||
@@ -475,6 +368,34 @@ function _loadBandParentSources(selectedId?: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function _loadProcessingTemplates(selectedId?: any) {
|
||||
const select = document.getElementById('audio-source-processing-template') as HTMLSelectElement | null;
|
||||
if (!select) return;
|
||||
// TODO: Load audio processing templates from cache/API when available in Phase 6
|
||||
// For now, populate from existing audio processing templates endpoint
|
||||
fetchWithAuth('/audio-processing-templates').then(async resp => {
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const templates = data.templates || [];
|
||||
select.innerHTML = templates.map((t: any) =>
|
||||
`<option value="${t.id}"${t.id === selectedId ? ' selected' : ''}>${escapeHtml(t.name)}</option>`
|
||||
).join('');
|
||||
|
||||
if (_asProcessingTemplateEntitySelect) _asProcessingTemplateEntitySelect.destroy();
|
||||
if (templates.length > 0) {
|
||||
_asProcessingTemplateEntitySelect = new EntitySelect({
|
||||
target: select,
|
||||
getItems: () => templates.map((tmpl: any) => ({
|
||||
value: tmpl.id,
|
||||
label: tmpl.name,
|
||||
icon: ICON_AUDIO_TEMPLATE,
|
||||
})),
|
||||
placeholder: t('palette.search'),
|
||||
} as any);
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function _loadAudioTemplates(selectedId?: any) {
|
||||
const select = document.getElementById('audio-source-audio-template') as HTMLSelectElement | null;
|
||||
if (!select) return;
|
||||
@@ -623,7 +544,7 @@ export function initAudioSourceDelegation(container: HTMLElement): void {
|
||||
const handler = _audioSourceActions[action];
|
||||
if (handler) {
|
||||
// Verify we're inside an audio source section
|
||||
const section = btn.closest<HTMLElement>('[data-card-section="audio-multi"], [data-card-section="audio-mono"], [data-card-section="audio-band-extract"]');
|
||||
const section = btn.closest<HTMLElement>('[data-card-section="audio-capture"], [data-card-section="audio-processed"]');
|
||||
if (!section) return;
|
||||
const card = btn.closest<HTMLElement>('[data-id]');
|
||||
const id = card?.getAttribute('data-id');
|
||||
@@ -695,3 +616,11 @@ function _renderAudioSpectrum() {
|
||||
beatDot!.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Removed types ─────────────────────────────────────────────
|
||||
// MonoAudioSource and BandExtractAudioSource have been removed.
|
||||
// Channel selection is now handled by the channel_extract audio filter.
|
||||
// Band filtering is now handled by the band_extract audio filter.
|
||||
// These are applied via ProcessedAudioSource referencing an AudioProcessingTemplate.
|
||||
// Exported stubs for backward compatibility (no-op):
|
||||
export function onBandPresetChange() { /* removed */ }
|
||||
|
||||
@@ -1544,7 +1544,7 @@ async function _loadAudioSources() {
|
||||
try {
|
||||
const sources: any[] = await audioSourcesCache.fetch();
|
||||
select.innerHTML = sources.map(s => {
|
||||
const badge = s.source_type === 'multichannel' ? ' [multichannel]' : s.source_type === 'band_extract' ? ' [band]' : ' [mono]';
|
||||
const badge = s.source_type === 'capture' ? ' [capture]' : ' [processed]';
|
||||
return `<option value="${s.id}">${escapeHtml(s.name)}${badge}</option>`;
|
||||
}).join('');
|
||||
if (sources.length === 0) {
|
||||
@@ -1693,8 +1693,8 @@ const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
|
||||
${source.audio_source_id ? (() => {
|
||||
const as = audioSourceMap && audioSourceMap[source.audio_source_id];
|
||||
const asName = as ? as.name : source.audio_source_id;
|
||||
const asSection = as ? (as.source_type === 'mono' ? 'audio-mono' : as.source_type === 'band_extract' ? 'audio-band-extract' : 'audio-multi') : 'audio-multi';
|
||||
const asTab = as ? (as.source_type === 'mono' ? 'audio_mono' : as.source_type === 'band_extract' ? 'audio_band_extract' : 'audio_multi') : 'audio_multi';
|
||||
const asSection = as ? (as.source_type === 'processed' ? 'audio-processed' : 'audio-capture') : 'audio-capture';
|
||||
const asTab = as ? (as.source_type === 'processed' ? 'audio_processed' : 'audio_capture') : 'audio_capture';
|
||||
return `<span class="stream-card-prop${as ? ' stream-card-link' : ''}" title="${t('color_strip.audio.source')}"${as ? ` onclick="event.stopPropagation(); navigateToCard('streams','${asTab}','${asSection}','data-id','${source.audio_source_id}')"` : ''}>${ICON_AUDIO_LOOPBACK} ${escapeHtml(asName)}</span>`;
|
||||
})() : ''}
|
||||
${source.mirror ? `<span class="stream-card-prop">🪞</span>` : ''}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { t } from '../core/i18n.ts';
|
||||
import { ICON_HEART, ICON_EXTERNAL_LINK, ICON_X, ICON_GITHUB } from '../core/icons.ts';
|
||||
import { ICON_HEART, ICON_EXTERNAL_LINK, ICON_X, ICON_GITHUB, ICON_HELP } from '../core/icons.ts';
|
||||
|
||||
// ─── Config ─────────────────────────────────────────────────
|
||||
|
||||
@@ -114,13 +114,10 @@ function _showBanner(): void {
|
||||
</a>`;
|
||||
}
|
||||
|
||||
if (_repoUrl) {
|
||||
actions += `<a href="${_repoUrl}" target="_blank" rel="noopener"
|
||||
class="btn btn-icon donation-banner-action"
|
||||
title="${t('donation.view_source')}">
|
||||
${ICON_GITHUB}
|
||||
</a>`;
|
||||
}
|
||||
actions += `<button class="btn btn-icon donation-banner-action"
|
||||
onclick="openSettingsModal(); switchSettingsTab('about')" title="${t('donation.about')}">
|
||||
${ICON_HELP}
|
||||
</button>`;
|
||||
|
||||
actions += `<button class="btn btn-icon donation-banner-action"
|
||||
onclick="snoozeDonation()" title="${t('donation.later')}">
|
||||
|
||||
@@ -64,7 +64,7 @@ import {
|
||||
getEngineIcon, getAudioEngineIcon, getPictureSourceIcon, getAudioSourceIcon, getColorStripIcon,
|
||||
ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE,
|
||||
ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_CLOCK, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT,
|
||||
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO, ICON_ACTIVITY,
|
||||
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH,
|
||||
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_CSPT, ICON_HELP, ICON_TRASH, ICON_PALETTE, ICON_ASSET,
|
||||
ICON_GAMEPAD,
|
||||
getAssetTypeIcon,
|
||||
@@ -168,9 +168,8 @@ const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section
|
||||
const csRawTemplates = new CardSection('raw-templates', { titleKey: 'templates.title', gridClass: 'templates-grid', addCardOnclick: "showAddTemplateModal()", keyAttr: 'data-template-id', emptyKey: 'section.empty.capture_templates', bulkActions: _captureTemplateDeleteAction });
|
||||
const csProcStreams = new CardSection('proc-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('processed')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
|
||||
const csProcTemplates = new CardSection('proc-templates', { titleKey: 'postprocessing.title', gridClass: 'templates-grid', addCardOnclick: "showAddPPTemplateModal()", keyAttr: 'data-pp-template-id', emptyKey: 'section.empty.pp_templates', bulkActions: _ppTemplateDeleteAction });
|
||||
const csAudioMulti = new CardSection('audio-multi', { titleKey: 'audio_source.group.multichannel', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('multichannel')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
|
||||
const csAudioMono = new CardSection('audio-mono', { titleKey: 'audio_source.group.mono', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('mono')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
|
||||
const csAudioBandExtract = new CardSection('audio-band-extract', { titleKey: 'audio_source.group.band_extract', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('band_extract')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
|
||||
const csAudioCapture = new CardSection('audio-capture', { titleKey: 'audio_source.group.capture', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('capture')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
|
||||
const csAudioProcessed = new CardSection('audio-processed', { titleKey: 'audio_source.group.processed', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('processed')", keyAttr: 'data-id', emptyKey: 'section.empty.audio_sources', bulkActions: _audioSourceDeleteAction });
|
||||
const csStaticStreams = new CardSection('static-streams', { titleKey: 'streams.group.static_image', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('static_image')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
|
||||
const csVideoStreams = new CardSection('video-streams', { titleKey: 'streams.group.video', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('video')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
|
||||
const csAudioTemplates = new CardSection('audio-templates', { titleKey: 'audio_template.title', gridClass: 'templates-grid', addCardOnclick: "showAddAudioTemplateModal()", keyAttr: 'data-audio-template-id', emptyKey: 'section.empty.audio_templates', bulkActions: _audioTemplateDeleteAction });
|
||||
@@ -349,9 +348,8 @@ const _streamSectionMap = {
|
||||
proc_templates: [csProcTemplates],
|
||||
css_processing: [csCSPTemplates],
|
||||
color_strip: [csColorStrips],
|
||||
audio_multi: [csAudioMulti],
|
||||
audio_mono: [csAudioMono],
|
||||
audio_band_extract: [csAudioBandExtract],
|
||||
audio_capture: [csAudioCapture],
|
||||
audio_processed: [csAudioProcessed],
|
||||
audio_templates: [csAudioTemplates],
|
||||
value: [csValueSources],
|
||||
sync: [csSyncClocks],
|
||||
@@ -552,9 +550,8 @@ function renderPictureSourcesList(streams: any) {
|
||||
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
|
||||
const videoStreams = streams.filter(s => s.stream_type === 'video');
|
||||
|
||||
const multichannelSources = _cachedAudioSources.filter(s => s.source_type === 'multichannel');
|
||||
const monoSources = _cachedAudioSources.filter(s => s.source_type === 'mono');
|
||||
const bandExtractSources = _cachedAudioSources.filter(s => s.source_type === 'band_extract');
|
||||
const captureSources = _cachedAudioSources.filter(s => s.source_type === 'capture');
|
||||
const processedAudioSources = _cachedAudioSources.filter(s => s.source_type === 'processed');
|
||||
|
||||
// CSPT templates
|
||||
const csptTemplates = csptCache.data;
|
||||
@@ -578,9 +575,8 @@ function renderPictureSourcesList(streams: any) {
|
||||
{ key: 'css_processing', icon: ICON_CSPT, titleKey: 'streams.group.css_processing', count: csptTemplates.length },
|
||||
{ key: 'color_strip', icon: getColorStripIcon('static'), titleKey: 'streams.group.color_strip', count: colorStrips.length },
|
||||
{ key: 'gradients', icon: ICON_PALETTE, titleKey: 'streams.group.gradients', count: gradients.length },
|
||||
{ key: 'audio_multi', icon: getAudioSourceIcon('multichannel'), titleKey: 'audio_source.group.multichannel', count: multichannelSources.length },
|
||||
{ key: 'audio_mono', icon: getAudioSourceIcon('mono'), titleKey: 'audio_source.group.mono', count: monoSources.length },
|
||||
{ key: 'audio_band_extract', icon: getAudioSourceIcon('band_extract'), titleKey: 'audio_source.group.band_extract', count: bandExtractSources.length },
|
||||
{ key: 'audio_capture', icon: getAudioSourceIcon('capture'), titleKey: 'audio_source.group.capture', count: captureSources.length },
|
||||
{ key: 'audio_processed', icon: getAudioSourceIcon('processed'), titleKey: 'audio_source.group.processed', count: processedAudioSources.length },
|
||||
{ key: 'audio_templates', icon: ICON_AUDIO_TEMPLATE, titleKey: 'streams.group.audio_templates', count: _cachedAudioTemplates.length },
|
||||
{ key: 'value', icon: ICON_VALUE_SOURCE, titleKey: 'streams.group.value', count: _cachedValueSources.length },
|
||||
{ key: 'sync', icon: ICON_CLOCK, titleKey: 'streams.group.sync', count: _cachedSyncClocks.length },
|
||||
@@ -628,11 +624,10 @@ function renderPictureSourcesList(streams: any) {
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'audio_group', icon: getAudioSourceIcon('multichannel'), titleKey: 'tree.group.audio',
|
||||
key: 'audio_group', icon: getAudioSourceIcon('capture'), titleKey: 'tree.group.audio',
|
||||
children: [
|
||||
{ key: 'audio_multi', titleKey: 'audio_source.group.multichannel', icon: getAudioSourceIcon('multichannel'), count: multichannelSources.length },
|
||||
{ key: 'audio_mono', titleKey: 'audio_source.group.mono', icon: getAudioSourceIcon('mono'), count: monoSources.length },
|
||||
{ key: 'audio_band_extract', titleKey: 'audio_source.group.band_extract', icon: getAudioSourceIcon('band_extract'), count: bandExtractSources.length },
|
||||
{ key: 'audio_capture', titleKey: 'audio_source.group.capture', icon: getAudioSourceIcon('capture'), count: captureSources.length },
|
||||
{ key: 'audio_processed', titleKey: 'audio_source.group.processed', icon: getAudioSourceIcon('processed'), count: processedAudioSources.length },
|
||||
{ key: 'audio_templates', titleKey: 'tree.leaf.templates', icon: ICON_AUDIO_TEMPLATE, count: _cachedAudioTemplates.length },
|
||||
]
|
||||
},
|
||||
@@ -655,51 +650,34 @@ function renderPictureSourcesList(streams: any) {
|
||||
}
|
||||
];
|
||||
|
||||
const _bandLabels: Record<string, string> = { bass: 'Bass', mid: 'Mid', treble: 'Treble', custom: 'Custom' };
|
||||
|
||||
const _getSectionForSource = (sourceType: string): string => {
|
||||
if (sourceType === 'multichannel') return 'audio-multi';
|
||||
if (sourceType === 'mono') return 'audio-mono';
|
||||
return 'audio-band-extract';
|
||||
if (sourceType === 'capture') return 'audio-capture';
|
||||
return 'audio-processed';
|
||||
};
|
||||
|
||||
const _getTabForSource = (sourceType: string): string => {
|
||||
if (sourceType === 'multichannel') return 'audio_multi';
|
||||
if (sourceType === 'mono') return 'audio_mono';
|
||||
return 'audio_band_extract';
|
||||
if (sourceType === 'capture') return 'audio_capture';
|
||||
return 'audio_processed';
|
||||
};
|
||||
|
||||
const renderAudioSourceCard = (src: any) => {
|
||||
const icon = getAudioSourceIcon(src.source_type);
|
||||
|
||||
let propsHtml = '';
|
||||
if (src.source_type === 'mono') {
|
||||
if (src.source_type === 'processed') {
|
||||
const parent = _cachedAudioSources.find(s => s.id === src.audio_source_id);
|
||||
const parentName = parent ? parent.name : src.audio_source_id;
|
||||
const chLabel = src.channel === 'left' ? 'L' : src.channel === 'right' ? 'R' : 'M';
|
||||
const parentSection = parent ? _getSectionForSource(parent.source_type) : 'audio-capture';
|
||||
const parentTab = parent ? _getTabForSource(parent.source_type) : 'audio_capture';
|
||||
const parentBadge = parent
|
||||
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.parent'))}" onclick="event.stopPropagation(); navigateToCard('streams','audio_multi','audio-multi','data-id','${src.audio_source_id}')">${ICON_AUDIO_LOOPBACK} ${escapeHtml(parentName)}</span>`
|
||||
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.parent'))}" onclick="event.stopPropagation(); navigateToCard('streams','${parentTab}','${parentSection}','data-id','${src.audio_source_id}')">${getAudioSourceIcon(parent.source_type)} ${escapeHtml(parentName)}</span>`
|
||||
: `<span class="stream-card-prop" title="${escapeHtml(t('audio_source.parent'))}">${ICON_AUDIO_LOOPBACK} ${escapeHtml(parentName)}</span>`;
|
||||
propsHtml = `
|
||||
${parentBadge}
|
||||
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.channel'))}">${ICON_RADIO} ${chLabel}</span>
|
||||
`;
|
||||
} else if (src.source_type === 'band_extract') {
|
||||
const parent = _cachedAudioSources.find(s => s.id === src.audio_source_id);
|
||||
const parentName = parent ? parent.name : src.audio_source_id;
|
||||
const parentSection = parent ? _getSectionForSource(parent.source_type) : 'audio-multi';
|
||||
const parentTab = parent ? _getTabForSource(parent.source_type) : 'audio_multi';
|
||||
const parentBadge = parent
|
||||
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.band_parent'))}" onclick="event.stopPropagation(); navigateToCard('streams','${parentTab}','${parentSection}','data-id','${src.audio_source_id}')">${getAudioSourceIcon(parent.source_type)} ${escapeHtml(parentName)}</span>`
|
||||
: `<span class="stream-card-prop" title="${escapeHtml(t('audio_source.band_parent'))}">${ICON_ACTIVITY} ${escapeHtml(parentName)}</span>`;
|
||||
const bandLabel = _bandLabels[src.band] || src.band;
|
||||
const freqRange = `${Math.round(src.freq_low)}–${Math.round(src.freq_high)} Hz`;
|
||||
propsHtml = `
|
||||
${parentBadge}
|
||||
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.band'))}">${ICON_ACTIVITY} ${bandLabel}</span>
|
||||
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.freq_range'))}">${freqRange}</span>
|
||||
`;
|
||||
propsHtml = `${parentBadge}`;
|
||||
if (src.audio_processing_template_id) {
|
||||
propsHtml += `<span class="stream-card-prop">${ICON_AUDIO_TEMPLATE} ${escapeHtml(src.audio_processing_template_id)}</span>`;
|
||||
}
|
||||
} else {
|
||||
// Capture source
|
||||
const devIdx = src.device_index ?? -1;
|
||||
const loopback = src.is_loopback !== false;
|
||||
const devLabel = loopback ? `${ICON_AUDIO_LOOPBACK} Loopback` : `${ICON_AUDIO_INPUT} Input`;
|
||||
@@ -800,9 +778,8 @@ function renderPictureSourcesList(streams: any) {
|
||||
const rawTemplateItems = csRawTemplates.applySortOrder(_cachedCaptureTemplates.map(t => ({ key: t.id, html: renderCaptureTemplateCard(t) })));
|
||||
const procStreamItems = csProcStreams.applySortOrder(processedStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
|
||||
const procTemplateItems = csProcTemplates.applySortOrder(_cachedPPTemplates.map(t => ({ key: t.id, html: renderPPTemplateCard(t) })));
|
||||
const multiItems = csAudioMulti.applySortOrder(multichannelSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
|
||||
const monoItems = csAudioMono.applySortOrder(monoSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
|
||||
const bandExtractItems = csAudioBandExtract.applySortOrder(bandExtractSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
|
||||
const captureItems = csAudioCapture.applySortOrder(captureSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
|
||||
const processedAudioItems = csAudioProcessed.applySortOrder(processedAudioSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
|
||||
const audioTemplateItems = csAudioTemplates.applySortOrder(_cachedAudioTemplates.map(t => ({ key: t.id, html: renderAudioTemplateCard(t) })));
|
||||
const staticItems = csStaticStreams.applySortOrder(staticImageStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
|
||||
const videoItems = csVideoStreams.applySortOrder(videoStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
|
||||
@@ -846,9 +823,8 @@ function renderPictureSourcesList(streams: any) {
|
||||
csCSPTemplates.reconcile(csptItems);
|
||||
csColorStrips.reconcile(colorStripItems);
|
||||
csGradients.reconcile(gradientItems);
|
||||
csAudioMulti.reconcile(multiItems);
|
||||
csAudioMono.reconcile(monoItems);
|
||||
csAudioBandExtract.reconcile(bandExtractItems);
|
||||
csAudioCapture.reconcile(captureItems);
|
||||
csAudioProcessed.reconcile(processedAudioItems);
|
||||
csAudioTemplates.reconcile(audioTemplateItems);
|
||||
csStaticStreams.reconcile(staticItems);
|
||||
csVideoStreams.reconcile(videoItems);
|
||||
@@ -870,9 +846,8 @@ function renderPictureSourcesList(streams: any) {
|
||||
else if (tab.key === 'css_processing') panelContent = csCSPTemplates.render(csptItems);
|
||||
else if (tab.key === 'color_strip') panelContent = csColorStrips.render(colorStripItems);
|
||||
else if (tab.key === 'gradients') panelContent = csGradients.render(gradientItems);
|
||||
else if (tab.key === 'audio_multi') panelContent = csAudioMulti.render(multiItems);
|
||||
else if (tab.key === 'audio_mono') panelContent = csAudioMono.render(monoItems);
|
||||
else if (tab.key === 'audio_band_extract') panelContent = csAudioBandExtract.render(bandExtractItems);
|
||||
else if (tab.key === 'audio_capture') panelContent = csAudioCapture.render(captureItems);
|
||||
else if (tab.key === 'audio_processed') panelContent = csAudioProcessed.render(processedAudioItems);
|
||||
else if (tab.key === 'audio_templates') panelContent = csAudioTemplates.render(audioTemplateItems);
|
||||
else if (tab.key === 'value') panelContent = csValueSources.render(valueItems);
|
||||
else if (tab.key === 'sync') panelContent = csSyncClocks.render(syncClockItems);
|
||||
@@ -887,7 +862,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = panels;
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioMulti, csAudioMono, csAudioBandExtract, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources, csHASources, csMQTTSources, csAssets, csGameIntegrations]);
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioCapture, csAudioProcessed, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources, csHASources, csMQTTSources, csAssets, csGameIntegrations]);
|
||||
|
||||
// Event delegation for card actions (replaces inline onclick handlers)
|
||||
initSyncClockDelegation(container);
|
||||
@@ -908,7 +883,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
'css-proc-templates': 'css_processing',
|
||||
'color-strips': 'color_strip',
|
||||
'gradients': 'gradients',
|
||||
'audio-multi': 'audio_multi', 'audio-mono': 'audio_mono', 'audio-band-extract': 'audio_band_extract',
|
||||
'audio-capture': 'audio_capture', 'audio-processed': 'audio_processed',
|
||||
'audio-templates': 'audio_templates',
|
||||
'value-sources': 'value',
|
||||
'sync-clocks': 'sync',
|
||||
|
||||
@@ -1245,8 +1245,8 @@ export function createValueSourceCard(src: ValueSource) {
|
||||
} else if (src.source_type === 'audio') {
|
||||
const audioSrc = _cachedAudioSources.find(a => a.id === src.audio_source_id);
|
||||
const audioName = audioSrc ? audioSrc.name : (src.audio_source_id || '-');
|
||||
const audioSection = audioSrc ? (audioSrc.source_type === 'mono' ? 'audio-mono' : audioSrc.source_type === 'band_extract' ? 'audio-band-extract' : 'audio-multi') : 'audio-multi';
|
||||
const audioTab = audioSrc ? (audioSrc.source_type === 'mono' ? 'audio_mono' : audioSrc.source_type === 'band_extract' ? 'audio_band_extract' : 'audio_multi') : 'audio_multi';
|
||||
const audioSection = audioSrc ? (audioSrc.source_type === 'processed' ? 'audio-processed' : 'audio-capture') : 'audio-capture';
|
||||
const audioTab = audioSrc ? (audioSrc.source_type === 'processed' ? 'audio_processed' : 'audio_capture') : 'audio_capture';
|
||||
const modeLabel = src.mode || 'rms';
|
||||
const audioBadge = audioSrc
|
||||
? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('value_source.audio_source'))}" onclick="event.stopPropagation(); navigateToCard('streams','${audioTab}','${audioSection}','data-id','${src.audio_source_id}')">${ICON_MUSIC} ${escapeHtml(audioName)}</span>`
|
||||
@@ -1384,7 +1384,7 @@ function _populateAudioSourceDropdown(selectedId: any) {
|
||||
const select = document.getElementById('value-source-audio-source') as HTMLSelectElement;
|
||||
if (!select) return;
|
||||
select.innerHTML = _cachedAudioSources.map((s: any) => {
|
||||
const badge = s.source_type === 'multichannel' ? ' [multichannel]' : s.source_type === 'band_extract' ? ' [band]' : ' [mono]';
|
||||
const badge = s.source_type === 'capture' ? ' [capture]' : ' [processed]';
|
||||
return `<option value="${s.id}"${s.id === selectedId ? ' selected' : ''}>${escapeHtml(s.name)}${badge}</option>`;
|
||||
}).join('');
|
||||
|
||||
|
||||
@@ -486,7 +486,7 @@ export type ValueSource =
|
||||
|
||||
// ── Audio Source ───────────────────────────────────────────────
|
||||
|
||||
export type AudioSourceType = 'multichannel' | 'mono' | 'band_extract';
|
||||
export type AudioSourceType = 'capture' | 'processed';
|
||||
|
||||
interface AudioSourceBase {
|
||||
id: string;
|
||||
@@ -498,31 +498,22 @@ interface AudioSourceBase {
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface MultichannelAudioSource extends AudioSourceBase {
|
||||
source_type: 'multichannel';
|
||||
export interface CaptureAudioSource extends AudioSourceBase {
|
||||
source_type: 'capture';
|
||||
device_index: number;
|
||||
is_loopback: boolean;
|
||||
audio_template_id?: string;
|
||||
}
|
||||
|
||||
export interface MonoAudioSource extends AudioSourceBase {
|
||||
source_type: 'mono';
|
||||
export interface ProcessedAudioSource extends AudioSourceBase {
|
||||
source_type: 'processed';
|
||||
audio_source_id: string;
|
||||
channel: string;
|
||||
}
|
||||
|
||||
export interface BandExtractAudioSource extends AudioSourceBase {
|
||||
source_type: 'band_extract';
|
||||
audio_source_id: string;
|
||||
band: string;
|
||||
freq_low: number;
|
||||
freq_high: number;
|
||||
audio_processing_template_id: string;
|
||||
}
|
||||
|
||||
export type AudioSource =
|
||||
| MultichannelAudioSource
|
||||
| MonoAudioSource
|
||||
| BandExtractAudioSource;
|
||||
| CaptureAudioSource
|
||||
| ProcessedAudioSource;
|
||||
|
||||
// ── Picture Source ─────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user