mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 01:50:47 -04:00
brush ducking only ig
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CxgQu649.js"></script>
|
<script type="module" crossorigin src="/assets/index-Betw1gOE.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Bz1ixYh7.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bz1ixYh7.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -38,11 +38,15 @@ export default function SettingsPanel() {
|
|||||||
const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume;
|
const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume;
|
||||||
const alertVolume = Number.isFinite(audioSettings?.alertVolume) ? audioSettings.alertVolume : AUDIO_SETTINGS_DEFAULTS.alertVolume;
|
const alertVolume = Number.isFinite(audioSettings?.alertVolume) ? audioSettings.alertVolume : AUDIO_SETTINGS_DEFAULTS.alertVolume;
|
||||||
const roverVolume = Number.isFinite(audioSettings?.roverVolume) ? audioSettings.roverVolume : AUDIO_SETTINGS_DEFAULTS.roverVolume;
|
const roverVolume = Number.isFinite(audioSettings?.roverVolume) ? audioSettings.roverVolume : AUDIO_SETTINGS_DEFAULTS.roverVolume;
|
||||||
const autoLevelEnabled =
|
const mainBrushDuckEnabled =
|
||||||
typeof audioSettings?.autoLevelEnabled === 'boolean'
|
typeof audioSettings?.mainBrushDuckEnabled === 'boolean'
|
||||||
|
? audioSettings.mainBrushDuckEnabled
|
||||||
|
: typeof audioSettings?.autoLevelEnabled === 'boolean'
|
||||||
? audioSettings.autoLevelEnabled
|
? audioSettings.autoLevelEnabled
|
||||||
: AUDIO_SETTINGS_DEFAULTS.autoLevelEnabled;
|
: AUDIO_SETTINGS_DEFAULTS.mainBrushDuckEnabled;
|
||||||
const autoLevelMode = audioSettings?.autoLevelMode === 'duck' ? 'duck' : 'compressor';
|
const mainBrushDuckAmount = Number.isFinite(audioSettings?.mainBrushDuckAmount)
|
||||||
|
? Math.max(0, Math.min(1, audioSettings.mainBrushDuckAmount))
|
||||||
|
: AUDIO_SETTINGS_DEFAULTS.mainBrushDuckAmount;
|
||||||
|
|
||||||
const sensorButtons = useMemo(
|
const sensorButtons = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -72,14 +76,9 @@ export default function SettingsPanel() {
|
|||||||
saveAudioSettings((current) => ({ ...(current ?? {}), [key]: next }));
|
saveAudioSettings((current) => ({ ...(current ?? {}), [key]: next }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAutoLevelEnabled = (event) => {
|
const handleMainBrushDuckEnabled = (event) => {
|
||||||
const checked = Boolean(event.target.checked);
|
const checked = Boolean(event.target.checked);
|
||||||
saveAudioSettings((current) => ({ ...(current ?? {}), autoLevelEnabled: checked }));
|
saveAudioSettings((current) => ({ ...(current ?? {}), mainBrushDuckEnabled: checked }));
|
||||||
};
|
|
||||||
|
|
||||||
const handleAutoLevelMode = (event) => {
|
|
||||||
const next = event.target.value === 'duck' ? 'duck' : 'compressor';
|
|
||||||
saveAudioSettings((current) => ({ ...(current ?? {}), autoLevelMode: next }));
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Tabs defaultTab="keybindings">
|
<Tabs defaultTab="keybindings">
|
||||||
@@ -163,28 +162,32 @@ export default function SettingsPanel() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center justify-between gap-0.5 text-slate-200">
|
<label className="flex items-center justify-between gap-0.5 text-slate-200">
|
||||||
<span>Auto-level rover audio</span>
|
<span>Main brush ducking</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="accent-emerald-500"
|
className="accent-emerald-500"
|
||||||
checked={autoLevelEnabled}
|
checked={mainBrushDuckEnabled}
|
||||||
onChange={handleAutoLevelEnabled}
|
onChange={handleMainBrushDuckEnabled}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center justify-between gap-0.5 text-slate-200">
|
<label className="grid gap-0.5 text-slate-200">
|
||||||
<span>Auto-level mode</span>
|
<div className="flex items-center justify-between gap-0.5">
|
||||||
<select
|
<span>Main brush duck amount</span>
|
||||||
value={autoLevelMode}
|
<span className="text-xs text-slate-400">{Math.round(mainBrushDuckAmount * 100)}%</span>
|
||||||
onChange={handleAutoLevelMode}
|
</div>
|
||||||
className="field-input text-sm"
|
<input
|
||||||
disabled={!autoLevelEnabled}
|
type="range"
|
||||||
>
|
min="0"
|
||||||
<option value="compressor">Compressor (preferred)</option>
|
max="1"
|
||||||
<option value="duck">Brush/vacuum ducking</option>
|
step="0.01"
|
||||||
</select>
|
value={mainBrushDuckAmount}
|
||||||
|
onChange={handleAudioRange('mainBrushDuckAmount')}
|
||||||
|
className="w-full accent-emerald-500"
|
||||||
|
disabled={!mainBrushDuckEnabled}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500">
|
||||||
Compressor evens out incoming audio; ducking lowers volume when cleaning motors are active.
|
Lowers rover audio only while the main brush is running.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section className="panel-section space-y-0.5 text-sm">
|
<section className="panel-section space-y-0.5 text-sm">
|
||||||
|
|||||||
@@ -13,17 +13,7 @@ import { buildBatteryVisual } from '../lib/battery.js';
|
|||||||
const RESTART_DELAY_MS = 2000;
|
const RESTART_DELAY_MS = 2000;
|
||||||
const UNMUTE_RETRY_MS = 3000;
|
const UNMUTE_RETRY_MS = 3000;
|
||||||
const AUDIO_RETRY_MS = 3000;
|
const AUDIO_RETRY_MS = 3000;
|
||||||
const AUDIO_DUCK_FACTOR = 0.55;
|
|
||||||
const BRUSH_CURRENT_THRESHOLD_MA = 40;
|
const BRUSH_CURRENT_THRESHOLD_MA = 40;
|
||||||
const COMPRESSOR_REDUCTION_ACTIVE_DB = -0.75;
|
|
||||||
const COMPRESSOR_SETTINGS = {
|
|
||||||
threshold: -30,
|
|
||||||
knee: 10,
|
|
||||||
ratio: 8,
|
|
||||||
attack: 0.002,
|
|
||||||
release: 0.18,
|
|
||||||
};
|
|
||||||
const COMPRESSOR_MAKEUP_GAIN = 2.25;
|
|
||||||
|
|
||||||
export default function VideoTile({
|
export default function VideoTile({
|
||||||
sessionInfo,
|
sessionInfo,
|
||||||
@@ -56,21 +46,10 @@ export default function VideoTile({
|
|||||||
const audioRestartTimer = useRef(null);
|
const audioRestartTimer = useRef(null);
|
||||||
const audioPlayInterval = useRef(null);
|
const audioPlayInterval = useRef(null);
|
||||||
const unmuteTimer = useRef(null);
|
const unmuteTimer = useRef(null);
|
||||||
const audioContextRef = useRef(null);
|
|
||||||
const audioSourceNodeRef = useRef(null);
|
|
||||||
const audioCompressorRef = useRef(null);
|
|
||||||
const audioGainRef = useRef(null);
|
|
||||||
const audioGraphConnectedRef = useRef(false);
|
|
||||||
const reductionPollRef = useRef(null);
|
|
||||||
const graphFailedRef = useRef(false);
|
|
||||||
const audioInteractionSeenRef = useRef(false);
|
|
||||||
const lastResumeAttemptAtRef = useRef(0);
|
|
||||||
const [status, setStatus] = useState('idle');
|
const [status, setStatus] = useState('idle');
|
||||||
const [detail, setDetail] = useState(null);
|
const [detail, setDetail] = useState(null);
|
||||||
const [audioStatus, setAudioStatus] = useState('idle');
|
const [audioStatus, setAudioStatus] = useState('idle');
|
||||||
const [audioDetail, setAudioDetail] = useState(null);
|
const [audioDetail, setAudioDetail] = useState(null);
|
||||||
const [levelIndicator, setLevelIndicator] = useState(null);
|
|
||||||
const [compressorActive, setCompressorActive] = useState(false);
|
|
||||||
const [restartToken, setRestartToken] = useState(0);
|
const [restartToken, setRestartToken] = useState(0);
|
||||||
const [audioRestartToken, setAudioRestartToken] = useState(0);
|
const [audioRestartToken, setAudioRestartToken] = useState(0);
|
||||||
const [muted, setMuted] = useState(true);
|
const [muted, setMuted] = useState(true);
|
||||||
@@ -84,10 +63,15 @@ export default function VideoTile({
|
|||||||
const roverVolume = Number.isFinite(audioSettings?.roverVolume)
|
const roverVolume = Number.isFinite(audioSettings?.roverVolume)
|
||||||
? audioSettings.roverVolume
|
? audioSettings.roverVolume
|
||||||
: AUDIO_SETTINGS_DEFAULTS.roverVolume;
|
: AUDIO_SETTINGS_DEFAULTS.roverVolume;
|
||||||
const autoLevelEnabled = typeof audioSettings?.autoLevelEnabled === 'boolean'
|
const mainBrushDuckEnabled =
|
||||||
? audioSettings.autoLevelEnabled
|
typeof audioSettings?.mainBrushDuckEnabled === 'boolean'
|
||||||
: AUDIO_SETTINGS_DEFAULTS.autoLevelEnabled;
|
? audioSettings.mainBrushDuckEnabled
|
||||||
const autoLevelMode = audioSettings?.autoLevelMode === 'duck' ? 'duck' : 'compressor';
|
: typeof audioSettings?.autoLevelEnabled === 'boolean'
|
||||||
|
? audioSettings.autoLevelEnabled
|
||||||
|
: AUDIO_SETTINGS_DEFAULTS.mainBrushDuckEnabled;
|
||||||
|
const mainBrushDuckAmount = Number.isFinite(audioSettings?.mainBrushDuckAmount)
|
||||||
|
? Math.max(0, Math.min(1, audioSettings.mainBrushDuckAmount))
|
||||||
|
: AUDIO_SETTINGS_DEFAULTS.mainBrushDuckAmount;
|
||||||
const baseRoverGain = Math.max(0, Math.min(1, masterVolume * roverVolume));
|
const baseRoverGain = Math.max(0, Math.min(1, masterVolume * roverVolume));
|
||||||
const batteryCharge = sensors?.batteryChargeMah ?? null;
|
const batteryCharge = sensors?.batteryChargeMah ?? null;
|
||||||
const desktopLayout = layoutFormat === 'desktop';
|
const desktopLayout = layoutFormat === 'desktop';
|
||||||
@@ -125,27 +109,29 @@ export default function VideoTile({
|
|||||||
const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
|
const overlayMotors = overcurrentMotors.length ? overcurrentMotors : limiterActive ? ['limiter'] : [];
|
||||||
const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
|
const overlayFill = limiterFill ?? (overcurrentMotors.length ? 1 : 0);
|
||||||
const overlayVisible = Boolean(overlayMotors.length);
|
const overlayVisible = Boolean(overlayMotors.length);
|
||||||
const brushOrVacuumActive = Boolean(
|
const mainBrushActive = Boolean(
|
||||||
(Number(sensors?.mainBrushCurrentMa) || 0) > BRUSH_CURRENT_THRESHOLD_MA ||
|
(Number(sensors?.mainBrushCurrentMa) || 0) > BRUSH_CURRENT_THRESHOLD_MA ||
|
||||||
(Number(sensors?.sideBrushCurrentMa) || 0) > BRUSH_CURRENT_THRESHOLD_MA ||
|
sensors?.wheelOvercurrents?.mainBrush,
|
||||||
sensors?.wheelOvercurrents?.mainBrush ||
|
|
||||||
sensors?.wheelOvercurrents?.sideBrush,
|
|
||||||
);
|
);
|
||||||
const duckGain = autoLevelEnabled && autoLevelMode === 'duck' && brushOrVacuumActive ? AUDIO_DUCK_FACTOR : 1;
|
const duckGain = mainBrushDuckEnabled && mainBrushActive ? 1 - mainBrushDuckAmount : 1;
|
||||||
const effectiveRoverGain = Math.max(0, Math.min(1, baseRoverGain * duckGain));
|
const effectiveRoverGain = Math.max(0, Math.min(1, baseRoverGain * duckGain));
|
||||||
|
const levelIndicator =
|
||||||
|
mainBrushDuckEnabled && mainBrushActive && mainBrushDuckAmount > 0
|
||||||
|
? `Volume decreased ${Math.round(mainBrushDuckAmount * 1000) / 10}%`
|
||||||
|
: null;
|
||||||
const logAudio = useCallback(
|
const logAudio = useCallback(
|
||||||
(event, meta = {}) => {
|
(event, meta = {}) => {
|
||||||
if (!debugAudio) return;
|
if (!debugAudio) return;
|
||||||
const audioEl = audioRef.current;
|
const audioEl = audioRef.current;
|
||||||
const ctx = audioContextRef.current;
|
|
||||||
const payload = {
|
const payload = {
|
||||||
event,
|
event,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
roverLabel: label || null,
|
roverLabel: label || null,
|
||||||
hasDedicatedAudio,
|
hasDedicatedAudio,
|
||||||
audioUrl: audioSessionInfo?.url || null,
|
audioUrl: audioSessionInfo?.url || null,
|
||||||
autoLevelEnabled,
|
mainBrushDuckEnabled,
|
||||||
autoLevelMode,
|
mainBrushDuckAmount,
|
||||||
|
mainBrushActive,
|
||||||
baseRoverGain,
|
baseRoverGain,
|
||||||
effectiveRoverGain,
|
effectiveRoverGain,
|
||||||
element: audioEl
|
element: audioEl
|
||||||
@@ -158,12 +144,6 @@ export default function VideoTile({
|
|||||||
networkState: audioEl.networkState,
|
networkState: audioEl.networkState,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
context: ctx
|
|
||||||
? {
|
|
||||||
state: ctx.state,
|
|
||||||
sampleRate: ctx.sampleRate,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
...meta,
|
...meta,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -177,8 +157,9 @@ export default function VideoTile({
|
|||||||
label,
|
label,
|
||||||
hasDedicatedAudio,
|
hasDedicatedAudio,
|
||||||
audioSessionInfo?.url,
|
audioSessionInfo?.url,
|
||||||
autoLevelEnabled,
|
mainBrushDuckEnabled,
|
||||||
autoLevelMode,
|
mainBrushDuckAmount,
|
||||||
|
mainBrushActive,
|
||||||
baseRoverGain,
|
baseRoverGain,
|
||||||
effectiveRoverGain,
|
effectiveRoverGain,
|
||||||
],
|
],
|
||||||
@@ -216,103 +197,6 @@ export default function VideoTile({
|
|||||||
clearTimeout(audioRestartTimer.current);
|
clearTimeout(audioRestartTimer.current);
|
||||||
audioRestartTimer.current = setTimeout(() => setAudioRestartToken(Date.now()), RESTART_DELAY_MS);
|
audioRestartTimer.current = setTimeout(() => setAudioRestartToken(Date.now()), RESTART_DELAY_MS);
|
||||||
}, []);
|
}, []);
|
||||||
const ensureAudioGraph = useCallback(() => {
|
|
||||||
if (graphFailedRef.current) return false;
|
|
||||||
const audioEl = audioRef.current;
|
|
||||||
if (!audioEl || typeof window === 'undefined') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
|
|
||||||
if (typeof AudioContextCtor !== 'function') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!audioContextRef.current) {
|
|
||||||
audioContextRef.current = new AudioContextCtor();
|
|
||||||
}
|
|
||||||
const ctx = audioContextRef.current;
|
|
||||||
if (!audioSourceNodeRef.current) {
|
|
||||||
audioSourceNodeRef.current = ctx.createMediaElementSource(audioEl);
|
|
||||||
}
|
|
||||||
if (!audioCompressorRef.current) {
|
|
||||||
const compressor = ctx.createDynamicsCompressor();
|
|
||||||
compressor.threshold.value = COMPRESSOR_SETTINGS.threshold;
|
|
||||||
compressor.knee.value = COMPRESSOR_SETTINGS.knee;
|
|
||||||
compressor.ratio.value = COMPRESSOR_SETTINGS.ratio;
|
|
||||||
compressor.attack.value = COMPRESSOR_SETTINGS.attack;
|
|
||||||
compressor.release.value = COMPRESSOR_SETTINGS.release;
|
|
||||||
audioCompressorRef.current = compressor;
|
|
||||||
logAudio('graph/compressor-created', {
|
|
||||||
threshold: compressor.threshold.value,
|
|
||||||
knee: compressor.knee.value,
|
|
||||||
ratio: compressor.ratio.value,
|
|
||||||
attack: compressor.attack.value,
|
|
||||||
release: compressor.release.value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!audioGainRef.current) {
|
|
||||||
audioGainRef.current = ctx.createGain();
|
|
||||||
}
|
|
||||||
if (!audioGraphConnectedRef.current) {
|
|
||||||
audioSourceNodeRef.current.disconnect();
|
|
||||||
audioCompressorRef.current.disconnect();
|
|
||||||
audioGainRef.current.disconnect();
|
|
||||||
audioSourceNodeRef.current.connect(audioCompressorRef.current);
|
|
||||||
audioCompressorRef.current.connect(audioGainRef.current);
|
|
||||||
audioGainRef.current.connect(ctx.destination);
|
|
||||||
audioGraphConnectedRef.current = true;
|
|
||||||
logAudio('graph/connected');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
graphFailedRef.current = true;
|
|
||||||
logAudio('graph/failed');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [logAudio]);
|
|
||||||
|
|
||||||
const resumeAudioContext = useCallback(async () => {
|
|
||||||
const ctx = audioContextRef.current;
|
|
||||||
if (!ctx || ctx.state !== 'suspended') return;
|
|
||||||
if (!audioInteractionSeenRef.current) {
|
|
||||||
logAudio('context/resume-skipped-no-interaction');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - lastResumeAttemptAtRef.current < 800) return;
|
|
||||||
lastResumeAttemptAtRef.current = now;
|
|
||||||
logAudio('context/resume-attempt');
|
|
||||||
await ctx.resume().catch(() => {});
|
|
||||||
if (
|
|
||||||
ctx.state === 'running' &&
|
|
||||||
autoLevelEnabled &&
|
|
||||||
autoLevelMode === 'compressor' &&
|
|
||||||
audioGainRef.current
|
|
||||||
) {
|
|
||||||
const graphGain = Math.max(0, Math.min(4, effectiveRoverGain * COMPRESSOR_MAKEUP_GAIN));
|
|
||||||
audioGainRef.current.gain.value = graphGain;
|
|
||||||
logAudio('context/gain-restored', { gain: graphGain, baseGain: effectiveRoverGain });
|
|
||||||
}
|
|
||||||
logAudio('context/resume-result');
|
|
||||||
}, [logAudio, autoLevelEnabled, autoLevelMode, effectiveRoverGain]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!audioSessionInfo?.url) return undefined;
|
|
||||||
const markInteraction = () => {
|
|
||||||
if (audioInteractionSeenRef.current) return;
|
|
||||||
audioInteractionSeenRef.current = true;
|
|
||||||
logAudio('interaction/first-user-gesture');
|
|
||||||
resumeAudioContext();
|
|
||||||
};
|
|
||||||
window.addEventListener('pointerdown', markInteraction, { passive: true, capture: true });
|
|
||||||
window.addEventListener('keydown', markInteraction, { capture: true });
|
|
||||||
window.addEventListener('touchstart', markInteraction, { passive: true, capture: true });
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('pointerdown', markInteraction);
|
|
||||||
window.removeEventListener('keydown', markInteraction);
|
|
||||||
window.removeEventListener('touchstart', markInteraction);
|
|
||||||
};
|
|
||||||
}, [audioSessionInfo?.url, logAudio, resumeAudioContext]);
|
|
||||||
|
|
||||||
const ensurePlayback = useCallback(async () => {
|
const ensurePlayback = useCallback(async () => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
@@ -365,14 +249,6 @@ export default function VideoTile({
|
|||||||
clearTimeout(audioRestartTimer.current);
|
clearTimeout(audioRestartTimer.current);
|
||||||
clearTimeout(unmuteTimer.current);
|
clearTimeout(unmuteTimer.current);
|
||||||
clearInterval(audioPlayInterval.current);
|
clearInterval(audioPlayInterval.current);
|
||||||
clearInterval(reductionPollRef.current);
|
|
||||||
audioGraphConnectedRef.current = false;
|
|
||||||
audioSourceNodeRef.current?.disconnect?.();
|
|
||||||
audioCompressorRef.current?.disconnect?.();
|
|
||||||
audioGainRef.current?.disconnect?.();
|
|
||||||
if (audioContextRef.current?.state && audioContextRef.current.state !== 'closed') {
|
|
||||||
audioContextRef.current.close().catch(() => {});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
@@ -440,90 +316,23 @@ export default function VideoTile({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audioEl = audioRef.current;
|
const audioEl = audioRef.current;
|
||||||
clearInterval(reductionPollRef.current);
|
|
||||||
reductionPollRef.current = null;
|
|
||||||
if (!audioEl || !audioSessionInfo?.url) {
|
if (!audioEl || !audioSessionInfo?.url) {
|
||||||
setCompressorActive(false);
|
|
||||||
setLevelIndicator(null);
|
|
||||||
logAudio('route/no-audio-url');
|
logAudio('route/no-audio-url');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const compressorMode = autoLevelEnabled && autoLevelMode === 'compressor';
|
|
||||||
const duckMode = autoLevelEnabled && autoLevelMode === 'duck';
|
|
||||||
const graphReady = compressorMode ? ensureAudioGraph() : false;
|
|
||||||
|
|
||||||
if (graphReady) {
|
|
||||||
const compressor = audioCompressorRef.current;
|
|
||||||
const gainNode = audioGainRef.current;
|
|
||||||
const contextRunning = audioContextRef.current?.state === 'running';
|
|
||||||
const canActivate = Boolean(contextRunning && compressorActive);
|
|
||||||
// Strict gating: graph-only output only after context is running and compressor is explicitly activated.
|
|
||||||
audioEl.volume = canActivate ? 0 : effectiveRoverGain;
|
|
||||||
logAudio('route/graph', { contextRunning, canActivate, elementVolume: audioEl.volume });
|
|
||||||
if (gainNode) {
|
|
||||||
const graphGain = canActivate
|
|
||||||
? Math.max(0, Math.min(4, effectiveRoverGain * COMPRESSOR_MAKEUP_GAIN))
|
|
||||||
: 0;
|
|
||||||
gainNode.gain.value = graphGain;
|
|
||||||
logAudio('route/graph-gain', { graphGain, baseGain: effectiveRoverGain });
|
|
||||||
}
|
|
||||||
if (compressor) {
|
|
||||||
compressor.threshold.value = COMPRESSOR_SETTINGS.threshold;
|
|
||||||
compressor.knee.value = COMPRESSOR_SETTINGS.knee;
|
|
||||||
compressor.ratio.value = COMPRESSOR_SETTINGS.ratio;
|
|
||||||
compressor.attack.value = COMPRESSOR_SETTINGS.attack;
|
|
||||||
compressor.release.value = COMPRESSOR_SETTINGS.release;
|
|
||||||
}
|
|
||||||
if (canActivate) {
|
|
||||||
reductionPollRef.current = setInterval(() => {
|
|
||||||
if (audioContextRef.current?.state !== 'running') {
|
|
||||||
setLevelIndicator(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reduction = compressor?.reduction;
|
|
||||||
if (typeof reduction === 'number' && reduction <= COMPRESSOR_REDUCTION_ACTIVE_DB) {
|
|
||||||
const amount = Math.round(Math.abs(reduction) * 10) / 10;
|
|
||||||
const formatted = Number.isInteger(amount) ? amount.toFixed(0) : amount.toFixed(1);
|
|
||||||
setLevelIndicator(`Level: ${formatted}dB`);
|
|
||||||
logAudio('compressor/reduction', { reduction, formatted });
|
|
||||||
} else {
|
|
||||||
setLevelIndicator(null);
|
|
||||||
}
|
|
||||||
}, 180);
|
|
||||||
} else {
|
|
||||||
setLevelIndicator(null);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCompressorActive(false);
|
|
||||||
audioEl.volume = effectiveRoverGain;
|
audioEl.volume = effectiveRoverGain;
|
||||||
logAudio('route/direct', { elementVolume: audioEl.volume, duckMode, brushOrVacuumActive });
|
logAudio('route/direct', {
|
||||||
if (audioCompressorRef.current) {
|
elementVolume: audioEl.volume,
|
||||||
audioCompressorRef.current.threshold.value = 0;
|
duckEnabled: mainBrushDuckEnabled,
|
||||||
audioCompressorRef.current.knee.value = 0;
|
mainBrushActive,
|
||||||
audioCompressorRef.current.ratio.value = 1;
|
duckAmount: mainBrushDuckAmount,
|
||||||
audioCompressorRef.current.attack.value = 0.003;
|
});
|
||||||
audioCompressorRef.current.release.value = 0.25;
|
|
||||||
}
|
|
||||||
if (audioGainRef.current) {
|
|
||||||
// In direct route, silence graph output to avoid duplicate path.
|
|
||||||
audioGainRef.current.gain.value = 0;
|
|
||||||
}
|
|
||||||
if (duckMode && brushOrVacuumActive) {
|
|
||||||
setLevelIndicator('Level: ducking');
|
|
||||||
} else {
|
|
||||||
setLevelIndicator(null);
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
audioSessionInfo?.url,
|
audioSessionInfo?.url,
|
||||||
autoLevelEnabled,
|
|
||||||
autoLevelMode,
|
|
||||||
brushOrVacuumActive,
|
|
||||||
compressorActive,
|
|
||||||
effectiveRoverGain,
|
effectiveRoverGain,
|
||||||
ensureAudioGraph,
|
mainBrushDuckEnabled,
|
||||||
|
mainBrushActive,
|
||||||
|
mainBrushDuckAmount,
|
||||||
logAudio,
|
logAudio,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -547,9 +356,6 @@ export default function VideoTile({
|
|||||||
}
|
}
|
||||||
return nextStatus;
|
return nextStatus;
|
||||||
});
|
});
|
||||||
if (nextStatus === 'playing') {
|
|
||||||
resumeAudioContext();
|
|
||||||
}
|
|
||||||
if (['error', 'failed'].includes(nextStatus)) {
|
if (['error', 'failed'].includes(nextStatus)) {
|
||||||
scheduleAudioRestart();
|
scheduleAudioRestart();
|
||||||
}
|
}
|
||||||
@@ -579,7 +385,6 @@ export default function VideoTile({
|
|||||||
audioSessionInfo?.token,
|
audioSessionInfo?.token,
|
||||||
audioRestartToken,
|
audioRestartToken,
|
||||||
scheduleAudioRestart,
|
scheduleAudioRestart,
|
||||||
resumeAudioContext,
|
|
||||||
logAudio,
|
logAudio,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -600,16 +405,6 @@ export default function VideoTile({
|
|||||||
const target = audioRef.current;
|
const target = audioRef.current;
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
target.muted = false;
|
target.muted = false;
|
||||||
const wantCompressor = autoLevelEnabled && autoLevelMode === 'compressor';
|
|
||||||
if (wantCompressor) {
|
|
||||||
ensureAudioGraph();
|
|
||||||
await resumeAudioContext();
|
|
||||||
const running = audioContextRef.current?.state === 'running';
|
|
||||||
if (running && !compressorActive) {
|
|
||||||
setCompressorActive(true);
|
|
||||||
logAudio('compressor/activated');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!target.paused && !target.ended) return;
|
if (!target.paused && !target.ended) return;
|
||||||
logAudio('retry/play-attempt');
|
logAudio('retry/play-attempt');
|
||||||
target
|
target
|
||||||
@@ -632,11 +427,6 @@ export default function VideoTile({
|
|||||||
}, [
|
}, [
|
||||||
audioSessionInfo?.url,
|
audioSessionInfo?.url,
|
||||||
audioStatus,
|
audioStatus,
|
||||||
autoLevelEnabled,
|
|
||||||
autoLevelMode,
|
|
||||||
compressorActive,
|
|
||||||
ensureAudioGraph,
|
|
||||||
resumeAudioContext,
|
|
||||||
logAudio,
|
logAudio,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,6 @@ export const AUDIO_SETTINGS_DEFAULTS = {
|
|||||||
masterVolume: 1,
|
masterVolume: 1,
|
||||||
alertVolume: 0.5,
|
alertVolume: 0.5,
|
||||||
roverVolume: 1,
|
roverVolume: 1,
|
||||||
autoLevelEnabled: true,
|
mainBrushDuckEnabled: true,
|
||||||
autoLevelMode: 'compressor',
|
mainBrushDuckAmount: 0.45,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user