Skip to content

Commit 51ad8f8

Browse files
Resample to audio context rate for playback
For now we do this only for samples Web Audio can't cope with. I suspect we need to reduce the range that we defer to Web Audio for FF and old Safari. This works in Chrome but nowhere else, which is a shame because everywhere else needs it more urgently than Chrome... Puzzling over this.
1 parent 6239300 commit 51ad8f8

File tree

4 files changed

+61
-41
lines changed

4 files changed

+61
-41
lines changed

Diff for: src/board/audio/index.ts

+44-22
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SRC } from "@alexanderolsen/libsamplerate-js/dist/src";
12
import { replaceBuiltinSound } from "./built-in-sounds";
23
import { SoundEmojiSynthesizer } from "./sound-emoji-synthesizer";
34
import { parseSoundEffects } from "./sound-expressions";
@@ -15,7 +16,10 @@ declare global {
1516

1617
interface AudioOptions {
1718
defaultAudioCallback: () => void;
19+
defaultResampler: SRC;
1820
speechAudioCallback: () => void;
21+
speechResampler: SRC;
22+
soundExpressionResampler: SRC;
1923
}
2024

2125
export class BoardAudio {
@@ -38,7 +42,10 @@ export class BoardAudio {
3842

3943
initializeCallbacks({
4044
defaultAudioCallback,
45+
defaultResampler,
4146
speechAudioCallback,
47+
speechResampler,
48+
soundExpressionResampler,
4249
}: AudioOptions) {
4350
if (!this.context) {
4451
throw new Error("Context must be pre-created from a user event");
@@ -60,16 +67,19 @@ export class BoardAudio {
6067
this.default = new BufferedAudio(
6168
this.context,
6269
this.volumeNode,
70+
defaultResampler,
6371
defaultAudioCallback
6472
);
6573
this.speech = new BufferedAudio(
6674
this.context,
6775
this.volumeNode,
76+
speechResampler,
6877
speechAudioCallback
6978
);
7079
this.soundExpression = new BufferedAudio(
7180
this.context,
7281
this.volumeNode,
82+
soundExpressionResampler,
7383
() => {
7484
if (this.currentSoundExpressionCallback) {
7585
this.currentSoundExpressionCallback();
@@ -79,12 +89,13 @@ export class BoardAudio {
7989
}
8090

8191
async createAudioContextFromUserInteraction(): Promise<void> {
92+
// If we set a 44.1kHz rate then we fail to connect to user media on Mac as it selects 48000
93+
// So we leave it at the default hoping it's most likely to match user media...
94+
// Until there's progress on this there doesn't seem a better way:
95+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1674892
8296
this.context =
83-
this.context ??
84-
new (window.AudioContext || window.webkitAudioContext)({
85-
// The highest rate is the sound expression synth.
86-
sampleRate: 44100,
87-
});
97+
this.context ?? new (window.AudioContext || window.webkitAudioContext)();
98+
8899
if (this.context.state === "suspended") {
89100
return this.context.resume();
90101
}
@@ -96,21 +107,16 @@ export class BoardAudio {
96107
this.stopSoundExpression();
97108
};
98109
const synth = new SoundEmojiSynthesizer(0, onDone);
110+
this.soundExpression!.setSampleRate(synth.sampleRate);
99111
synth.play(soundEffects);
100112

101113
const callback = () => {
102114
const source = synth.pull();
103115
if (this.context) {
104-
// Use createBuffer instead of new AudioBuffer to support Safari 14.0.
105-
const target = this.context.createBuffer(
106-
1,
107-
source.length,
108-
synth.sampleRate
109-
);
110-
const channel = target.getChannelData(0);
116+
const target = new Float32Array(source.length);
111117
for (let i = 0; i < source.length; i++) {
112118
// Buffer is (0, 1023) we need to map it to (-1, 1)
113-
channel[i] = (source[i] - 512) / 512;
119+
target[i] = (source[i] - 512) / 512;
114120
}
115121
this.soundExpression!.writeData(target);
116122
}
@@ -201,6 +207,7 @@ export class BoardAudio {
201207
try {
202208
micStream = await navigator.mediaDevices.getUserMedia({
203209
video: false,
210+
// It seems Firefox ignores the rate set here
204211
audio: true,
205212
});
206213
} catch (e) {
@@ -271,26 +278,41 @@ class BufferedAudio {
271278
constructor(
272279
private context: AudioContext,
273280
private destination: AudioNode,
281+
private resampler: SRC,
274282
private callback: () => void
275-
) {}
283+
) {
284+
this.resampler.outputSampleRate = this.context.sampleRate;
285+
}
276286

277287
init(sampleRate: number) {
278288
// This is called for each new audio source so don't reset nextStartTime
279289
// or we start to overlap audio
280-
this.sampleRate = sampleRate;
281-
}
282-
283-
createBuffer(length: number) {
284-
// Use createBuffer instead of new AudioBuffer to support Safari 14.0.
285-
return this.context.createBuffer(1, length, this.sampleRate);
290+
this.setSampleRate(sampleRate);
286291
}
287292

288293
setSampleRate(sampleRate: number) {
289294
this.sampleRate = sampleRate;
295+
this.resampler.inputSampleRate = sampleRate;
290296
}
291297

292-
writeData(buffer: AudioBuffer) {
293-
// Use createBufferSource instead of new AudioBufferSourceNode to support Safari 14.0.
298+
writeData(data: Float32Array) {
299+
let sampleRate = this.sampleRate;
300+
// In practice the supported range is less than the 8k..96k required by the spec
301+
if (sampleRate < 8_000 || sampleRate > 96_000) {
302+
// We need to resample
303+
//sampleRate = this.resampler.outputSampleRate;
304+
//data = this.resampler.full(data);
305+
}
306+
console.log(
307+
"Using actual rate",
308+
sampleRate,
309+
"for requested rate",
310+
this.sampleRate
311+
);
312+
313+
// Use createXXX instead to support Safari 14.0.
314+
const buffer = this.context.createBuffer(1, data.length, sampleRate);
315+
buffer.copyToChannel(data, 0);
294316
const source = this.context.createBufferSource();
295317
source.buffer = buffer;
296318
source.onended = this.callCallback;

Diff for: src/board/conversions.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,11 @@ export function convertAccelerometerNumberToString(value: number): string {
9898
export const convertAudioBuffer = (
9999
heap: Uint8Array,
100100
source: number,
101-
target: AudioBuffer
101+
target: Float32Array
102102
) => {
103-
const channel = target.getChannelData(0);
104-
for (let i = 0; i < channel.length; ++i) {
103+
for (let i = 0; i < target.length; ++i) {
105104
// Convert from uint8 to -1..+1 float.
106-
channel[i] = (heap[source + i] / 255) * 2 - 1;
105+
target[i] = (heap[source + i] / 255) * 2 - 1;
107106
}
108107
return target;
109108
};

Diff for: src/board/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { create as createResampler } from "@alexanderolsen/libsamplerate-js";
12
import svgText from "../microbit-drawing.svg";
23
import { Accelerometer } from "./accelerometer";
34
import { BoardAudio } from "./audio";
@@ -247,10 +248,20 @@ export class Board {
247248
noInitialRun: true,
248249
instantiateWasm,
249250
});
251+
252+
// We update the sample rates before use.
253+
const defaultResampler = await createResampler(1, 48000, 48000);
254+
const speechResampler = await createResampler(1, 48000, 48000);
255+
// Probably this one is never used so would be nice to avoid
256+
const soundExpressionResampler = await createResampler(1, 48000, 48000);
257+
250258
const module = new ModuleWrapper(wrapped);
251259
this.audio.initializeCallbacks({
252260
defaultAudioCallback: wrapped._microbit_hal_audio_raw_ready_callback,
261+
defaultResampler,
253262
speechAudioCallback: wrapped._microbit_hal_audio_speech_ready_callback,
263+
speechResampler,
264+
soundExpressionResampler,
254265
});
255266
this.accelerometer.initializeCallbacks(
256267
wrapped._microbit_hal_gesture_callback

Diff for: src/jshal.js

+3-15
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,7 @@ mergeInto(LibraryManager.library, {
230230
Module.conversions.convertAudioBuffer(
231231
Module.HEAPU8,
232232
buf,
233-
// @ts-expect-error
234-
Module.board.audio.default.createBuffer(num_samples)
233+
new Float32Array(num_samples)
235234
)
236235
);
237236
},
@@ -246,21 +245,10 @@ mergeInto(LibraryManager.library, {
246245
/** @type {number} */ num_samples
247246
) {
248247
/** @type {AudioBuffer | undefined} */ let webAudioBuffer;
249-
try {
250-
// @ts-expect-error
251-
webAudioBuffer = Module.board.audio.speech.createBuffer(num_samples);
252-
} catch (e) {
253-
// Swallow error on older Safari to keep the sim in a good state.
254-
// @ts-expect-error
255-
if (e.name === "NotSupportedError") {
256-
return;
257-
} else {
258-
throw e;
259-
}
260-
}
248+
const jsBuf = new Float32Array(num_samples);
261249
// @ts-expect-error
262250
Module.board.audio.speech.writeData(
263-
Module.conversions.convertAudioBuffer(Module.HEAPU8, buf, webAudioBuffer)
251+
Module.conversions.convertAudioBuffer(Module.HEAPU8, buf, jsBuf)
264252
);
265253
},
266254

0 commit comments

Comments
 (0)