Files
podman/frontend/src/lib/beat.ts
T
Kartikeya 8271188cf1 feat(frontend): live room view, beat connectivity test, session resume
- PodView now shows LIVE LiveKit participants (updates as people join/leave,
  active-speaker highlight) instead of the static roster — no refresh needed.
- "Play beat" button publishes a generated 4/4 beat into the room so every
  participant hears the same audio (speaker lights up) — a real connectivity test.
- "Share my screen" is now a deliberate button; joining only connects to the
  room (fast/reliable), so a denied/slow screen prompt no longer fails the join.
- Session persisted in sessionStorage with auto-reconnect, so a refresh keeps
  you in the room instead of dropping back to the pod list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:37:32 -07:00

78 lines
2.3 KiB
TypeScript

export interface BeatHandle {
track: MediaStreamTrack;
stop: () => void;
}
/**
* Generate a simple 4-on-the-floor beat (kick + hi-hat) as an audio
* MediaStreamTrack to publish into a LiveKit room. Also routes to the local
* speakers so the publisher hears it too. Pure Web Audio — no asset/CORS.
*/
export function startBeat(): BeatHandle {
const ctx = new AudioContext();
void ctx.resume();
const dest = ctx.createMediaStreamDestination();
const master = ctx.createGain();
master.gain.value = 0.5;
master.connect(dest); // -> published track (remote listeners)
master.connect(ctx.destination); // -> local speakers (publisher)
const bpm = 120;
const spb = 60 / bpm;
let next = ctx.currentTime + 0.1;
let beat = 0;
function kick(time: number, accent: boolean) {
const osc = ctx.createOscillator();
const g = ctx.createGain();
osc.frequency.setValueAtTime(accent ? 180 : 150, time);
osc.frequency.exponentialRampToValueAtTime(50, time + 0.12);
g.gain.setValueAtTime(0.0001, time);
g.gain.exponentialRampToValueAtTime(accent ? 1 : 0.7, time + 0.005);
g.gain.exponentialRampToValueAtTime(0.0001, time + 0.18);
osc.connect(g);
g.connect(master);
osc.start(time);
osc.stop(time + 0.2);
}
function hat(time: number) {
const size = Math.floor(ctx.sampleRate * 0.05);
const buffer = ctx.createBuffer(1, size, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < size; i++) data[i] = Math.random() * 2 - 1;
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const hp = ctx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = 7000;
const g = ctx.createGain();
g.gain.setValueAtTime(0.12, time);
g.gain.exponentialRampToValueAtTime(0.0001, time + 0.05);
noise.connect(hp);
hp.connect(g);
g.connect(master);
noise.start(time);
noise.stop(time + 0.05);
}
const timer = window.setInterval(() => {
while (next < ctx.currentTime + 0.2) {
kick(next, beat % 4 === 0);
hat(next + spb / 2);
next += spb;
beat++;
}
}, 50);
const track = dest.stream.getAudioTracks()[0]!;
return {
track,
stop: () => {
window.clearInterval(timer);
track.stop();
void ctx.close();
},
};
}