feat: add band_extract audio source type for frequency band filtering
Some checks failed
Lint & Test / test (push) Failing after 29s
Some checks failed
Lint & Test / test (push) Failing after 29s
New audio source type that filters a parent source to a specific frequency band (bass 20-250Hz, mid 250-4kHz, treble 4k-20kHz, or custom range). Supports chaining with frequency range intersection and cycle detection. Band filtering applied in both CSS audio streams and test WebSocket.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Audio Sources — CRUD for multichannel and mono audio sources.
|
||||
* Audio Sources — CRUD for multichannel, mono, and band extract 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.
|
||||
* CSS audio type references a mono source by ID.
|
||||
* mono sources extract a single channel from a multichannel source;
|
||||
* band extract sources filter a parent source to a frequency band.
|
||||
* CSS audio type references an audio source by ID.
|
||||
*
|
||||
* Card rendering is handled by streams.js (Audio tab).
|
||||
* This module manages the editor modal and API operations.
|
||||
@@ -38,6 +39,10 @@ class AudioSourceModal extends Modal {
|
||||
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,
|
||||
tags: JSON.stringify(_audioSourceTagsInput ? _audioSourceTagsInput.getValue() : []),
|
||||
};
|
||||
}
|
||||
@@ -49,21 +54,27 @@ const audioSourceModal = new AudioSourceModal();
|
||||
let _asTemplateEntitySelect: EntitySelect | null = null;
|
||||
let _asDeviceEntitySelect: EntitySelect | null = null;
|
||||
let _asParentEntitySelect: EntitySelect | null = null;
|
||||
let _asBandParentEntitySelect: EntitySelect | null = null;
|
||||
|
||||
// ── 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' },
|
||||
};
|
||||
|
||||
export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
const isEdit = !!editData;
|
||||
const titleKey = isEdit
|
||||
? (editData.source_type === 'mono' ? 'audio_source.edit.mono' : 'audio_source.edit.multichannel')
|
||||
: (sourceType === 'mono' ? 'audio_source.add.mono' : 'audio_source.add.multichannel');
|
||||
const st = isEdit ? editData.source_type : sourceType;
|
||||
const titleKey = _titleKeys[st]?.[isEdit ? 'edit' : 'add'] || _titleKeys.multichannel.add;
|
||||
|
||||
document.getElementById('audio-source-modal-title')!.innerHTML = `${ICON_MUSIC} ${t(titleKey)}`;
|
||||
(document.getElementById('audio-source-id') as HTMLInputElement).value = isEdit ? editData.id : '';
|
||||
(document.getElementById('audio-source-error') as HTMLElement).style.display = 'none';
|
||||
|
||||
const typeSelect = document.getElementById('audio-source-type') as HTMLSelectElement;
|
||||
typeSelect.value = isEdit ? editData.source_type : sourceType;
|
||||
typeSelect.value = st;
|
||||
typeSelect.disabled = isEdit; // can't change type after creation
|
||||
|
||||
onAudioSourceTypeChange();
|
||||
@@ -77,9 +88,15 @@ export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = _filterDevicesBySelectedTemplate;
|
||||
await _loadAudioDevices();
|
||||
_selectAudioDevice(editData.device_index, editData.is_loopback);
|
||||
} else {
|
||||
} else if (editData.source_type === 'mono') {
|
||||
_loadMultichannelSources(editData.audio_source_id);
|
||||
(document.getElementById('audio-source-channel') as HTMLSelectElement).value = editData.channel || 'mono';
|
||||
} else if (editData.source_type === 'band_extract') {
|
||||
_loadBandParentSources(editData.audio_source_id);
|
||||
(document.getElementById('audio-source-band') as HTMLSelectElement).value = editData.band || 'bass';
|
||||
(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 {
|
||||
(document.getElementById('audio-source-name') as HTMLInputElement).value = '';
|
||||
@@ -89,8 +106,14 @@ export async function showAudioSourceModal(sourceType: any, editData?: any) {
|
||||
_loadAudioTemplates();
|
||||
(document.getElementById('audio-source-audio-template') as HTMLSelectElement).onchange = _filterDevicesBySelectedTemplate;
|
||||
await _loadAudioDevices();
|
||||
} else {
|
||||
} else if (sourceType === 'mono') {
|
||||
_loadMultichannelSources();
|
||||
} else if (sourceType === 'band_extract') {
|
||||
_loadBandParentSources();
|
||||
(document.getElementById('audio-source-band') as HTMLSelectElement).value = 'bass';
|
||||
(document.getElementById('audio-source-freq-low') as HTMLInputElement).value = '20';
|
||||
(document.getElementById('audio-source-freq-high') as HTMLInputElement).value = '20000';
|
||||
onBandPresetChange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +134,12 @@ 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';
|
||||
}
|
||||
|
||||
// ── Save ──────────────────────────────────────────────────────
|
||||
@@ -136,9 +165,16 @@ export async function saveAudioSource() {
|
||||
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 {
|
||||
} else if (sourceType === 'mono') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -321,6 +357,30 @@ function _loadMultichannelSources(selectedId?: any) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 (sources.length > 0) {
|
||||
_asBandParentEntitySelect = new EntitySelect({
|
||||
target: select,
|
||||
getItems: () => sources.map((s: any) => ({
|
||||
value: s.id,
|
||||
label: s.name,
|
||||
icon: getAudioSourceIcon(s.source_type),
|
||||
desc: s.source_type,
|
||||
})),
|
||||
placeholder: t('palette.search'),
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
|
||||
function _loadAudioTemplates(selectedId?: any) {
|
||||
const select = document.getElementById('audio-source-audio-template') as HTMLSelectElement | null;
|
||||
if (!select) return;
|
||||
@@ -469,7 +529,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"]');
|
||||
const section = btn.closest<HTMLElement>('[data-card-section="audio-multi"], [data-card-section="audio-mono"], [data-card-section="audio-band-extract"]');
|
||||
if (!section) return;
|
||||
const card = btn.closest<HTMLElement>('[data-id]');
|
||||
const id = card?.getAttribute('data-id');
|
||||
|
||||
Reference in New Issue
Block a user