Отладка звука
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* Стили аудио-индикатора */
|
||||
/* web/css/audio.css */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
@@ -10,6 +10,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== АУДИО ИНДИКАТОР ===== */
|
||||
#audio-indicator {
|
||||
font-family: var(--font-tech);
|
||||
font-size: var(--font-size-lg);
|
||||
@@ -17,14 +18,231 @@
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid #333;
|
||||
min-width: 50px;
|
||||
min-width: 60px; /* Фиксированная минимальная ширина */
|
||||
width: 60px; /* Фиксированная ширина */
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#audio-indicator.active {
|
||||
border-color: #00ff88;
|
||||
box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
|
||||
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;
|
||||
}
|
||||
@@ -25,11 +25,25 @@
|
||||
<h1>G-Port Warden</h1>
|
||||
<div class="header-controls">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Остальной код без изменений -->
|
||||
<div class="layout">
|
||||
<!-- LEFT -->
|
||||
<div class="panel left" id="leftPanel">
|
||||
|
||||
@@ -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}`;
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user