feat: add music sync viz modes and auto_gain audio filter
Lint & Test / test (push) Has been cancelled
Lint & Test / test (push) Has been cancelled
Add 4 new audio visualization modes powered by MusicAnalyzer: - pulse_on_beat: BPM-synced pulsing with smooth beat phase - energy_gradient: bass/mid/treble mapped to scrolling gradient - spectrum_bands: three VU zones for frequency bands - strobe_on_drop: state-driven strobe on detected musical drops MusicAnalyzer provides BPM estimation (median IBI), beat phase tracking, asymmetric energy envelope, 3-band frequency splitting, and drop detection state machine (idle/buildup/drop/recovery). Add auto_gain audio filter for automatic level normalization via rolling peak tracking with configurable target level and response time. Deprecate auto_gain on Audio Value Source (use the filter instead).
This commit is contained in:
@@ -68,6 +68,7 @@ class CSSEditorModal extends Modal {
|
||||
if (_smoothingWidget) { _smoothingWidget.destroy(); _smoothingWidget = null; }
|
||||
if (_audioSensitivityWidget) { _audioSensitivityWidget.destroy(); _audioSensitivityWidget = null; }
|
||||
if (_audioSmoothingWidget) { _audioSmoothingWidget.destroy(); _audioSmoothingWidget = null; }
|
||||
if (_audioBeatDecayWidget) { _audioBeatDecayWidget.destroy(); _audioBeatDecayWidget = null; }
|
||||
if (_effectIntensityWidget) { _effectIntensityWidget.destroy(); _effectIntensityWidget = null; }
|
||||
if (_effectScaleWidget) { _effectScaleWidget.destroy(); _effectScaleWidget = null; }
|
||||
if (_apiInputTimeoutWidget) { _apiInputTimeoutWidget.destroy(); _apiInputTimeoutWidget = null; }
|
||||
@@ -122,6 +123,7 @@ class CSSEditorModal extends Modal {
|
||||
audio_source: (document.getElementById('css-editor-audio-source') as HTMLInputElement).value,
|
||||
audio_sensitivity: _audioSensitivityWidget ? JSON.stringify(_audioSensitivityWidget.getValue()) : '1.0',
|
||||
audio_smoothing: _audioSmoothingWidget ? JSON.stringify(_audioSmoothingWidget.getValue()) : '0.3',
|
||||
audio_beat_decay: _audioBeatDecayWidget ? JSON.stringify(_audioBeatDecayWidget.getValue()) : '0.15',
|
||||
audio_palette: (document.getElementById('css-editor-audio-palette') as HTMLInputElement).value,
|
||||
audio_color: _audioColorWidget ? JSON.stringify(_audioColorWidget.getValue()) : '[]',
|
||||
audio_color_peak: _audioColorPeakWidget ? JSON.stringify(_audioColorPeakWidget.getValue()) : '[]',
|
||||
@@ -166,6 +168,7 @@ let _cssTagsInput: any = null;
|
||||
let _smoothingWidget: BindableScalarWidget | null = null;
|
||||
let _audioSensitivityWidget: BindableScalarWidget | null = null;
|
||||
let _audioSmoothingWidget: BindableScalarWidget | null = null;
|
||||
let _audioBeatDecayWidget: BindableScalarWidget | null = null;
|
||||
let _effectIntensityWidget: BindableScalarWidget | null = null;
|
||||
let _effectScaleWidget: BindableScalarWidget | null = null;
|
||||
let _apiInputTimeoutWidget: BindableScalarWidget | null = null;
|
||||
@@ -881,6 +884,19 @@ function _ensureAudioSmoothingWidget(): BindableScalarWidget {
|
||||
return _audioSmoothingWidget;
|
||||
}
|
||||
|
||||
function _ensureAudioBeatDecayWidget(): BindableScalarWidget {
|
||||
if (!_audioBeatDecayWidget) {
|
||||
_audioBeatDecayWidget = new BindableScalarWidget({
|
||||
container: document.getElementById('css-editor-audio-beat-decay-container')!,
|
||||
min: 0.01, max: 0.5, step: 0.01, default: 0.15,
|
||||
idPrefix: 'css-editor-audio-beat-decay',
|
||||
valueSources: () => _cachedValueSources,
|
||||
format: (v) => v.toFixed(2),
|
||||
});
|
||||
}
|
||||
return _audioBeatDecayWidget;
|
||||
}
|
||||
|
||||
function _ensureEffectIntensityWidget(): BindableScalarWidget {
|
||||
if (!_effectIntensityWidget) {
|
||||
_effectIntensityWidget = new BindableScalarWidget({
|
||||
@@ -1267,12 +1283,16 @@ function _ensureAudioVizIconSelect() {
|
||||
const sel = document.getElementById('css-editor-audio-viz') as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
const items = [
|
||||
{ value: 'spectrum', icon: _icon(P.activity), label: t('color_strip.audio.viz.spectrum'), desc: t('color_strip.audio.viz.spectrum.desc') },
|
||||
{ value: 'beat_pulse', icon: _icon(P.zap), label: t('color_strip.audio.viz.beat_pulse'), desc: t('color_strip.audio.viz.beat_pulse.desc') },
|
||||
{ value: 'vu_meter', icon: _icon(P.trendingUp), label: t('color_strip.audio.viz.vu_meter'), desc: t('color_strip.audio.viz.vu_meter.desc') },
|
||||
{ value: 'spectrum', icon: _icon(P.activity), label: t('color_strip.audio.viz.spectrum'), desc: t('color_strip.audio.viz.spectrum.desc') },
|
||||
{ value: 'beat_pulse', icon: _icon(P.zap), label: t('color_strip.audio.viz.beat_pulse'), desc: t('color_strip.audio.viz.beat_pulse.desc') },
|
||||
{ value: 'vu_meter', icon: _icon(P.trendingUp), label: t('color_strip.audio.viz.vu_meter'), desc: t('color_strip.audio.viz.vu_meter.desc') },
|
||||
{ value: 'pulse_on_beat', icon: _icon(P.heart), label: t('color_strip.audio.viz.pulse_on_beat'), desc: t('color_strip.audio.viz.pulse_on_beat.desc') },
|
||||
{ value: 'energy_gradient', icon: _icon(P.flame), label: t('color_strip.audio.viz.energy_gradient'), desc: t('color_strip.audio.viz.energy_gradient.desc') },
|
||||
{ value: 'spectrum_bands', icon: _icon(P.radio), label: t('color_strip.audio.viz.spectrum_bands'), desc: t('color_strip.audio.viz.spectrum_bands.desc') },
|
||||
{ value: 'strobe_on_drop', icon: _icon(P.sparkles), label: t('color_strip.audio.viz.strobe_on_drop'), desc: t('color_strip.audio.viz.strobe_on_drop.desc') },
|
||||
];
|
||||
if (_audioVizIconSelect) { _audioVizIconSelect.updateItems(items); return; }
|
||||
_audioVizIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||
_audioVizIconSelect = new IconSelect({ target: sel, items, columns: 4 });
|
||||
}
|
||||
|
||||
function _buildGradientEntityItems() {
|
||||
@@ -1660,14 +1680,18 @@ function _resetMappedState() {
|
||||
|
||||
export function onAudioVizChange() {
|
||||
const viz = (document.getElementById('css-editor-audio-viz') as HTMLInputElement).value;
|
||||
// Palette: spectrum / beat_pulse
|
||||
const MUSIC_MODES = new Set(['pulse_on_beat', 'energy_gradient', 'spectrum_bands', 'strobe_on_drop']);
|
||||
// Palette: spectrum / beat_pulse / energy_gradient / spectrum_bands
|
||||
(document.getElementById('css-editor-audio-palette-group') as HTMLElement).style.display =
|
||||
(viz === 'spectrum' || viz === 'beat_pulse') ? '' : 'none';
|
||||
(viz === 'spectrum' || viz === 'beat_pulse' || viz === 'energy_gradient' || viz === 'spectrum_bands') ? '' : 'none';
|
||||
// Base color + Peak color: vu_meter only
|
||||
(document.getElementById('css-editor-audio-color-group') as HTMLElement).style.display = viz === 'vu_meter' ? '' : 'none';
|
||||
(document.getElementById('css-editor-audio-color-peak-group') as HTMLElement).style.display = viz === 'vu_meter' ? '' : 'none';
|
||||
// Mirror: spectrum only
|
||||
(document.getElementById('css-editor-audio-mirror-group') as HTMLElement).style.display = viz === 'spectrum' ? '' : 'none';
|
||||
// Beat decay: new music modes only
|
||||
(document.getElementById('css-editor-audio-beat-decay-group') as HTMLElement).style.display = MUSIC_MODES.has(viz) ? '' : 'none';
|
||||
if (MUSIC_MODES.has(viz)) _ensureAudioBeatDecayWidget();
|
||||
_autoGenerateCSSName();
|
||||
}
|
||||
|
||||
@@ -1709,6 +1733,7 @@ function _loadAudioState(css: any) {
|
||||
|
||||
_ensureAudioSensitivityWidget().setValue(css.sensitivity ?? 1.0);
|
||||
_ensureAudioSmoothingWidget().setValue(css.smoothing ?? 0.3);
|
||||
_ensureAudioBeatDecayWidget().setValue(css.beat_decay ?? 0.15);
|
||||
|
||||
const audioGradientId = css.gradient_id || 'gr_builtin_rainbow';
|
||||
(document.getElementById('css-editor-audio-palette') as HTMLInputElement).value = audioGradientId;
|
||||
@@ -1729,6 +1754,7 @@ function _resetAudioState() {
|
||||
if (_audioVizIconSelect) _audioVizIconSelect.setValue('spectrum');
|
||||
_ensureAudioSensitivityWidget().setValue(1.0);
|
||||
_ensureAudioSmoothingWidget().setValue(0.3);
|
||||
_ensureAudioBeatDecayWidget().setValue(0.15);
|
||||
(document.getElementById('css-editor-audio-palette') as HTMLInputElement).value = 'gr_builtin_rainbow';
|
||||
if (_audioPaletteEntitySelect) _audioPaletteEntitySelect.setValue('gr_builtin_rainbow');
|
||||
_ensureAudioColorWidget().setValue([0, 255, 0]);
|
||||
@@ -1819,7 +1845,7 @@ const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
|
||||
audio: (source, { audioSourceMap }) => {
|
||||
const vizLabel = t('color_strip.audio.viz.' + (source.visualization_mode || 'spectrum')) || source.visualization_mode || 'spectrum';
|
||||
const vizMode = source.visualization_mode || 'spectrum';
|
||||
const showPalette = (vizMode === 'spectrum' || vizMode === 'beat_pulse') && source.palette;
|
||||
const showPalette = (vizMode === 'spectrum' || vizMode === 'beat_pulse' || vizMode === 'energy_gradient' || vizMode === 'spectrum_bands') && source.palette;
|
||||
const audioPaletteLabel = showPalette ? (t('color_strip.palette.' + source.palette) || source.palette) : '';
|
||||
return `
|
||||
<span class="stream-card-prop">${ICON_MUSIC} ${escapeHtml(vizLabel)}</span>
|
||||
@@ -2187,6 +2213,7 @@ const _typeHandlers: Record<string, { load: (...args: any[]) => any; reset: (...
|
||||
audio_source_id: (document.getElementById('css-editor-audio-source') as HTMLInputElement).value || null,
|
||||
sensitivity: _ensureAudioSensitivityWidget().getValue(),
|
||||
smoothing: _ensureAudioSmoothingWidget().getValue(),
|
||||
beat_decay: _ensureAudioBeatDecayWidget().getValue(),
|
||||
gradient_id: (document.getElementById('css-editor-audio-palette') as HTMLInputElement).value,
|
||||
color: _ensureAudioColorWidget().getValue(),
|
||||
color_peak: _ensureAudioColorPeakWidget().getValue(),
|
||||
|
||||
@@ -562,6 +562,7 @@
|
||||
"filters.inverter.desc": "Invert all levels (1 minus value)",
|
||||
"filters.beat_gate.desc": "Pass signal only around detected beats",
|
||||
"filters.delay.desc": "Time-shift the audio analysis by a delay",
|
||||
"filters.auto_gain.desc": "Auto-normalize audio levels to use full range",
|
||||
"postprocessing.description_label": "Description (optional):",
|
||||
"postprocessing.description_placeholder": "Describe this template...",
|
||||
"postprocessing.created": "Template created successfully",
|
||||
@@ -1264,6 +1265,16 @@
|
||||
"color_strip.audio.viz.beat_pulse.desc": "All LEDs pulse on the beat",
|
||||
"color_strip.audio.viz.vu_meter": "VU Meter",
|
||||
"color_strip.audio.viz.vu_meter.desc": "Volume level fills the strip",
|
||||
"color_strip.audio.viz.pulse_on_beat": "Pulse on Beat",
|
||||
"color_strip.audio.viz.pulse_on_beat.desc": "LEDs pulse with each detected beat",
|
||||
"color_strip.audio.viz.energy_gradient": "Energy Gradient",
|
||||
"color_strip.audio.viz.energy_gradient.desc": "Gradient intensity follows audio energy",
|
||||
"color_strip.audio.viz.spectrum_bands": "Spectrum Bands",
|
||||
"color_strip.audio.viz.spectrum_bands.desc": "Grouped frequency bands across the strip",
|
||||
"color_strip.audio.viz.strobe_on_drop": "Strobe on Drop",
|
||||
"color_strip.audio.viz.strobe_on_drop.desc": "Flash strobe effect on bass drops",
|
||||
"color_strip.audio.beat_decay": "Beat Decay:",
|
||||
"color_strip.audio.beat_decay.hint": "How quickly the beat pulse fades. Lower values = longer fade, higher = snappier response.",
|
||||
"color_strip.audio.source": "Audio Source:",
|
||||
"color_strip.audio.source.hint": "Audio source for this visualization. Can be a multichannel (device) or mono (single channel) source. Create and manage audio sources in the Sources tab.",
|
||||
"color_strip.audio.sensitivity": "Sensitivity:",
|
||||
|
||||
@@ -1194,6 +1194,16 @@
|
||||
"color_strip.audio.viz.beat_pulse.desc": "Все LED пульсируют в такт",
|
||||
"color_strip.audio.viz.vu_meter": "VU-метр",
|
||||
"color_strip.audio.viz.vu_meter.desc": "Уровень громкости заполняет ленту",
|
||||
"color_strip.audio.viz.pulse_on_beat": "Пульс на бит",
|
||||
"color_strip.audio.viz.pulse_on_beat.desc": "LED пульсируют при каждом ударе",
|
||||
"color_strip.audio.viz.energy_gradient": "Энергетический градиент",
|
||||
"color_strip.audio.viz.energy_gradient.desc": "Интенсивность градиента следует за энергией звука",
|
||||
"color_strip.audio.viz.spectrum_bands": "Полосы спектра",
|
||||
"color_strip.audio.viz.spectrum_bands.desc": "Группы частот по ленте",
|
||||
"color_strip.audio.viz.strobe_on_drop": "Стробоскоп на дропе",
|
||||
"color_strip.audio.viz.strobe_on_drop.desc": "Вспышка стробоскопа на басовых дропах",
|
||||
"color_strip.audio.beat_decay": "Затухание бита:",
|
||||
"color_strip.audio.beat_decay.hint": "Скорость затухания пульса. Меньшие значения = более долгое затухание, большие = более резкая реакция.",
|
||||
"color_strip.audio.source": "Аудиоисточник:",
|
||||
"color_strip.audio.source.hint": "Аудиоисточник для визуализации. Может быть многоканальным (устройство) или моно (один канал). Создавайте и управляйте аудиоисточниками на вкладке Источники.",
|
||||
"color_strip.audio.sensitivity": "Чувствительность:",
|
||||
|
||||
@@ -1194,6 +1194,16 @@
|
||||
"color_strip.audio.viz.beat_pulse.desc": "所有LED随节拍脉动",
|
||||
"color_strip.audio.viz.vu_meter": "VU 表",
|
||||
"color_strip.audio.viz.vu_meter.desc": "音量填充灯带",
|
||||
"color_strip.audio.viz.pulse_on_beat": "节拍脉动",
|
||||
"color_strip.audio.viz.pulse_on_beat.desc": "LED随每个检测到的节拍脉动",
|
||||
"color_strip.audio.viz.energy_gradient": "能量渐变",
|
||||
"color_strip.audio.viz.energy_gradient.desc": "渐变强度跟随音频能量",
|
||||
"color_strip.audio.viz.spectrum_bands": "频谱频段",
|
||||
"color_strip.audio.viz.spectrum_bands.desc": "分组频段分布在灯带上",
|
||||
"color_strip.audio.viz.strobe_on_drop": "低音闪烁",
|
||||
"color_strip.audio.viz.strobe_on_drop.desc": "低音下降时闪烁频闪效果",
|
||||
"color_strip.audio.beat_decay": "节拍衰减:",
|
||||
"color_strip.audio.beat_decay.hint": "节拍脉冲消退的速度。较低值 = 较长衰减,较高值 = 更灵敏的响应。",
|
||||
"color_strip.audio.source": "音频源:",
|
||||
"color_strip.audio.source.hint": "此可视化的音频源。可以是多声道(设备)或单声道(单通道)源。在源标签页中创建和管理音频源。",
|
||||
"color_strip.audio.sensitivity": "灵敏度:",
|
||||
|
||||
Reference in New Issue
Block a user