1
+ import { SRC } from "@alexanderolsen/libsamplerate-js/dist/src" ;
1
2
import { replaceBuiltinSound } from "./built-in-sounds" ;
2
3
import { SoundEmojiSynthesizer } from "./sound-emoji-synthesizer" ;
3
4
import { parseSoundEffects } from "./sound-expressions" ;
@@ -15,7 +16,10 @@ declare global {
15
16
16
17
interface AudioOptions {
17
18
defaultAudioCallback : ( ) => void ;
19
+ defaultResampler : SRC ;
18
20
speechAudioCallback : ( ) => void ;
21
+ speechResampler : SRC ;
22
+ soundExpressionResampler : SRC ;
19
23
}
20
24
21
25
export class BoardAudio {
@@ -38,7 +42,10 @@ export class BoardAudio {
38
42
39
43
initializeCallbacks ( {
40
44
defaultAudioCallback,
45
+ defaultResampler,
41
46
speechAudioCallback,
47
+ speechResampler,
48
+ soundExpressionResampler,
42
49
} : AudioOptions ) {
43
50
if ( ! this . context ) {
44
51
throw new Error ( "Context must be pre-created from a user event" ) ;
@@ -60,16 +67,19 @@ export class BoardAudio {
60
67
this . default = new BufferedAudio (
61
68
this . context ,
62
69
this . volumeNode ,
70
+ defaultResampler ,
63
71
defaultAudioCallback
64
72
) ;
65
73
this . speech = new BufferedAudio (
66
74
this . context ,
67
75
this . volumeNode ,
76
+ speechResampler ,
68
77
speechAudioCallback
69
78
) ;
70
79
this . soundExpression = new BufferedAudio (
71
80
this . context ,
72
81
this . volumeNode ,
82
+ soundExpressionResampler ,
73
83
( ) => {
74
84
if ( this . currentSoundExpressionCallback ) {
75
85
this . currentSoundExpressionCallback ( ) ;
@@ -79,12 +89,13 @@ export class BoardAudio {
79
89
}
80
90
81
91
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
82
96
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
+
88
99
if ( this . context . state === "suspended" ) {
89
100
return this . context . resume ( ) ;
90
101
}
@@ -96,21 +107,16 @@ export class BoardAudio {
96
107
this . stopSoundExpression ( ) ;
97
108
} ;
98
109
const synth = new SoundEmojiSynthesizer ( 0 , onDone ) ;
110
+ this . soundExpression ! . setSampleRate ( synth . sampleRate ) ;
99
111
synth . play ( soundEffects ) ;
100
112
101
113
const callback = ( ) => {
102
114
const source = synth . pull ( ) ;
103
115
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 ) ;
111
117
for ( let i = 0 ; i < source . length ; i ++ ) {
112
118
// 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 ;
114
120
}
115
121
this . soundExpression ! . writeData ( target ) ;
116
122
}
@@ -201,6 +207,7 @@ export class BoardAudio {
201
207
try {
202
208
micStream = await navigator . mediaDevices . getUserMedia ( {
203
209
video : false ,
210
+ // It seems Firefox ignores the rate set here
204
211
audio : true ,
205
212
} ) ;
206
213
} catch ( e ) {
@@ -271,26 +278,41 @@ class BufferedAudio {
271
278
constructor (
272
279
private context : AudioContext ,
273
280
private destination : AudioNode ,
281
+ private resampler : SRC ,
274
282
private callback : ( ) => void
275
- ) { }
283
+ ) {
284
+ this . resampler . outputSampleRate = this . context . sampleRate ;
285
+ }
276
286
277
287
init ( sampleRate : number ) {
278
288
// This is called for each new audio source so don't reset nextStartTime
279
289
// 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 ) ;
286
291
}
287
292
288
293
setSampleRate ( sampleRate : number ) {
289
294
this . sampleRate = sampleRate ;
295
+ this . resampler . inputSampleRate = sampleRate ;
290
296
}
291
297
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 ) ;
294
316
const source = this . context . createBufferSource ( ) ;
295
317
source . buffer = buffer ;
296
318
source . onended = this . callCallback ;
0 commit comments