From c1f389c87f789452ff186a364ee41d27ac663557 Mon Sep 17 00:00:00 2001 From: Matt Hillsdon Date: Tue, 27 Aug 2024 12:32:31 +0100 Subject: [PATCH] Tidy up and add a background recording demo There are still Firefox/Safari issues that we're trying to understand. --- src/board/audio/index.ts | 39 ++++++++++++++----------------- src/board/index.ts | 2 ++ src/demo.html | 1 + src/examples/record_background.py | 19 +++++++++++++++ 4 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 src/examples/record_background.py diff --git a/src/board/audio/index.ts b/src/board/audio/index.ts index 7a7a297..e779365 100644 --- a/src/board/audio/index.ts +++ b/src/board/audio/index.ts @@ -20,6 +20,7 @@ interface AudioOptions { speechAudioCallback: () => void; speechResampler: SRC; soundExpressionResampler: SRC; + recordingResampler: SRC; } export class BoardAudio { @@ -32,6 +33,8 @@ export class BoardAudio { private muteNode: GainNode | undefined; private sensitivityNode: GainNode | undefined; + private recordingResampler: SRC | undefined; + default: BufferedAudio | undefined; speech: BufferedAudio | undefined; soundExpression: BufferedAudio | undefined; @@ -46,10 +49,13 @@ export class BoardAudio { speechAudioCallback, speechResampler, soundExpressionResampler, + recordingResampler, }: AudioOptions) { if (!this.context) { throw new Error("Context must be pre-created from a user event"); } + this.recordingResampler = recordingResampler; + this.muteNode = this.context.createGain(); this.muteNode.gain.setValueAtTime( this.muted ? 0 : 1, @@ -221,17 +227,13 @@ export class BoardAudio { source.connect(this.sensitivityNode!); const recorder = this.context!.createScriptProcessor(2048, 1, 1); - const inputSampleRate = 44100; - const sampleRateConverter = await createSampleRateConverter( - 1, - inputSampleRate, - sampleRate, - { - converterType: ConverterType.SRC_SINC_FASTEST, - } - ); + + const inputSampleRate = this.context!.sampleRate; + this.recordingResampler!.inputSampleRate = inputSampleRate; + this.recordingResampler!.outputSampleRate = sampleRate; + recorder.onaudioprocess = (e) => { - const resampled = sampleRateConverter.full( + const resampled = this.recordingResampler!.full( e.inputBuffer.getChannelData(0) ); onChunk(resampled); @@ -248,8 +250,7 @@ export class BoardAudio { recorder.disconnect(); this.sensitivityNode!.disconnect(); source.disconnect(); - micStream.getTracks().forEach((track) => track.stop()); - sampleRateConverter.destroy(); + micStream?.getTracks().forEach((track) => track.stop()); this.microphoneEl.style.display = "none"; this.stopActiveRecording = undefined; }; @@ -273,7 +274,6 @@ export class BoardAudio { class BufferedAudio { nextStartTime: number = -1; - private sampleRate: number = -1; constructor( private context: AudioContext, @@ -291,19 +291,14 @@ class BufferedAudio { } setSampleRate(sampleRate: number) { - this.sampleRate = sampleRate; this.resampler.inputSampleRate = sampleRate; } writeData(data: Float32Array) { - let sampleRate = this.sampleRate; - // In practice the supported range is less than the 8k..96k required by the spec - let alwaysResample = true; - if (alwaysResample || sampleRate < 8_000 || sampleRate > 96_000) { - // We need to resample - sampleRate = this.context.sampleRate; - data = this.resampler.full(data); - } + // In practice the supported range is less than the 8k..96k required by the spec and varies by browser + // for a consistent performance profile we're always resampling for now rather than letting Web Audio do it + let sampleRate = this.context.sampleRate; + data = this.resampler.full(data); // Use createXXX instead to support Safari 14.0. const buffer = this.context.createBuffer(1, data.length, sampleRate); diff --git a/src/board/index.ts b/src/board/index.ts index 96d0b7a..e3752c2 100644 --- a/src/board/index.ts +++ b/src/board/index.ts @@ -250,6 +250,7 @@ export class Board { }); // We update the sample rates before use. + const recordingResampler = await createResampler(1, 48000, 48000); const defaultResampler = await createResampler(1, 48000, 48000); const speechResampler = await createResampler(1, 48000, 48000); // Probably this one is never used so would be nice to avoid @@ -262,6 +263,7 @@ export class Board { speechAudioCallback: wrapped._microbit_hal_audio_speech_ready_callback, speechResampler, soundExpressionResampler, + recordingResampler, }); this.accelerometer.initializeCallbacks( wrapped._microbit_hal_gesture_callback diff --git a/src/demo.html b/src/demo.html index a887cd8..8f3cd3f 100644 --- a/src/demo.html +++ b/src/demo.html @@ -99,6 +99,7 @@

MicroPython-micro:bit simulator example embedding

+ diff --git a/src/examples/record_background.py b/src/examples/record_background.py new file mode 100644 index 0000000..2d06859 --- /dev/null +++ b/src/examples/record_background.py @@ -0,0 +1,19 @@ +from microbit import microphone, audio, button_a, button_b, sleep + +rates = [7812, 3906, 15624] +rate_index = 0 + +print("Recording...") +my_recording = audio.AudioRecording(3000) +my_track = microphone.record_into(my_recording, wait=False) +sleep(3000) +print("Button A to play") +while True: + if button_a.was_pressed(): + audio.play(my_track, wait=False) + print("Rate playing", rates[rate_index]) + + if button_b.was_pressed(): + rate_index = (rate_index + 1) % len(rates) + print("Rate change to", rates[rate_index]) + my_track.set_rate(rates[rate_index]) \ No newline at end of file