Fix ~68 pre-existing strict null errors across 13 feature modules. Add non-null assertions for DOM element lookups, null coalescing for optional values, and type guards for nullable properties. Zero tsc errors now with --noEmit.
549 lines
22 KiB
TypeScript
549 lines
22 KiB
TypeScript
/**
|
|
* Streams — Audio template CRUD, engine config, test modal.
|
|
* Extracted from streams.ts to reduce file size.
|
|
*/
|
|
|
|
import {
|
|
availableAudioEngines, setAvailableAudioEngines,
|
|
currentEditingAudioTemplateId, setCurrentEditingAudioTemplateId,
|
|
_audioTemplateNameManuallyEdited, set_audioTemplateNameManuallyEdited,
|
|
_cachedAudioTemplates,
|
|
audioTemplatesCache,
|
|
apiKey,
|
|
} from '../core/state.ts';
|
|
import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.ts';
|
|
import { t } from '../core/i18n.ts';
|
|
import { Modal } from '../core/modal.ts';
|
|
import { showToast, showConfirm, setupBackdropClose } from '../core/ui.ts';
|
|
import {
|
|
getAudioEngineIcon,
|
|
ICON_AUDIO_TEMPLATE,
|
|
} from '../core/icons.ts';
|
|
import * as P from '../core/icon-paths.ts';
|
|
import { TagInput } from '../core/tag-input.ts';
|
|
import { IconSelect } from '../core/icon-select.ts';
|
|
import { loadPictureSources } from './streams.ts';
|
|
|
|
const _icon = (d: string) => `<svg class="icon" viewBox="0 0 24 24">${d}</svg>`;
|
|
|
|
// ── TagInput instance for audio template modal ──
|
|
let _audioTemplateTagsInput: TagInput | null = null;
|
|
|
|
class AudioTemplateModal extends Modal {
|
|
constructor() { super('audio-template-modal'); }
|
|
|
|
snapshotValues() {
|
|
const vals: any = {
|
|
name: (document.getElementById('audio-template-name') as HTMLInputElement).value,
|
|
description: (document.getElementById('audio-template-description') as HTMLInputElement).value,
|
|
engine: (document.getElementById('audio-template-engine') as HTMLSelectElement).value,
|
|
tags: JSON.stringify(_audioTemplateTagsInput ? _audioTemplateTagsInput.getValue() : []),
|
|
};
|
|
document.querySelectorAll('#audio-engine-config-fields [data-config-key]').forEach((field: any) => {
|
|
vals['cfg_' + field.dataset.configKey] = field.value;
|
|
});
|
|
return vals;
|
|
}
|
|
|
|
onForceClose() {
|
|
if (_audioTemplateTagsInput) { _audioTemplateTagsInput.destroy(); _audioTemplateTagsInput = null; }
|
|
setCurrentEditingAudioTemplateId(null);
|
|
set_audioTemplateNameManuallyEdited(false);
|
|
}
|
|
}
|
|
|
|
const audioTemplateModal = new AudioTemplateModal();
|
|
|
|
// ===== Audio Templates =====
|
|
|
|
async function loadAvailableAudioEngines() {
|
|
try {
|
|
const response = await fetchWithAuth('/audio-engines');
|
|
if (!response.ok) throw new Error(`Failed to load audio engines: ${response.status}`);
|
|
const data = await response.json();
|
|
setAvailableAudioEngines(data.engines || []);
|
|
|
|
const select = document.getElementById('audio-template-engine') as HTMLSelectElement;
|
|
select.innerHTML = '';
|
|
|
|
availableAudioEngines.forEach((engine: any) => {
|
|
const option = document.createElement('option');
|
|
option.value = engine.type;
|
|
option.textContent = `${engine.type.toUpperCase()}`;
|
|
if (!engine.available) {
|
|
option.disabled = true;
|
|
option.textContent += ` (${t('audio_template.engine.unavailable')})`;
|
|
}
|
|
select.appendChild(option);
|
|
});
|
|
|
|
if (!select.value) {
|
|
const firstAvailable = availableAudioEngines.find(e => e.available);
|
|
if (firstAvailable) select.value = firstAvailable.type;
|
|
}
|
|
|
|
// Update icon-grid selector with dynamic engine list
|
|
const items = availableAudioEngines
|
|
.filter(e => e.available)
|
|
.map(e => ({ value: e.type, icon: getAudioEngineIcon(e.type), label: e.type.toUpperCase(), desc: '' }));
|
|
if (_audioEngineIconSelect) { _audioEngineIconSelect.updateItems(items); }
|
|
else { _audioEngineIconSelect = new IconSelect({ target: select, items, columns: 2 }); }
|
|
_audioEngineIconSelect.setValue(select.value);
|
|
} catch (error) {
|
|
console.error('Error loading audio engines:', error);
|
|
showToast(t('audio_template.error.engines') + ': ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
let _audioEngineIconSelect: IconSelect | null = null;
|
|
|
|
export async function onAudioEngineChange() {
|
|
const engineType = (document.getElementById('audio-template-engine') as HTMLSelectElement).value;
|
|
if (_audioEngineIconSelect) _audioEngineIconSelect.setValue(engineType);
|
|
const configSection = document.getElementById('audio-engine-config-section')!;
|
|
const configFields = document.getElementById('audio-engine-config-fields')!;
|
|
|
|
if (!engineType) { configSection.style.display = 'none'; return; }
|
|
|
|
const engine = availableAudioEngines.find((e: any) => e.type === engineType);
|
|
if (!engine) { configSection.style.display = 'none'; return; }
|
|
|
|
if (!_audioTemplateNameManuallyEdited && !(document.getElementById('audio-template-id') as HTMLInputElement).value) {
|
|
(document.getElementById('audio-template-name') as HTMLInputElement).value = engine.type.toUpperCase();
|
|
}
|
|
|
|
const hint = document.getElementById('audio-engine-availability-hint')!;
|
|
if (!engine.available) {
|
|
hint.textContent = t('audio_template.engine.unavailable.hint');
|
|
hint.style.display = 'block';
|
|
hint.style.color = 'var(--error-color)';
|
|
} else {
|
|
hint.style.display = 'none';
|
|
}
|
|
|
|
configFields.innerHTML = '';
|
|
const defaultConfig = engine.default_config || {};
|
|
|
|
if (Object.keys(defaultConfig).length === 0) {
|
|
configSection.style.display = 'none';
|
|
return;
|
|
} else {
|
|
let gridHtml = '<div class="config-grid">';
|
|
Object.entries(defaultConfig).forEach(([key, value]) => {
|
|
const fieldType = typeof value === 'number' ? 'number' : 'text';
|
|
const fieldValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : value;
|
|
gridHtml += `
|
|
<label class="config-grid-label" for="audio-config-${key}">${key}</label>
|
|
<div class="config-grid-value">
|
|
${typeof value === 'boolean' ? `
|
|
<select id="audio-config-${key}" data-config-key="${key}">
|
|
<option value="true" ${value ? 'selected' : ''}>true</option>
|
|
<option value="false" ${!value ? 'selected' : ''}>false</option>
|
|
</select>
|
|
` : `
|
|
<input type="${fieldType}" id="audio-config-${key}" data-config-key="${key}" value="${fieldValue}">
|
|
`}
|
|
</div>
|
|
`;
|
|
});
|
|
gridHtml += '</div>';
|
|
configFields.innerHTML = gridHtml;
|
|
}
|
|
|
|
configSection.style.display = 'block';
|
|
}
|
|
|
|
function populateAudioEngineConfig(config: any) {
|
|
Object.entries(config).forEach(([key, value]: [string, any]) => {
|
|
const field = document.getElementById(`audio-config-${key}`) as HTMLInputElement | HTMLSelectElement | null;
|
|
if (field) {
|
|
if (field.tagName === 'SELECT') {
|
|
field.value = value.toString();
|
|
} else {
|
|
field.value = value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function collectAudioEngineConfig() {
|
|
const config: any = {};
|
|
document.querySelectorAll('#audio-engine-config-fields [data-config-key]').forEach((field: any) => {
|
|
const key = field.dataset.configKey;
|
|
let value: any = field.value;
|
|
if (field.type === 'number') {
|
|
value = parseFloat(value);
|
|
} else if (field.tagName === 'SELECT' && (value === 'true' || value === 'false')) {
|
|
value = value === 'true';
|
|
}
|
|
config[key] = value;
|
|
});
|
|
return config;
|
|
}
|
|
|
|
async function loadAudioTemplates() {
|
|
try {
|
|
await audioTemplatesCache.fetch();
|
|
await loadPictureSources();
|
|
} catch (error) {
|
|
if (error.isAuth) return;
|
|
console.error('Error loading audio templates:', error);
|
|
showToast(t('audio_template.error.load'), 'error');
|
|
}
|
|
}
|
|
|
|
export async function showAddAudioTemplateModal(cloneData: any = null) {
|
|
setCurrentEditingAudioTemplateId(null);
|
|
document.getElementById('audio-template-modal-title')!.innerHTML = `${ICON_AUDIO_TEMPLATE} ${t('audio_template.add')}`;
|
|
(document.getElementById('audio-template-form') as HTMLFormElement).reset();
|
|
(document.getElementById('audio-template-id') as HTMLInputElement).value = '';
|
|
document.getElementById('audio-engine-config-section')!.style.display = 'none';
|
|
document.getElementById('audio-template-error')!.style.display = 'none';
|
|
|
|
set_audioTemplateNameManuallyEdited(!!cloneData);
|
|
(document.getElementById('audio-template-name') as HTMLInputElement).oninput = () => { set_audioTemplateNameManuallyEdited(true); };
|
|
|
|
await loadAvailableAudioEngines();
|
|
|
|
if (cloneData) {
|
|
(document.getElementById('audio-template-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)';
|
|
(document.getElementById('audio-template-description') as HTMLInputElement).value = cloneData.description || '';
|
|
(document.getElementById('audio-template-engine') as HTMLSelectElement).value = cloneData.engine_type;
|
|
await onAudioEngineChange();
|
|
populateAudioEngineConfig(cloneData.engine_config);
|
|
}
|
|
|
|
// Tags
|
|
if (_audioTemplateTagsInput) { _audioTemplateTagsInput.destroy(); _audioTemplateTagsInput = null; }
|
|
_audioTemplateTagsInput = new TagInput(document.getElementById('audio-template-tags-container'), { placeholder: t('tags.placeholder') });
|
|
_audioTemplateTagsInput.setValue(cloneData ? (cloneData.tags || []) : []);
|
|
|
|
audioTemplateModal.open();
|
|
audioTemplateModal.snapshot();
|
|
}
|
|
|
|
export async function editAudioTemplate(templateId: any) {
|
|
try {
|
|
const response = await fetchWithAuth(`/audio-templates/${templateId}`);
|
|
if (!response.ok) throw new Error(`Failed to load audio template: ${response.status}`);
|
|
const template = await response.json();
|
|
|
|
setCurrentEditingAudioTemplateId(templateId);
|
|
document.getElementById('audio-template-modal-title')!.innerHTML = `${ICON_AUDIO_TEMPLATE} ${t('audio_template.edit')}`;
|
|
(document.getElementById('audio-template-id') as HTMLInputElement).value = templateId;
|
|
(document.getElementById('audio-template-name') as HTMLInputElement).value = template.name;
|
|
(document.getElementById('audio-template-description') as HTMLInputElement).value = template.description || '';
|
|
|
|
await loadAvailableAudioEngines();
|
|
(document.getElementById('audio-template-engine') as HTMLSelectElement).value = template.engine_type;
|
|
await onAudioEngineChange();
|
|
populateAudioEngineConfig(template.engine_config);
|
|
|
|
document.getElementById('audio-template-error')!.style.display = 'none';
|
|
|
|
// Tags
|
|
if (_audioTemplateTagsInput) { _audioTemplateTagsInput.destroy(); _audioTemplateTagsInput = null; }
|
|
_audioTemplateTagsInput = new TagInput(document.getElementById('audio-template-tags-container'), { placeholder: t('tags.placeholder') });
|
|
_audioTemplateTagsInput.setValue(template.tags || []);
|
|
|
|
audioTemplateModal.open();
|
|
audioTemplateModal.snapshot();
|
|
} catch (error: any) {
|
|
console.error('Error loading audio template:', error);
|
|
showToast(t('audio_template.error.load') + ': ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
export async function closeAudioTemplateModal() {
|
|
await audioTemplateModal.close();
|
|
}
|
|
|
|
export async function saveAudioTemplate() {
|
|
const templateId = currentEditingAudioTemplateId;
|
|
const name = (document.getElementById('audio-template-name') as HTMLInputElement).value.trim();
|
|
const engineType = (document.getElementById('audio-template-engine') as HTMLSelectElement).value;
|
|
|
|
if (!name || !engineType) {
|
|
showToast(t('audio_template.error.required'), 'error');
|
|
return;
|
|
}
|
|
|
|
const description = (document.getElementById('audio-template-description') as HTMLInputElement).value.trim();
|
|
const engineConfig = collectAudioEngineConfig();
|
|
|
|
const payload = { name, engine_type: engineType, engine_config: engineConfig, description: description || null, tags: _audioTemplateTagsInput ? _audioTemplateTagsInput.getValue() : [] };
|
|
|
|
try {
|
|
let response;
|
|
if (templateId) {
|
|
response = await fetchWithAuth(`/audio-templates/${templateId}`, { method: 'PUT', body: JSON.stringify(payload) });
|
|
} else {
|
|
response = await fetchWithAuth('/audio-templates', { method: 'POST', body: JSON.stringify(payload) });
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || error.message || 'Failed to save audio template');
|
|
}
|
|
|
|
showToast(templateId ? t('audio_template.updated') : t('audio_template.created'), 'success');
|
|
audioTemplateModal.forceClose();
|
|
audioTemplatesCache.invalidate();
|
|
await loadAudioTemplates();
|
|
} catch (error) {
|
|
console.error('Error saving audio template:', error);
|
|
document.getElementById('audio-template-error')!.textContent = (error as any).message;
|
|
document.getElementById('audio-template-error')!.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
export async function deleteAudioTemplate(templateId: any) {
|
|
const confirmed = await showConfirm(t('audio_template.delete.confirm'));
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
const response = await fetchWithAuth(`/audio-templates/${templateId}`, { method: 'DELETE' });
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || error.message || 'Failed to delete audio template');
|
|
}
|
|
showToast(t('audio_template.deleted'), 'success');
|
|
audioTemplatesCache.invalidate();
|
|
await loadAudioTemplates();
|
|
} catch (error) {
|
|
console.error('Error deleting audio template:', error);
|
|
showToast(t('audio_template.error.delete') + ': ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
export async function cloneAudioTemplate(templateId: any) {
|
|
try {
|
|
const resp = await fetchWithAuth(`/audio-templates/${templateId}`);
|
|
if (!resp.ok) throw new Error('Failed to load audio template');
|
|
const tmpl = await resp.json();
|
|
showAddAudioTemplateModal(tmpl);
|
|
} catch (error) {
|
|
if (error.isAuth) return;
|
|
console.error('Failed to clone audio template:', error);
|
|
showToast(t('audio_template.error.clone_failed'), 'error');
|
|
}
|
|
}
|
|
|
|
// ===== Audio Template Test =====
|
|
|
|
const NUM_BANDS_TPL = 64;
|
|
const TPL_PEAK_DECAY = 0.02;
|
|
const TPL_BEAT_FLASH_DECAY = 0.06;
|
|
|
|
let _tplTestWs: WebSocket | null = null;
|
|
let _tplTestAnimFrame: number | null = null;
|
|
let _tplTestLatest: any = null;
|
|
let _tplTestPeaks = new Float32Array(NUM_BANDS_TPL);
|
|
let _tplTestBeatFlash = 0;
|
|
let _currentTestAudioTemplateId: string | null = null;
|
|
|
|
const testAudioTemplateModal = new Modal('test-audio-template-modal', { backdrop: true, lock: true });
|
|
|
|
export async function showTestAudioTemplateModal(templateId: any) {
|
|
_currentTestAudioTemplateId = templateId;
|
|
|
|
// Find template's engine type so we show the correct device list
|
|
const template = _cachedAudioTemplates.find((t: any) => t.id === templateId);
|
|
const engineType = template ? template.engine_type : null;
|
|
|
|
// Load audio devices for picker — filter by engine type
|
|
const deviceSelect = document.getElementById('test-audio-template-device') as HTMLSelectElement;
|
|
try {
|
|
const resp = await fetchWithAuth('/audio-devices');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
// Use engine-specific device list if available, fall back to flat list
|
|
const devices = (engineType && data.by_engine && data.by_engine[engineType])
|
|
? data.by_engine[engineType]
|
|
: (data.devices || []);
|
|
deviceSelect.innerHTML = devices.map(d => {
|
|
const label = d.name;
|
|
const val = `${d.index}:${d.is_loopback ? '1' : '0'}`;
|
|
return `<option value="${val}">${escapeHtml(label)}</option>`;
|
|
}).join('');
|
|
if (devices.length === 0) {
|
|
deviceSelect.innerHTML = '<option value="-1:1">Default</option>';
|
|
}
|
|
}
|
|
} catch {
|
|
deviceSelect.innerHTML = '<option value="-1:1">Default</option>';
|
|
}
|
|
|
|
// Restore last used device
|
|
const lastDevice = localStorage.getItem('lastAudioTestDevice');
|
|
if (lastDevice) {
|
|
const opt = Array.from(deviceSelect.options).find((o: any) => o.value === lastDevice);
|
|
if (opt) deviceSelect.value = lastDevice;
|
|
}
|
|
|
|
// Reset visual state
|
|
document.getElementById('audio-template-test-canvas')!.style.display = 'none';
|
|
document.getElementById('audio-template-test-stats')!.style.display = 'none';
|
|
document.getElementById('audio-template-test-status')!.style.display = 'none';
|
|
document.getElementById('test-audio-template-start-btn')!.style.display = '';
|
|
|
|
_tplCleanupTest();
|
|
|
|
testAudioTemplateModal.open();
|
|
}
|
|
|
|
export function closeTestAudioTemplateModal() {
|
|
_tplCleanupTest();
|
|
testAudioTemplateModal.forceClose();
|
|
_currentTestAudioTemplateId = null;
|
|
}
|
|
|
|
export function startAudioTemplateTest() {
|
|
if (!_currentTestAudioTemplateId) return;
|
|
|
|
const deviceVal = (document.getElementById('test-audio-template-device') as HTMLSelectElement).value || '-1:1';
|
|
const [devIdx, devLoop] = deviceVal.split(':');
|
|
localStorage.setItem('lastAudioTestDevice', deviceVal);
|
|
|
|
// Show canvas + stats, hide run button, disable device picker
|
|
document.getElementById('audio-template-test-canvas')!.style.display = '';
|
|
document.getElementById('audio-template-test-stats')!.style.display = '';
|
|
document.getElementById('test-audio-template-start-btn')!.style.display = 'none';
|
|
(document.getElementById('test-audio-template-device') as HTMLSelectElement).disabled = true;
|
|
|
|
const statusEl = document.getElementById('audio-template-test-status')!;
|
|
statusEl.textContent = t('audio_source.test.connecting');
|
|
statusEl.style.display = '';
|
|
|
|
// Reset state
|
|
_tplTestLatest = null;
|
|
_tplTestPeaks.fill(0);
|
|
_tplTestBeatFlash = 0;
|
|
document.getElementById('audio-template-test-rms')!.textContent = '---';
|
|
document.getElementById('audio-template-test-peak')!.textContent = '---';
|
|
document.getElementById('audio-template-test-beat-dot')!.classList.remove('active');
|
|
|
|
// Size canvas
|
|
const canvas = document.getElementById('audio-template-test-canvas') as HTMLCanvasElement;
|
|
_tplSizeCanvas(canvas);
|
|
|
|
// Connect WebSocket
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const wsUrl = `${protocol}//${window.location.host}${API_BASE}/audio-templates/${_currentTestAudioTemplateId}/test/ws?token=${encodeURIComponent(apiKey ?? '')}&device_index=${devIdx}&is_loopback=${devLoop === '1' ? '1' : '0'}`;
|
|
|
|
try {
|
|
_tplTestWs = new WebSocket(wsUrl);
|
|
|
|
_tplTestWs.onopen = () => {
|
|
statusEl.style.display = 'none';
|
|
};
|
|
|
|
_tplTestWs.onmessage = (event) => {
|
|
try { _tplTestLatest = JSON.parse(event.data); } catch {}
|
|
};
|
|
|
|
_tplTestWs.onclose = () => { _tplTestWs = null; };
|
|
|
|
_tplTestWs.onerror = () => {
|
|
showToast(t('audio_source.test.error'), 'error');
|
|
_tplCleanupTest();
|
|
};
|
|
} catch {
|
|
showToast(t('audio_source.test.error'), 'error');
|
|
_tplCleanupTest();
|
|
return;
|
|
}
|
|
|
|
_tplTestAnimFrame = requestAnimationFrame(_tplRenderLoop);
|
|
}
|
|
|
|
function _tplCleanupTest() {
|
|
if (_tplTestAnimFrame) {
|
|
cancelAnimationFrame(_tplTestAnimFrame);
|
|
_tplTestAnimFrame = null;
|
|
}
|
|
if (_tplTestWs) {
|
|
_tplTestWs.onclose = null;
|
|
_tplTestWs.close();
|
|
_tplTestWs = null;
|
|
}
|
|
_tplTestLatest = null;
|
|
// Re-enable device picker
|
|
const devSel = document.getElementById('test-audio-template-device') as HTMLSelectElement | null;
|
|
if (devSel) devSel.disabled = false;
|
|
}
|
|
|
|
function _tplSizeCanvas(canvas: HTMLCanvasElement) {
|
|
const rect = canvas.parentElement!.getBoundingClientRect();
|
|
const dpr = window.devicePixelRatio || 1;
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = 200 * dpr;
|
|
canvas.style.height = '200px';
|
|
canvas.getContext('2d')!.scale(dpr, dpr);
|
|
}
|
|
|
|
function _tplRenderLoop() {
|
|
_tplRenderSpectrum();
|
|
if (testAudioTemplateModal.isOpen && _tplTestWs) {
|
|
_tplTestAnimFrame = requestAnimationFrame(_tplRenderLoop);
|
|
}
|
|
}
|
|
|
|
function _tplRenderSpectrum() {
|
|
const canvas = document.getElementById('audio-template-test-canvas') as HTMLCanvasElement | null;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d')!;
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const w = canvas.width / dpr;
|
|
const h = canvas.height / dpr;
|
|
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
const data = _tplTestLatest;
|
|
if (!data || !data.spectrum) return;
|
|
|
|
const spectrum = data.spectrum;
|
|
const gap = 1;
|
|
const barWidth = (w - gap * (NUM_BANDS_TPL - 1)) / NUM_BANDS_TPL;
|
|
|
|
// Beat flash
|
|
if (data.beat) _tplTestBeatFlash = Math.min(1.0, data.beat_intensity + 0.3);
|
|
if (_tplTestBeatFlash > 0) {
|
|
ctx.fillStyle = `rgba(255, 255, 255, ${_tplTestBeatFlash * 0.08})`;
|
|
ctx.fillRect(0, 0, w, h);
|
|
_tplTestBeatFlash = Math.max(0, _tplTestBeatFlash - TPL_BEAT_FLASH_DECAY);
|
|
}
|
|
|
|
for (let i = 0; i < NUM_BANDS_TPL; i++) {
|
|
const val = Math.min(1, spectrum[i]);
|
|
const barHeight = val * h;
|
|
const x = i * (barWidth + gap);
|
|
const y = h - barHeight;
|
|
|
|
const hue = (1 - val) * 120;
|
|
ctx.fillStyle = `hsl(${hue}, 85%, 50%)`;
|
|
ctx.fillRect(x, y, barWidth, barHeight);
|
|
|
|
if (val > _tplTestPeaks[i]) {
|
|
_tplTestPeaks[i] = val;
|
|
} else {
|
|
_tplTestPeaks[i] = Math.max(0, _tplTestPeaks[i] - TPL_PEAK_DECAY);
|
|
}
|
|
const peakY = h - _tplTestPeaks[i] * h;
|
|
const peakHue = (1 - _tplTestPeaks[i]) * 120;
|
|
ctx.fillStyle = `hsl(${peakHue}, 90%, 70%)`;
|
|
ctx.fillRect(x, peakY, barWidth, 2);
|
|
}
|
|
|
|
document.getElementById('audio-template-test-rms')!.textContent = (data.rms * 100).toFixed(1) + '%';
|
|
document.getElementById('audio-template-test-peak')!.textContent = (data.peak * 100).toFixed(1) + '%';
|
|
const beatDot = document.getElementById('audio-template-test-beat-dot')!;
|
|
if (data.beat) {
|
|
beatDot.classList.add('active');
|
|
} else {
|
|
beatDot.classList.remove('active');
|
|
}
|
|
}
|