Отладка звука

This commit is contained in:
Maxim
2026-06-23 10:10:14 +03:00
parent 78069b0aa4
commit 118e49c7f9
5 changed files with 449 additions and 99 deletions

View File

@@ -1,4 +1,4 @@
// app.ts - Оркестратор приложения
// web/src/app.ts - Оркестратор приложения
import {
fetchHealth,
@@ -38,12 +38,111 @@ const MAX_ERRORS: number = 3;
let pollTimer: number | null = null;
let lastSignalLevel: number = 0;
// ===== ИНИЦИАЛИЗАЦИЯ РЕГУЛЯТОРА ГРОМКОСТИ =====
function initVolumeControl(): void {
const slider = document.getElementById('volumeSlider') as HTMLInputElement | null;
const trackFill = document.getElementById('volumeTrackFill') as HTMLElement | null;
const indicator = document.getElementById('audio-indicator');
const wrapper = document.getElementById('audioWrapper');
if (!slider || !trackFill || !indicator) return;
// Функция обновления заливки
function updateTrackFill(volume: number): void {
if (trackFill) {
trackFill.style.width = `${volume}%`;
}
}
// Функция обновления индикатора в зависимости от громкости и уровня сигнала
function updateIndicator(volume: number, signalLevel: number): void {
if (!indicator) return;
if (volume === 0) {
indicator.textContent = '🔇';
indicator.classList.add('muted');
indicator.classList.remove('active');
} else if (signalLevel > 0) {
indicator.textContent = '🔊';
indicator.classList.remove('muted');
indicator.classList.add('active');
} else {
indicator.textContent = '🔇';
indicator.classList.add('muted');
indicator.classList.remove('active');
}
}
// Устанавливаем начальное состояние (звук выключен)
audioEngine.setVolume(0);
updateTrackFill(0);
lastSignalLevel = 0;
updateIndicator(0, 0);
// Обработчик изменения громкости
slider.addEventListener('input', () => {
const volume = parseInt(slider.value);
updateTrackFill(volume);
audioEngine.setVolume(volume);
if (volume === 0) {
// Громкость 0 - звук выключен
updateIndicator(0, 0);
if (lastSignalLevel > 0) {
audioEngine.updateLevel(0);
}
} else {
// Громкость > 0 - показываем состояние сигнала
updateIndicator(volume, lastSignalLevel);
if (lastSignalLevel > 0) {
audioEngine.updateLevel(lastSignalLevel);
}
}
});
// Для тач-устройств
if (wrapper) {
wrapper.addEventListener('click', (e) => {
const control = document.getElementById('volumeControl');
if (control && window.innerWidth <= 768) {
control.classList.toggle('visible');
e.stopPropagation();
}
});
document.addEventListener('click', (e) => {
const control = document.getElementById('volumeControl');
if (control && window.innerWidth <= 768) {
if (!wrapper.contains(e.target as Node)) {
control.classList.remove('visible');
}
}
});
}
// Сохраняем настройки
const savedVolume = localStorage.getItem('gport_volume');
if (savedVolume) {
const vol = parseInt(savedVolume);
if (vol >= 0 && vol <= 100) {
slider.value = String(vol);
slider.dispatchEvent(new Event('input'));
}
}
slider.addEventListener('change', () => {
localStorage.setItem('gport_volume', slider.value);
});
}
// ================= INIT =================
window.addEventListener("DOMContentLoaded", () => {
initDOM();
initAccordion();
initResize();
initCamera();
initVolumeControl();
document.addEventListener('click', () => {
audioEngine.updateLevel(0);
@@ -55,10 +154,32 @@ window.addEventListener("DOMContentLoaded", () => {
setSignal(state.peakSignalLevel);
const signalLevel = state.peakSignalLevel;
const currentVolume = audioEngine.getVolume();
if (signalLevel !== lastSignalLevel) {
lastSignalLevel = signalLevel;
audioEngine.updateLevel(signalLevel);
setAudioIndicator(signalLevel > 0, signalLevel);
// Обновляем индикатор в зависимости от громкости и уровня сигнала
const indicator = document.getElementById('audio-indicator');
if (indicator) {
if (currentVolume > 0 && signalLevel > 0) {
indicator.textContent = '🔊';
indicator.classList.remove('muted');
indicator.classList.add('active');
audioEngine.updateLevel(signalLevel);
} else if (currentVolume > 0 && signalLevel === 0) {
indicator.textContent = '🔇';
indicator.classList.add('muted');
indicator.classList.remove('active');
audioEngine.updateLevel(0);
} else {
indicator.textContent = '🔇';
indicator.classList.add('muted');
indicator.classList.remove('active');
audioEngine.updateLevel(0);
}
}
setAudioIndicator(signalLevel > 0 && currentVolume > 0, signalLevel);
}
}
});
@@ -102,14 +223,14 @@ document.addEventListener("keydown", (e: KeyboardEvent) => {
}
break;
case 'm':
if (lastSignalLevel > 0) {
if (lastSignalLevel > 0 && audioEngine.getVolume() > 0) {
audioEngine.updateLevel(0);
setAudioIndicator(false);
window._mutedLevel = lastSignalLevel;
lastSignalLevel = 0;
} else {
const level = window._mutedLevel || state.peakSignalLevel;
if (level > 0) {
if (level > 0 && audioEngine.getVolume() > 0) {
audioEngine.updateLevel(level);
lastSignalLevel = level;
setAudioIndicator(true, level);
@@ -239,12 +360,10 @@ async function update(): Promise<void> {
if (arr.length === 0) {
DOM.history.innerHTML = 'Нет данных';
} else {
// Разворачиваем массив для отображения новых сверху
const reversed = [...arr].reverse();
DOM.history.innerHTML = reversed
.map((b, i) => {
const originalIndex = arr.length - 1 - i;
// Форматируем: индекс, бинарное представление, десятичное значение
const binary = (b & 0xFF).toString(2).padStart(8, "0");
return `[${originalIndex}] ${binary} = ${b & 0xFF}`;
})

View File

@@ -1,4 +1,4 @@
// audio.ts - Звуковой движок для сигнала
// web/src/audio.ts - Звуковой движок для сигнала
export class AudioEngine {
private audioContext: AudioContext | null = null;
@@ -8,20 +8,73 @@ export class AudioEngine {
private currentLevel: number = 0;
private currentFrequency: number = 800;
private fadeTimeout: number | null = null;
private readonly FADE_TIME: number = 0.05; // 50 мс для плавного старта/остановки
private readonly FADE_TIME: number = 0.05;
private readonly FREQUENCY_MAP: Record<number, number> = {
1: 800, // Уровень 1 → 800 Гц
1: 600, // Уровень 1 → 600 Гц
2: 1000, // Уровень 2 → 1000 Гц
3: 1200 // Уровень 3 → 1200 Гц
};
// ===== НОВОЕ: громкость (0-100) =====
private volume: number = 0;
private maxVolume: number = 0.45; // 45% максимум
constructor() {
// Инициализация при первом использовании
}
/**
* Инициализация AudioContext (требует взаимодействия пользователя)
* Установка громкости (0-100)
*/
public setVolume(volume: number): void {
this.volume = Math.max(0, Math.min(100, volume));
this.updateVolume();
}
/**
* Получение текущей громкости (0-100)
*/
public getVolume(): number {
return this.volume;
}
/**
* Применение громкости к усилителю
*/
private updateVolume(): void {
if (!this.gainNode) return;
const normalized = (this.volume / 100) * this.maxVolume;
const currentLevel = this.currentLevel;
// Если уровень 0 или громкость 0 - выключаем звук
if (currentLevel === 0 || this.volume === 0) {
this.gainNode.gain.value = 0;
return;
}
// Рассчитываем громкость на основе уровня и общей громкости
const volumeMap: Record<number, number> = {
1: 0.10 * (normalized / this.maxVolume),
2: 0.25 * (normalized / this.maxVolume),
3: 0.45 * (normalized / this.maxVolume)
};
const targetVolume = Math.min(volumeMap[currentLevel] || 0, this.maxVolume * (this.volume / 100));
try {
const currentTime = this.audioContext?.currentTime || 0;
this.gainNode.gain.exponentialRampToValueAtTime(
Math.max(targetVolume, 0.001),
currentTime + this.FADE_TIME
);
} catch (error) {
this.gainNode.gain.value = targetVolume;
}
}
// ===== ОСТАЛЬНЫЕ МЕТОДЫ =====
private async initAudio(): Promise<boolean> {
try {
if (!this.audioContext) {
@@ -57,7 +110,6 @@ export class AudioEngine {
if (!this.oscillator) return;
try {
// Плавное изменение частоты за 50 мс
const currentTime = this.audioContext?.currentTime || 0;
this.oscillator.frequency.exponentialRampToValueAtTime(
frequency,
@@ -65,7 +117,6 @@ export class AudioEngine {
);
this.currentFrequency = frequency;
} catch (error) {
// Если не удалось плавно, устанавливаем сразу
this.oscillator.frequency.value = frequency;
this.currentFrequency = frequency;
}
@@ -77,29 +128,26 @@ export class AudioEngine {
private createSoundGraph(level: number): void {
if (!this.audioContext) return;
// Останавливаем старый звук
this.stopSound();
const frequency = this.getFrequencyForLevel(level);
this.currentFrequency = frequency;
// Создаём осциллятор
this.oscillator = this.audioContext.createOscillator();
this.oscillator.type = 'sine';
this.oscillator.frequency.value = frequency;
// Создаём усилитель
this.gainNode = this.audioContext.createGain();
this.gainNode.gain.value = 0; // Начинаем с тишины
this.gainNode.gain.value = 0;
// Подключаем: осциллятор → усилитель → выход
this.oscillator.connect(this.gainNode);
this.gainNode.connect(this.audioContext.destination);
// Запускаем осциллятор
this.oscillator.start();
this.isPlaying = true;
// Применяем текущую громкость
this.updateVolume();
}
/**
@@ -107,18 +155,14 @@ export class AudioEngine {
* @param level - уровень сигнала (0-3)
*/
public async updateLevel(level: number): Promise<void> {
// Инициализация при первом вызове
if (!this.audioContext) {
const initialized = await this.initAudio();
if (!initialized) {
return;
}
if (!initialized) return;
}
const context = this.audioContext;
if (!context) return;
// Если AudioContext не готов, пробуем восстановить
if (context.state !== 'running') {
try {
await context.resume();
@@ -128,80 +172,42 @@ export class AudioEngine {
}
}
// Приводим уровень к числу
const newLevel = Math.max(0, Math.min(3, Math.round(level)));
// Если уровень не изменился — ничего не делаем
if (newLevel === this.currentLevel) {
return;
}
this.currentLevel = newLevel;
// Получаем частоту для нового уровня
const newFrequency = this.getFrequencyForLevel(newLevel);
// Если уровень > 0 и звук не создан — создаём
if (newLevel > 0 && !this.oscillator) {
this.createSoundGraph(newLevel);
// Устанавливаем громкость после создания
this.setVolume(newLevel);
// Если громкость 0 - ничего не включаем
if (this.volume === 0) {
this.stopSound();
return;
}
const newFrequency = this.getFrequencyForLevel(newLevel);
if (newLevel > 0 && !this.oscillator) {
this.createSoundGraph(newLevel);
return;
}
// Если уровень 0 — выключаем звук
if (newLevel === 0) {
this.fadeOut();
return;
}
// Если звук не играет — включаем заново
if (!this.isPlaying || !this.oscillator) {
this.createSoundGraph(newLevel);
this.setVolume(newLevel);
return;
}
// Изменяем частоту, если она изменилась
if (newFrequency !== this.currentFrequency) {
this.updateFrequency(newFrequency);
}
// Обновляем громкость
this.setVolume(newLevel);
}
/**
* Установка громкости для уровня
*/
private setVolume(level: number): void {
if (!this.gainNode || !this.audioContext) return;
// Расчёт громкости: уровень 1 = 10%, 2 = 25%, 3 = 45%
const volumeMap: Record<number, number> = {
1: 0.10,
2: 0.25,
3: 0.45
};
const targetVolume = volumeMap[level] || 0;
try {
const currentGain = this.gainNode.gain.value;
// Если звук выключен или очень тихий, включаем сразу
if (currentGain < 0.01) {
this.gainNode.gain.setValueAtTime(targetVolume, this.audioContext.currentTime);
} else {
// Плавное изменение за 50 мс
this.gainNode.gain.exponentialRampToValueAtTime(
Math.max(targetVolume, 0.001),
this.audioContext.currentTime + this.FADE_TIME
);
}
} catch (error) {
// fallback
this.gainNode.gain.value = targetVolume;
}
this.updateVolume();
}
/**
@@ -210,22 +216,18 @@ export class AudioEngine {
private fadeOut(): void {
if (!this.gainNode || !this.audioContext) return;
// Отменяем старый таймер
if (this.fadeTimeout) {
clearTimeout(this.fadeTimeout);
this.fadeTimeout = null;
}
try {
// Плавно убавляем громкость за 100 мс
const currentTime = this.audioContext.currentTime;
this.gainNode.gain.exponentialRampToValueAtTime(0.001, currentTime + this.FADE_TIME * 2);
} catch (error) {
// fallback
this.gainNode.gain.value = 0;
}
// Останавливаем звук после затухания
this.fadeTimeout = window.setTimeout(() => {
this.stopSound();
this.fadeTimeout = null;
@@ -240,18 +242,14 @@ export class AudioEngine {
try {
this.oscillator.stop();
this.oscillator.disconnect();
} catch (e) {
// Игнорируем ошибки при остановке
}
} catch (e) {}
this.oscillator = null;
}
if (this.gainNode) {
try {
this.gainNode.disconnect();
} catch (e) {
// Игнорируем ошибки
}
} catch (e) {}
this.gainNode = null;
}
@@ -270,15 +268,13 @@ export class AudioEngine {
/**
* Проверка, активен ли звук
*/
// noinspection JSUnusedGlobalSymbols
public isActive(): boolean {
return this.isPlaying && this.currentLevel > 0;
return this.isPlaying && this.currentLevel > 0 && this.volume > 0;
}
/**
* Получение текущей частоты
*/
// noinspection JSUnusedGlobalSymbols
public getCurrentFrequency(): number {
return this.currentFrequency;
}
@@ -286,7 +282,6 @@ export class AudioEngine {
/**
* Получение текущего уровня
*/
// noinspection JSUnusedGlobalSymbols
public getCurrentLevel(): number {
return this.currentLevel;
}
@@ -294,23 +289,19 @@ export class AudioEngine {
/**
* Полная остановка и освобождение ресурсов
*/
// noinspection JSUnusedGlobalSymbols
public async dispose(): Promise<void> {
this.stopSound();
if (this.audioContext) {
try {
await this.audioContext.close();
} catch (e) {
// Игнорируем
}
} catch (e) {}
this.audioContext = null;
}
this.currentLevel = 0;
this.currentFrequency = 800;
this.volume = 0;
console.log('[Audio] Ресурсы освобождены');
}
}
// noinspection JSUnusedGlobalSymbols
export const audioEngine = new AudioEngine();

View File

@@ -134,16 +134,24 @@ export function setAudioIndicator(active: boolean, level: number = 0): void {
const el = DOM.audioIndicator;
if (!el) return;
if (active && level > 0) {
el.textContent = `🔊 ${level}`;
// Проверяем громкость
const volume = localStorage.getItem('gport_volume');
const currentVolume = volume ? parseInt(volume) : 0;
if (active && level > 0 && currentVolume > 0) {
el.textContent = '🔊';
el.style.color = '#00ff88';
el.style.opacity = '1';
el.style.animation = 'pulse 1s ease-in-out infinite';
el.classList.remove('muted');
el.classList.add('active');
} else {
el.textContent = '🔇';
el.style.color = '#666';
el.style.opacity = '0.5';
el.style.animation = 'none';
el.classList.add('muted');
el.classList.remove('active');
}
}