Skip to content

Commit 2981677

Browse files
committed
bring back the old mixer play instructions (#11123)
1 parent eba3fa5 commit 2981677

2 files changed

Lines changed: 329 additions & 12 deletions

File tree

pxtsim/sound/audioContextManager.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -187,25 +187,16 @@ namespace pxsim.AudioContextManager {
187187
export async function playInstructionsAsync(instructions: Uint8Array, isCancelled?: () => boolean, onPull?: SoundPreviewCallback) {
188188
soundEventCallback?.("playinstructions", instructions);
189189

190-
await AudioWorkletSource.initializeWorklet(context());
190+
const channel = new PlayInstructionsSource(context(), destination);
191191

192-
let channel: AudioWorkletSource;
193192
let finished = false;
194193

195194
if (onPull) {
196-
channel = new AudioWorkletSource(context(), destination);
197-
198-
initOscilloscope(onPull, channel.analyser, () => finished || isCancelled?.());
199-
}
200-
else {
201-
channel = AudioWorkletSource.getAvailableSource();
202-
203-
if (!channel) {
204-
channel = new AudioWorkletSource(context(), destination);
205-
}
195+
initOscilloscope(onPull, channel.analyser, () => finished || isCancelled?.() || channel.isDisposed());
206196
}
207197

208198
await channel.playInstructionsAsync(instructions, isCancelled);
199+
channel.dispose();
209200

210201
finished = true;
211202
}

pxtsim/sound/playInstructions.ts

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
namespace pxsim.AudioContextManager {
2+
const waveForms: OscillatorType[] = [null, "triangle", "sawtooth", "sine"]
3+
let noiseBuffer: AudioBuffer
4+
let rectNoiseBuffer: AudioBuffer
5+
let cycleNoiseBuffer: AudioBuffer[] = []
6+
let squareBuffer: AudioBuffer[] = []
7+
8+
function getNoiseBuffer(context: AudioContext) {
9+
if (!noiseBuffer) {
10+
const bufferSize = 100000;
11+
noiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
12+
const output = noiseBuffer.getChannelData(0);
13+
14+
let x = 0xf01ba80;
15+
for (let i = 0; i < bufferSize; i++) {
16+
x ^= x << 13;
17+
x ^= x >> 17;
18+
x ^= x << 5;
19+
output[i] = ((x & 1023) / 512.0) - 1.0;
20+
}
21+
}
22+
return noiseBuffer
23+
}
24+
25+
function getRectNoiseBuffer(context: AudioContext) {
26+
// Create a square wave filtered by a pseudorandom bit sequence.
27+
// This uses four samples per cycle to create square-ish waves.
28+
// The Web Audio API's frequency scaling may be using linear
29+
// interpolation which would turn a two-sample wave into a triangle.
30+
if (!rectNoiseBuffer) {
31+
const bufferSize = 131072; // must be a multiple of 4
32+
rectNoiseBuffer = context.createBuffer(1, bufferSize, context.sampleRate);
33+
const output = rectNoiseBuffer.getChannelData(0);
34+
35+
let x = 0xf01ba80;
36+
for (let i = 0; i < bufferSize; i += 4) {
37+
// see https://en.wikipedia.org/wiki/Xorshift
38+
x ^= x << 13;
39+
x ^= x >> 17;
40+
x ^= x << 5;
41+
if (x & 0x8000) {
42+
output[i] = 1.0;
43+
output[i + 1] = 1.0;
44+
output[i + 2] = -1.0;
45+
output[i + 3] = -1.0;
46+
} else {
47+
output[i] = 0.0;
48+
output[i + 1] = 0.0;
49+
output[i + 2] = 0.0;
50+
output[i + 3] = 0.0;
51+
}
52+
}
53+
}
54+
return rectNoiseBuffer
55+
}
56+
57+
function getCycleNoiseBuffer(context: AudioContext, bits: number) {
58+
if (!cycleNoiseBuffer[bits]) {
59+
// Buffer size needs to be a multiple of 4x the largest cycle length,
60+
// 4*64 in this case.
61+
const bufferSize = 1024;
62+
const buf = context.createBuffer(1, bufferSize, context.sampleRate);
63+
const output = buf.getChannelData(0);
64+
65+
// See pxt-common-packages's libs/mixer/melody.cpp for details.
66+
// "bits" must be in the range 4..6.
67+
const cycle_bits: number[] = [0x2df0eb47, 0xc8165a93];
68+
const mask_456: number[] = [0xf, 0x1f, 0x3f];
69+
for (let i = 0; i < bufferSize; i += 4) {
70+
let cycle: number = i / 4;
71+
let is_on: boolean;
72+
let cycle_mask = mask_456[bits - 4];
73+
cycle &= cycle_mask;
74+
is_on = (cycle_bits[cycle >> 5] & (1 << (cycle & 0x1f))) != 0;
75+
if (is_on) {
76+
output[i] = 1.0;
77+
output[i + 1] = 1.0;
78+
output[i + 2] = -1.0;
79+
output[i + 3] = -1.0;
80+
} else {
81+
output[i] = 0.0;
82+
output[i + 1] = 0.0;
83+
output[i + 2] = 0.0;
84+
output[i + 3] = 0.0;
85+
}
86+
}
87+
cycleNoiseBuffer[bits] = buf
88+
}
89+
return cycleNoiseBuffer[bits]
90+
}
91+
92+
function getSquareBuffer(context: AudioContext, param: number) {
93+
if (!squareBuffer[param]) {
94+
const bufferSize = 1024;
95+
const buf = context.createBuffer(1, bufferSize, context.sampleRate);
96+
const output = buf.getChannelData(0);
97+
for (let i = 0; i < bufferSize; i++) {
98+
output[i] = i < (param / 100 * bufferSize) ? 1 : -1;
99+
}
100+
squareBuffer[param] = buf
101+
}
102+
return squareBuffer[param]
103+
}
104+
105+
/*
106+
#define SW_TRIANGLE 1
107+
#define SW_SAWTOOTH 2
108+
#define SW_SINE 3
109+
#define SW_TUNEDNOISE 4
110+
#define SW_NOISE 5
111+
#define SW_SQUARE_10 11
112+
#define SW_SQUARE_50 15
113+
#define SW_SQUARE_CYCLE_16 16
114+
#define SW_SQUARE_CYCLE_32 17
115+
#define SW_SQUARE_CYCLE_64 18
116+
*/
117+
118+
119+
/*
120+
struct SoundInstruction {
121+
uint8_t soundWave;
122+
uint8_t flags;
123+
uint16_t frequency;
124+
uint16_t duration;
125+
uint16_t startVolume;
126+
uint16_t endVolume;
127+
};
128+
*/
129+
130+
function getGenerator(context: AudioContext, waveFormIdx: number, hz: number): OscillatorNode | AudioBufferSourceNode {
131+
let form = waveForms[waveFormIdx]
132+
if (form) {
133+
let src = context.createOscillator()
134+
src.type = form
135+
src.frequency.value = hz
136+
return src
137+
}
138+
139+
let buffer: AudioBuffer
140+
if (waveFormIdx == 4)
141+
buffer = getRectNoiseBuffer(context)
142+
else if (waveFormIdx == 5)
143+
buffer = getNoiseBuffer(context)
144+
else if (11 <= waveFormIdx && waveFormIdx <= 15)
145+
buffer = getSquareBuffer(context, (waveFormIdx - 10) * 10)
146+
else if (16 <= waveFormIdx && waveFormIdx <= 18)
147+
buffer = getCycleNoiseBuffer(context, (waveFormIdx - 16) + 4)
148+
else
149+
return null
150+
151+
let node = context.createBufferSource();
152+
node.buffer = buffer;
153+
node.loop = true;
154+
const isFilteredNoise = waveFormIdx == 4 || (16 <= waveFormIdx && waveFormIdx <= 18);
155+
if (isFilteredNoise)
156+
node.playbackRate.value = hz / (context.sampleRate / 4);
157+
else if (waveFormIdx != 5)
158+
node.playbackRate.value = hz / (context.sampleRate / 1024);
159+
160+
return node
161+
}
162+
163+
export class PlayInstructionsSource extends AudioSource {
164+
analyser: AnalyserNode;
165+
gain: GainNode;
166+
167+
constructor(public context: AudioContext, public destination: AudioNode) {
168+
super(context, destination);
169+
this.analyser = context.createAnalyser();
170+
this.analyser.fftSize = 2048;
171+
this.analyser.connect(this.vca);
172+
173+
this.gain = context.createGain();
174+
this.gain.connect(this.analyser);
175+
}
176+
177+
playInstructionsAsync(instructions: Uint8Array, isCancelled?: () => boolean, onPull?: (freq: number, volume: number) => void) {
178+
return new Promise<void>(async resolve => {
179+
soundEventCallback?.("playinstructions", instructions);
180+
let resolved = false;
181+
182+
const oscillators: pxt.Map<OscillatorNode | AudioBufferSourceNode> = {};
183+
const gains: pxt.Map<GainNode> = {};
184+
let startTime = this.context.currentTime;
185+
let currentTime = startTime;
186+
let currentWave = 0;
187+
188+
let totalDuration = 0;
189+
190+
/** Square waves are perceved as much louder than other sounds, so scale it down a bit to make it less jarring **/
191+
const scaleVol = (n: number, isSqWave?: boolean) => (n / 1024) / 4 * (isSqWave ? .5 : 1);
192+
193+
const disconnectNodes = () => {
194+
if (resolved) return;
195+
resolved = true;
196+
197+
for (const wave of Object.keys(oscillators)) {
198+
oscillators[wave].stop();
199+
oscillators[wave].disconnect();
200+
gains[wave].disconnect();
201+
}
202+
resolve();
203+
}
204+
205+
for (let i = 0; i < instructions.length; i += 12) {
206+
const wave = instructions[i];
207+
const startFrequency = readUint16(instructions, i + 2);
208+
const duration = readUint16(instructions, i + 4) / 1000;
209+
const startVolume = readUint16(instructions, i + 6);
210+
const endVolume = readUint16(instructions, i + 8);
211+
const endFrequency = readUint16(instructions, i + 10);
212+
totalDuration += duration
213+
214+
if (wave === 0) {
215+
currentTime += duration;
216+
continue;
217+
}
218+
219+
const isSquareWave = 11 <= wave && wave <= 15;
220+
221+
if (!oscillators[wave]) {
222+
oscillators[wave] = getGenerator(this.context, wave, startFrequency);
223+
gains[wave] = this.context.createGain();
224+
gains[wave].gain.value = 0;
225+
gains[wave].connect(this.gain);
226+
oscillators[wave].connect(gains[wave]);
227+
oscillators[wave].start();
228+
}
229+
230+
if (currentWave && wave !== currentWave) {
231+
gains[currentWave].gain.setTargetAtTime(0, currentTime, 0.015);
232+
}
233+
234+
const osc = oscillators[wave];
235+
const gain = gains[wave];
236+
237+
if (osc instanceof OscillatorNode) {
238+
osc.frequency.setValueAtTime(startFrequency, currentTime);
239+
osc.frequency.linearRampToValueAtTime(endFrequency, currentTime + duration);
240+
}
241+
else {
242+
const isFilteredNoise = wave == 4 || (16 <= wave && wave <= 18);
243+
244+
if (isFilteredNoise)
245+
osc.playbackRate.linearRampToValueAtTime(endFrequency / (this.context.sampleRate / 4), currentTime + duration);
246+
else if (wave != 5)
247+
osc.playbackRate.linearRampToValueAtTime(endFrequency / (this.context.sampleRate / 1024), currentTime + duration);
248+
}
249+
gain.gain.setValueAtTime(scaleVol(startVolume, isSquareWave), currentTime);
250+
gain.gain.linearRampToValueAtTime(scaleVol(endVolume, isSquareWave), currentTime + duration);
251+
252+
currentWave = wave;
253+
currentTime += duration;
254+
}
255+
this.gain.gain.setTargetAtTime(0, currentTime, 0.015);
256+
257+
if (isCancelled || onPull) {
258+
const handleAnimationFrame = () => {
259+
const time = this.context.currentTime;
260+
if (time > startTime + totalDuration) {
261+
return;
262+
}
263+
264+
if ((isCancelled && isCancelled()) || this.isDisposed()) {
265+
disconnectNodes();
266+
return;
267+
}
268+
269+
const { frequency, volume } = findFrequencyAndVolumeAtTime((time - startTime) * 1000, instructions);
270+
if (onPull) onPull(frequency, volume / 1024);
271+
272+
requestAnimationFrame(handleAnimationFrame)
273+
}
274+
requestAnimationFrame(handleAnimationFrame);
275+
}
276+
277+
await U.delay(totalDuration * 1000)
278+
disconnectNodes();
279+
});
280+
}
281+
282+
dispose(): void {
283+
if (this.isDisposed()) return;
284+
super.dispose();
285+
this.analyser.disconnect();
286+
this.gain.disconnect();
287+
}
288+
}
289+
290+
291+
function readUint16(buf: Uint8Array, offset: number) {
292+
const temp = new Uint8Array(2);
293+
temp[0] = buf[offset];
294+
temp[1] = buf[offset + 1];
295+
return new Uint16Array(temp.buffer)[0];
296+
}
297+
298+
function findFrequencyAndVolumeAtTime(millis: number, instructions: Uint8Array) {
299+
let currentTime = 0;
300+
301+
for (let i = 0; i < instructions.length; i += 12) {
302+
const startFrequency = readUint16(instructions, i + 2);
303+
const duration = readUint16(instructions, i + 4);
304+
const startVolume = readUint16(instructions, i + 6);
305+
const endVolume = readUint16(instructions, i + 8);
306+
const endFrequency = readUint16(instructions, i + 10);
307+
308+
if (currentTime + duration < millis) {
309+
currentTime += duration;
310+
continue;
311+
}
312+
313+
const offset = (millis - currentTime) / duration;
314+
315+
return {
316+
frequency: startFrequency + (endFrequency - startFrequency) * offset,
317+
volume: startVolume + (endVolume - startVolume) * offset,
318+
}
319+
}
320+
321+
return {
322+
frequency: -1,
323+
volume: -1
324+
};
325+
}
326+
}

0 commit comments

Comments
 (0)