Files
go-service/cmd/server/web/js/audio.ts

221 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// audio.ts - Звуковой движок для сигнала
export class AudioEngine {
private audioContext: AudioContext | null = null;
private oscillator: OscillatorNode | null = null;
private gainNode: GainNode | null = null;
private isPlaying: boolean = false;
private currentLevel: number = 0;
private fadeTimeout: number | null = null;
private readonly FREQUENCY: number = 800; // 800 Гц
private readonly FADE_TIME: number = 0.05; // 50 мс для плавного старта/остановки
constructor() {
// Инициализация при первом использовании
}
/**
* Инициализация AudioContext (требует взаимодействия пользователя)
*/
private initAudio(): boolean {
try {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
}
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
return this.audioContext.state === 'running';
} catch (error) {
console.warn('[Audio] Ошибка инициализации:', error);
return false;
}
}
/**
* Создание звукового графа
*/
private createSoundGraph(): void {
if (!this.audioContext) return;
// Останавливаем старый звук
this.stopSound();
// Создаём осциллятор
this.oscillator = this.audioContext.createOscillator();
this.oscillator.type = 'sine';
this.oscillator.frequency.value = this.FREQUENCY;
// Создаём усилитель
this.gainNode = this.audioContext.createGain();
this.gainNode.gain.value = 0; // Начинаем с тишины
// Подключаем: осциллятор → усилитель → выход
this.oscillator.connect(this.gainNode);
this.gainNode.connect(this.audioContext.destination);
// Запускаем осциллятор
this.oscillator.start();
this.isPlaying = true;
console.log('[Audio] Звук создан, частота:', this.FREQUENCY, 'Гц');
}
/**
* Обновление уровня сигнала
* @param level - уровень сигнала (0-3)
*/
public updateLevel(level: number): void {
// Инициализация при первом вызове
if (!this.audioContext) {
if (!this.initAudio()) {
return;
}
}
// Если AudioContext не готов, пробуем восстановить
if (this.audioContext?.state !== 'running') {
this.audioContext?.resume();
return;
}
// Приводим уровень к числу
const newLevel = Math.max(0, Math.min(3, Math.round(level)));
// Если уровень не изменился — ничего не делаем
if (newLevel === this.currentLevel) {
return;
}
this.currentLevel = newLevel;
// Если уровень > 0 и звук не создан — создаём
if (newLevel > 0 && !this.oscillator) {
this.createSoundGraph();
}
// Если уровень 0 — выключаем звук
if (newLevel === 0) {
this.fadeOut();
return;
}
// Если звук не играет — включаем
if (!this.isPlaying || !this.oscillator) {
this.createSoundGraph();
}
// Расчёт громкости: уровень 1 = 15%, 2 = 30%, 3 = 50%
const volumeMap: Record<number, number> = {
1: 0.10,
2: 0.25,
3: 0.45
};
const targetVolume = volumeMap[newLevel] || 0;
// Плавно меняем громкость
if (this.gainNode) {
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
);
}
}
console.log(`[Audio] Уровень: ${newLevel}, Громкость: ${(targetVolume * 100).toFixed(0)}%`);
}
/**
* Плавное затухание и остановка
*/
private fadeOut(): void {
if (!this.gainNode || !this.audioContext) return;
// Отменяем старый таймер
if (this.fadeTimeout) {
clearTimeout(this.fadeTimeout);
this.fadeTimeout = null;
}
// Плавно убавляем громкость за 100 мс
const currentTime = this.audioContext.currentTime;
this.gainNode.gain.exponentialRampToValueAtTime(0.001, currentTime + this.FADE_TIME * 2);
// Останавливаем звук после затухания
this.fadeTimeout = window.setTimeout(() => {
this.stopSound();
this.fadeTimeout = null;
}, this.FADE_TIME * 2 * 1000 + 50);
console.log('[Audio] Затухание...');
}
/**
* Немедленная остановка звука
*/
private stopSound(): void {
if (this.oscillator) {
try {
this.oscillator.stop();
this.oscillator.disconnect();
} catch (e) {
// Игнорируем ошибки при остановке
}
this.oscillator = null;
}
if (this.gainNode) {
try {
this.gainNode.disconnect();
} catch (e) {
// Игнорируем ошибки
}
this.gainNode = null;
}
this.isPlaying = false;
if (this.fadeTimeout) {
clearTimeout(this.fadeTimeout);
this.fadeTimeout = null;
}
console.log('[Audio] Звук остановлен');
}
/**
* Проверка, активен ли звук
*/
public isActive(): boolean {
return this.isPlaying && this.currentLevel > 0;
}
/**
* Полная остановка и освобождение ресурсов
*/
public dispose(): void {
this.stopSound();
if (this.audioContext) {
try {
this.audioContext.close();
} catch (e) {
// Игнорируем
}
this.audioContext = null;
}
this.currentLevel = 0;
console.log('[Audio] Ресурсы освобождены');
}
}
// Экспортируем одиночный экземпляр
export const audioEngine = new AudioEngine();