// 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 currentFrequency: number = 800; private fadeTimeout: number | null = null; private readonly FADE_TIME: number = 0.05; // 50 мс для плавного старта/остановки private readonly FREQUENCY_MAP: Record = { 1: 800, // Уровень 1 → 800 Гц 2: 1000, // Уровень 2 → 1000 Гц 3: 1200 // Уровень 3 → 1200 Гц }; constructor() { // Инициализация при первом использовании } /** * Инициализация AudioContext (требует взаимодействия пользователя) */ private async initAudio(): Promise { try { if (!this.audioContext) { const AudioContextClass = (window as any).AudioContext || (window as any).webkitAudioContext; this.audioContext = new AudioContextClass(); } const context = this.audioContext; if (!context) return false; if (context.state === 'suspended') { await context.resume(); } return context.state === 'running'; } catch (error) { console.warn('[Audio] Ошибка инициализации:', error); return false; } } /** * Получение частоты для уровня */ private getFrequencyForLevel(level: number): number { return this.FREQUENCY_MAP[level] || 800; } /** * Обновление частоты осциллятора */ private updateFrequency(frequency: number): void { if (!this.oscillator) return; try { // Плавное изменение частоты за 50 мс const currentTime = this.audioContext?.currentTime || 0; this.oscillator.frequency.exponentialRampToValueAtTime( frequency, currentTime + this.FADE_TIME ); this.currentFrequency = frequency; console.log(`[Audio] Частота: ${frequency} Гц`); } catch (error) { // Если не удалось плавно, устанавливаем сразу this.oscillator.frequency.value = frequency; this.currentFrequency = frequency; } } /** * Создание звукового графа */ 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.oscillator.connect(this.gainNode); this.gainNode.connect(this.audioContext.destination); // Запускаем осциллятор this.oscillator.start(); this.isPlaying = true; console.log(`[Audio] Звук создан, частота: ${frequency} Гц, уровень: ${level}`); } /** * Обновление уровня сигнала * @param level - уровень сигнала (0-3) */ public async updateLevel(level: number): Promise { // Инициализация при первом вызове if (!this.audioContext) { const initialized = await this.initAudio(); if (!initialized) { return; } } const context = this.audioContext; if (!context) return; // Если AudioContext не готов, пробуем восстановить if (context.state !== 'running') { try { await context.resume(); } catch (error) { console.warn('[Audio] Ошибка возобновления контекста:', error); return; } } // Приводим уровень к числу const newLevel = Math.max(0, Math.min(3, Math.round(level))); // Если уровень не изменился — ничего не делаем if (newLevel === this.currentLevel) { return; } const oldLevel = this.currentLevel; this.currentLevel = newLevel; // Получаем частоту для нового уровня const newFrequency = this.getFrequencyForLevel(newLevel); // Если уровень > 0 и звук не создан — создаём if (newLevel > 0 && !this.oscillator) { this.createSoundGraph(newLevel); // Устанавливаем громкость после создания this.setVolume(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); console.log(`[Audio] Уровень: ${oldLevel} → ${newLevel}, Частота: ${newFrequency} Гц`); } /** * Установка громкости для уровня */ private setVolume(level: number): void { if (!this.gainNode || !this.audioContext) return; // Расчёт громкости: уровень 1 = 10%, 2 = 25%, 3 = 45% const volumeMap: Record = { 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; } } /** * Плавное затухание и остановка */ 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; }, 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] Звук остановлен'); } // ============================================ // ПУБЛИЧНЫЕ МЕТОДЫ // ============================================ /** * Проверка, активен ли звук */ // noinspection JSUnusedGlobalSymbols public isActive(): boolean { return this.isPlaying && this.currentLevel > 0; } /** * Получение текущей частоты */ // noinspection JSUnusedGlobalSymbols public getCurrentFrequency(): number { return this.currentFrequency; } /** * Получение текущего уровня */ // noinspection JSUnusedGlobalSymbols public getCurrentLevel(): number { return this.currentLevel; } /** * Полная остановка и освобождение ресурсов */ // noinspection JSUnusedGlobalSymbols public async dispose(): Promise { this.stopSound(); if (this.audioContext) { try { await this.audioContext.close(); } catch (e) { // Игнорируем } this.audioContext = null; } this.currentLevel = 0; this.currentFrequency = 800; console.log('[Audio] Ресурсы освобождены'); } } // noinspection JSUnusedGlobalSymbols export const audioEngine = new AudioEngine();