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

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 @@
/* Стили аудио-индикатора */ /* web/css/audio.css */
@keyframes pulse { @keyframes pulse {
0%, 100% { 0%, 100% {
opacity: 1; opacity: 1;
@@ -10,6 +10,7 @@
} }
} }
/* ===== АУДИО ИНДИКАТОР ===== */
#audio-indicator { #audio-indicator {
font-family: var(--font-tech); font-family: var(--font-tech);
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
@@ -17,14 +18,231 @@
border-radius: 6px; border-radius: 6px;
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
border: 1px solid #333; border: 1px solid #333;
min-width: 50px; min-width: 60px; /* Фиксированная минимальная ширина */
width: 60px; /* Фиксированная ширина */
text-align: center; text-align: center;
user-select: none; user-select: none;
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
} }
#audio-indicator.active { #audio-indicator.active {
border-color: #00ff88; border-color: #00ff88;
box-shadow: 0 0 15px rgba(0, 255, 136, 0.3); box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
animation: pulse 1s ease-in-out infinite; animation: pulse 1s ease-in-out infinite;
}
#audio-indicator.muted {
color: #666 !important;
border-color: #333;
animation: none !important;
box-shadow: none !important;
}
/* ===== ОБЕРТКА ДЛЯ АУДИО ===== */
.audio-wrapper {
position: relative;
display: inline-flex;
align-items: center;
}
/* ===== РЕГУЛЯТОР ГРОМКОСТИ ===== */
.volume-control {
position: absolute;
top: calc(100% + 2px);
left: 50%;
transform: translateX(-50%);
background: rgba(10, 20, 15, 0.95);
border: 1px solid rgba(0, 255, 136, 0.25);
border-radius: 8px;
padding: 10px 16px;
display: none;
align-items: center;
backdrop-filter: blur(10px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
z-index: 1000;
animation: volumeFadeIn 0.15s ease;
min-width: 100px;
}
.audio-wrapper:hover .volume-control {
display: flex;
}
.volume-control:hover {
display: flex !important;
}
@keyframes volumeFadeIn {
from {
opacity: 0;
transform: translateX(-50%) translateY(-4px) scale(0.96);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
}
/* ===== ОБЕРТКА ДЛЯ ПОЛЗУНКА ===== */
.volume-slider-wrapper {
position: relative;
width: 80px;
height: 20px;
display: flex;
align-items: center;
}
/* ===== ТРЕК (фон) ===== */
.volume-track {
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
height: 4px;
background: rgba(255, 255, 255, 0.12);
border-radius: 2px;
pointer-events: none;
overflow: hidden;
}
/* ===== ЗАЛИВКА ТРЕКА ===== */
.volume-track-fill {
height: 100%;
width: 0%;
background: #00ff88;
border-radius: 2px;
transition: width 0.05s linear;
box-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
}
/* ===== ПОЛЗУНОК ===== */
.volume-slider {
-webkit-appearance: none;
appearance: none;
position: relative;
width: 100%;
height: 20px;
background: transparent;
outline: none;
cursor: pointer;
z-index: 2;
margin: 0;
padding: 0;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #00ff88;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 16px rgba(0, 255, 136, 0.5);
transition: all 0.15s ease;
border: 2px solid rgba(0, 255, 136, 0.3);
margin-top: -5px;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.15);
box-shadow: 0 0 24px rgba(0, 255, 136, 0.7);
border-color: #00ff88;
}
.volume-slider::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
background: transparent;
border: none;
border-radius: 2px;
}
.volume-slider::-moz-range-thumb {
width: 14px;
height: 14px;
background: #00ff88;
border: 2px solid rgba(0, 255, 136, 0.3);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 0 16px rgba(0, 255, 136, 0.5);
}
.volume-slider::-moz-range-thumb:hover {
transform: scale(1.15);
box-shadow: 0 0 24px rgba(0, 255, 136, 0.7);
border-color: #00ff88;
}
.volume-slider::-moz-range-track {
width: 100%;
height: 4px;
background: transparent;
border: none;
border-radius: 2px;
}
/* ===== МОБИЛЬНАЯ ВЕРСИЯ ===== */
@media (max-width: 768px) {
.volume-control {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
top: auto;
padding: 12px 20px;
border-radius: 10px;
background: rgba(10, 20, 15, 0.98);
border: 1px solid rgba(0, 255, 136, 0.3);
display: none !important;
min-width: 140px;
}
.audio-wrapper .volume-control.visible {
display: flex !important;
animation: volumeFadeInMobile 0.2s ease;
}
@keyframes volumeFadeInMobile {
from {
opacity: 0;
transform: translateX(-50%) translateY(10px) scale(0.95);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
}
.volume-slider-wrapper {
width: 120px;
}
}
@media (max-width: 480px) {
.volume-control {
padding: 10px 16px;
bottom: 70px;
min-width: 120px;
}
.volume-slider-wrapper {
width: 100px;
}
}
@media (hover: none) {
.audio-wrapper:hover .volume-control {
display: none !important;
}
}
.audio-wrapper.touch-active .volume-control {
display: flex !important;
} }

