salgadev commited on
Commit
5f6cd09
·
verified ·
1 Parent(s): fa8e525

Sync from GitHub 7eaee086

Browse files
Files changed (1) hide show
  1. apps/web/lib/jambuddy/recorder.ts +78 -7
apps/web/lib/jambuddy/recorder.ts CHANGED
@@ -238,6 +238,70 @@ export async function grantMicPermission(): Promise<MediaStream> {
238
  return navigator.mediaDevices.getUserMedia({ audio: true });
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  /**
242
  * Create an audio recorder. Requests microphone access (user gesture). Throws
243
  * if getUserMedia/MediaRecorder is unsupported or the mic is denied.
@@ -289,17 +353,24 @@ export async function createAudioRecorder(
289
  },
290
  stop() {
291
  return new Promise((resolve) => {
292
- const onStop = () => {
293
  active = false;
294
  stream.getTracks().forEach((t) => t.stop());
295
  const type = rec.mimeType || "audio/webm";
 
 
 
 
 
 
 
 
296
  const file = new File(
297
- chunks,
298
- `jambuddy-live-audio-${new Date()
299
- .toISOString()
300
- .replace(/[-:]/g, "")
301
- .replace(/\.\d+Z$/, "Z")}.webm`,
302
- { type },
303
  );
304
  resolve({
305
  file,
 
238
  return navigator.mediaDevices.getUserMedia({ audio: true });
239
  }
240
 
241
+ /**
242
+ * Decode an audio Blob (any browser-playable format, e.g. WebM/Opus from
243
+ * MediaRecorder) into a 16-bit PCM WAV Blob. The Python pipeline reads the
244
+ * take with `soundfile`, which does NOT understand WebM — it needs a real WAV.
245
+ * Returns null if decode fails (caller can fall back to the raw blob).
246
+ */
247
+ export async function blobToWav(blob: Blob): Promise<Blob | null> {
248
+ try {
249
+ const buf = await blob.arrayBuffer();
250
+ const Ctx =
251
+ window.AudioContext ??
252
+ (window as unknown as { webkitAudioContext?: typeof AudioContext })
253
+ .webkitAudioContext;
254
+ if (!Ctx) return null;
255
+ const ctx = new Ctx();
256
+ try {
257
+ const audioBuf = await ctx.decodeAudioData(buf);
258
+ const numChannels = audioBuf.numberOfChannels ?? 1;
259
+ const sr = audioBuf.sampleRate ?? 44100;
260
+ const samples = audioBuf.getChannelData(0);
261
+ const numFrames = samples.length;
262
+ const interleaved = new Float32Array(numFrames * numChannels);
263
+ for (let ch = 0; ch < numChannels; ch++) {
264
+ const chan = audioBuf.getChannelData(ch);
265
+ for (let i = 0; i < numFrames; i++) {
266
+ const v = chan[i];
267
+ if (v !== undefined) interleaved[i * numChannels + ch] = v;
268
+ }
269
+ }
270
+ // 16-bit PCM WAV.
271
+ const buffer = new ArrayBuffer(44 + interleaved.length * 2);
272
+ const view = new DataView(buffer);
273
+ const writeStr = (off: number, s: string) => {
274
+ for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i));
275
+ };
276
+ writeStr(0, "RIFF");
277
+ view.setUint32(4, 36 + interleaved.length * 2, true);
278
+ writeStr(8, "WAVE");
279
+ writeStr(12, "fmt ");
280
+ view.setUint32(16, 16, true);
281
+ view.setUint16(20, 1, true); // PCM
282
+ view.setUint16(22, numChannels, true);
283
+ view.setUint32(24, sr, true);
284
+ view.setUint32(28, sr * numChannels * 2, true);
285
+ view.setUint16(32, numChannels * 2, true);
286
+ view.setUint16(34, 16, true);
287
+ writeStr(36, "data");
288
+ view.setUint32(40, interleaved.length * 2, true);
289
+ let off = 44;
290
+ for (let i = 0; i < interleaved.length; i++) {
291
+ const raw = interleaved[i] ?? 0;
292
+ const s = Math.max(-1, Math.min(1, raw));
293
+ view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7fff, true);
294
+ off += 2;
295
+ }
296
+ return new Blob([buffer], { type: "audio/wav" });
297
+ } finally {
298
+ void ctx.close();
299
+ }
300
+ } catch {
301
+ return null;
302
+ }
303
+ }
304
+
305
  /**
306
  * Create an audio recorder. Requests microphone access (user gesture). Throws
307
  * if getUserMedia/MediaRecorder is unsupported or the mic is denied.
 
353
  },
354
  stop() {
355
  return new Promise((resolve) => {
356
+ const onStop = async () => {
357
  active = false;
358
  stream.getTracks().forEach((t) => t.stop());
359
  const type = rec.mimeType || "audio/webm";
360
+ const raw = new Blob(chunks, { type });
361
+ // Convert WebM -> PCM WAV so the Python pipeline (soundfile) can read
362
+ // it. If decode fails, fall back to the raw blob.
363
+ const wav = await blobToWav(raw);
364
+ const ts = new Date()
365
+ .toISOString()
366
+ .replace(/[-:]/g, "")
367
+ .replace(/\.\d+Z$/, "Z");
368
  const file = new File(
369
+ [wav ?? raw],
370
+ wav
371
+ ? `jambuddy-live-audio-${ts}.wav`
372
+ : `jambuddy-live-audio-${ts}.webm`,
373
+ { type: wav ? "audio/wav" : type },
 
374
  );
375
  resolve({
376
  file,