feat: complete bark battle playable demo

This commit is contained in:
2026-05-12 14:42:58 +08:00
parent 22810245f5
commit 33c9079d3b
16 changed files with 639 additions and 196 deletions

View File

@@ -22,3 +22,64 @@ export function isMicrophoneApiSupported(windowLike: { isSecureContext?: boolean
export function stopMediaStreamTracks(stream: MediaStream) {
stream.getTracks().forEach((track) => track.stop());
}
export type BrowserMicrophoneSampler = {
stop: () => void;
};
export type BrowserMicrophoneVolumeHandler = (volume: number, atMs: number) => void;
export async function startBrowserMicrophoneSampler(onVolume: BrowserMicrophoneVolumeHandler): Promise<BrowserMicrophoneSampler> {
const supported = isMicrophoneApiSupported(window);
if (!supported.ok) {
throw Object.assign(new Error(supported.reason), { reason: supported.reason });
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
if (!AudioContextCtor) {
stopMediaStreamTracks(stream);
throw Object.assign(new Error('audio-context-blocked'), { reason: 'audio-context-blocked' });
}
const audioContext = new AudioContextCtor();
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
const analyser = audioContext.createAnalyser();
analyser.fftSize = 512;
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
const data = new Uint8Array(analyser.fftSize);
const sampleStartedAtMs = window.performance.now();
let rafId = 0;
const sample = () => {
analyser.getByteTimeDomainData(data);
let sum = 0;
for (const value of data) {
const centered = (value - 128) / 128;
sum += centered * centered;
}
const volume = Math.min(1, Math.sqrt(sum / data.length) * 3.5);
onVolume(volume, window.performance.now() - sampleStartedAtMs);
rafId = window.requestAnimationFrame(sample);
};
sample();
return {
stop: () => {
window.cancelAnimationFrame(rafId);
source.disconnect();
void audioContext.close();
stopMediaStreamTracks(stream);
},
};
} catch (error) {
const reason = error && typeof error === 'object' && 'reason' in error ? (error as { reason: MicrophoneFailureReason }).reason : mapGetUserMediaError(error);
throw Object.assign(new Error(reason), { reason });
}
}
declare global {
interface Window {
webkitAudioContext?: typeof AudioContext;
}
}