View File

@@ -25,11 +25,25 @@
<h1>G-Port Warden</h1> <h1>G-Port Warden</h1>
<div class="header-controls"> <div class="header-controls">
<a href="/logs.html" class="header-link">Журнал</a> <a href="/logs.html" class="header-link">Журнал</a>
<div id="audio-indicator" class="audio-indicator">🔇</div> <!-- Аудио индикатор с регулятором -->
<div class="audio-wrapper" id="audioWrapper">
<div id="audio-indicator" class="audio-indicator">🔇</div>
<!-- Регулятор громкости -->
<div class="volume-control" id="volumeControl">
<div class="volume-slider-wrapper">
<input type="range" id="volumeSlider" class="volume-slider"
min="0" max="100" value="0" step="1">
<div class="volume-track">
<div class="volume-track-fill" id="volumeTrackFill"></div>
</div>
</div>
</div>
</div>
<div id="server-time" class="header-time">--:--:--</div> <div id="server-time" class="header-time">--:--:--</div>
</div> </div>
</div> </div>
<!-- Остальной код без изменений -->
<div class="layout"> <div class="layout">
<!-- LEFT --> <!-- LEFT -->
<div class="panel left" id="leftPanel"> <div class="panel left" id="leftPanel">

View File

@@ -1,4 +1,4 @@
// app.ts - Оркестратор приложения // web/src/app.ts - Оркестратор приложения
import { import {
fetchHealth, fetchHealth,
@@ -38,12 +38,111 @@ const MAX_ERRORS: number = 3;
let pollTimer: number | null = null; let pollTimer: number | null = null;
let lastSignalLevel: number = 0; 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 ================= // ================= INIT =================
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
initDOM(); initDOM();
initAccordion(); initAccordion();
initResize(); initResize();
initCamera(); initCamera();
initVolumeControl();
document.addEventListener('click', () => { document.addEventListener('click', () => {
audioEngine.updateLevel(0); audioEngine.updateLevel(0);
@@ -55,10 +154,32 @@ window.addEventListener("DOMContentLoaded", () => {
setSignal(state.peakSignalLevel); setSignal(state.peakSignalLevel);
const signalLevel = state.peakSignalLevel; const signalLevel = state.peakSignalLevel;
const currentVolume = audioEngine.getVolume();
if (signalLevel !== lastSignalLevel) { if (signalLevel !== lastSignalLevel) {
lastSignalLevel = signalLevel; 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; break;
case 'm': case 'm':
if (lastSignalLevel > 0) { if (lastSignalLevel > 0 && audioEngine.getVolume() > 0) {
audioEngine.updateLevel(0); audioEngine.updateLevel(0);
setAudioIndicator(false); setAudioIndicator(false);
window._mutedLevel = lastSignalLevel; window._mutedLevel = lastSignalLevel;
lastSignalLevel = 0; lastSignalLevel = 0;
} else { } else {
const level = window._mutedLevel || state.peakSignalLevel; const level = window._mutedLevel || state.peakSignalLevel;
if (level > 0) { if (level > 0 && audioEngine.getVolume() > 0) {
audioEngine.updateLevel(level); audioEngine.updateLevel(level);
lastSignalLevel = level; lastSignalLevel = level;
setAudioIndicator(true, level); setAudioIndicator(true, level);
@@ -239,12 +360,10 @@ async function update(): Promise<void> {
if (arr.length === 0) { if (arr.length === 0) {
DOM.history.innerHTML = 'Нет данных'; DOM.history.innerHTML = 'Нет данных';
} else { } else {
// Разворачиваем массив для отображения новых сверху
const reversed = [...arr].reverse(); const reversed = [...arr].reverse();
DOM.history.innerHTML = reversed DOM.history.innerHTML = reversed
.map((b, i) => { .map((b, i) => {
const originalIndex = arr.length - 1 - i; const originalIndex = arr.length - 1 - i;
// Форматируем: индекс, бинарное представление, десятичное значение
const binary = (b & 0xFF).toString(2).padStart(8, "0"); const binary = (b & 0xFF).toString(2).padStart(8, "0");
return `[${originalIndex}] ${binary} = ${b & 0xFF}`; return `[${originalIndex}] ${binary} = ${b & 0xFF}`;
}) })

View File

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

View File

@@ -134,16 +134,24 @@ export function setAudioIndicator(active: boolean, level: number = 0): void {
const el = DOM.audioIndicator; const el = DOM.audioIndicator;
if (!el) return; 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.color = '#00ff88';
el.style.opacity = '1'; el.style.opacity = '1';
el.style.animation = 'pulse 1s ease-in-out infinite'; el.style.animation = 'pulse 1s ease-in-out infinite';
el.classList.remove('muted');
el.classList.add('active');
} else { } else {
el.textContent = '🔇'; el.textContent = '🔇';
el.style.color = '#666'; el.style.color = '#666';
el.style.opacity = '0.5'; el.style.opacity = '0.5';
el.style.animation = 'none'; el.style.animation = 'none';
el.classList.add('muted');
el.classList.remove('active');
} }
} }