Отладка звука
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}`;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user