Use AudioWorklet where available (#82)

Use AudioWorklet where available
This commit is contained in:
Will Scullin 2021-06-25 15:38:35 -07:00 committed by GitHub
parent 8087294456
commit b4c13d7620
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 76 deletions

View File

@ -82,6 +82,8 @@ let io: Apple2IO;
let _currentDrive: DriveNumber = 1;
let _e: boolean;
let ready: Promise<[void, void]>;
export const driveLights = new DriveLights();
export function dumpAppleSoftProgram() {
@ -213,7 +215,7 @@ function loadingStop() {
MicroModal.close('loading-modal');
if (!paused) {
_apple2.ready.then(() => {
ready.then(() => {
_apple2.run();
}).catch(console.error);
}
@ -762,7 +764,7 @@ export function updateUI() {
export function pauseRun() {
const label = document.querySelector<HTMLElement>('#pause-run i')!;
if (paused) {
_apple2.ready.then(() => {
ready.then(() => {
_apple2.run();
}).catch(console.error);
label.classList.remove('fa-play');
@ -831,6 +833,8 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
optionsModal.addOptions(audio);
initSoundToggle();
ready = Promise.all([audio.ready, apple2.ready]);
MicroModal.init();
keyboard = new KeyBoard(cpu, io, e);
@ -859,7 +863,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
event.preventDefault();
});
window.addEventListener('copy', (event) => {
window.addEventListener('copy', (event: Event) => {
event.clipboardData!.setData('text/plain', vm.getText());
event.preventDefault();
});
@ -879,7 +883,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
_apple2.stop();
processHash(hash);
} else {
_apple2.ready.then(() => {
ready.then(() => {
_apple2.run();
}).catch(console.error);
}

View File

@ -17,6 +17,7 @@ import { debug } from '../util';
* Audio Handling
*/
const QUANTUM_SIZE = 128;
const SAMPLE_SIZE = 1024;
const SAMPLE_RATE = 44000;
@ -36,45 +37,66 @@ export class Audio implements OptionHandler {
private audioContext;
private audioNode;
private workletNode: AudioWorkletNode;
private started = false;
ready: Promise<void>;
constructor(io: Apple2IO) {
this.audioContext = new AudioContext({
sampleRate: SAMPLE_RATE
});
// TODO(flan): MDN says that createScriptProcessor is deprecated and
// replaced by AudioWorklet. FF and Chrome support AudioWorklet, but
// Safari does not (yet).
this.audioNode = this.audioContext.createScriptProcessor(SAMPLE_SIZE, 1, 1);
if (window.AudioWorklet) {
this.ready = this.audioContext.audioWorklet.addModule('./dist/audio_worker.bundle.js');
this.ready
.then(() => {
this.workletNode = new AudioWorkletNode(this.audioContext, 'audio_worker');
this.audioNode.onaudioprocess = (event) => {
const data = event.outputBuffer.getChannelData(0);
const sample = this.samples.shift();
let idx = 0;
let len = data.length;
io.sampleRate(this.audioContext.sampleRate, QUANTUM_SIZE);
io.addSampleListener((sample) => {
if (this.sound && this.audioContext.state === 'running') {
this.workletNode.port.postMessage(sample);
}
});
this.workletNode.connect(this.audioContext.destination);
})
.catch(console.error);
} else {
// TODO(flan): MDN says that createScriptProcessor is deprecated and
// replaced by AudioWorklet. FF and Chrome support AudioWorklet, but
// Safari does not (yet).
this.audioNode = this.audioContext.createScriptProcessor(SAMPLE_SIZE, 1, 1);
if (sample) {
len = Math.min(sample.length, len);
for (; idx < len; idx++) {
data[idx] = sample[idx];
this.audioNode.onaudioprocess = (event) => {
const data = event.outputBuffer.getChannelData(0);
const sample = this.samples.shift();
let idx = 0;
let len = data.length;
if (sample) {
len = Math.min(sample.length, len);
for (; idx < len; idx++) {
data[idx] = sample[idx];
}
}
}
for (; idx < data.length; idx++) {
data[idx] = 0.0;
}
};
this.audioNode.connect(this.audioContext.destination);
io.sampleRate(this.audioContext.sampleRate, SAMPLE_SIZE);
io.addSampleListener((sample: number[]) => {
if (this.sound) {
if (this.samples.length < 5) {
this.samples.push(sample);
for (; idx < data.length; idx++) {
data[idx] = 0.0;
}
}
});
};
this.audioNode.connect(this.audioContext.destination);
io.sampleRate(this.audioContext.sampleRate, SAMPLE_SIZE);
io.addSampleListener((sample) => {
if (this.sound && this.audioContext.state === 'running') {
if (this.samples.length < 5) {
this.samples.push(sample);
}
}
});
this.ready = Promise.resolve();
}
window.addEventListener('keydown', this.autoStart);
if (window.ontouchstart !== undefined) {
@ -85,7 +107,6 @@ export class Audio implements OptionHandler {
debug('Sound initialized');
}
autoStart = () => {
if (this.audioContext && !this.started) {
this.samples = [];

58
js/ui/audio_worker.ts Normal file
View File

@ -0,0 +1,58 @@
/* Copyright 2021 Will Scullin <scullin@scullinsteel.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
declare global {
interface AudioWorkletProcessor {
readonly port: MessagePort;
process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Map<string, Float32Array>): void;
}
const AudioWorkletProcessor: {
prototype: AudioWorkletProcessor;
new(options?: AudioWorkletNodeOptions): AudioWorkletProcessor;
};
function registerProcessor(name: string, ctor :{ new(): AudioWorkletProcessor; }): void
}
export class AppleAudioProcessor extends AudioWorkletProcessor {
private samples: Float32Array[] = []
constructor() {
super();
console.info('AppleAudioProcessor constructor');
this.port.onmessage = (ev: MessageEvent) => {
this.samples.push(ev.data);
if (this.samples.length > 256) {
this.samples.shift();
}
};
}
static get parameterDescriptors() {
return [];
}
process(_inputList: Float32Array[][], outputList: Float32Array[][], _parameters: Map<string, Float32Array>) {
const sample = this.samples.shift();
const output = outputList[0];
if (sample) {
for (let idx = 0; idx < sample.length; idx++) {
output[0][idx] = sample[idx];
}
}
// Keep alive indefinitely.
return true;
}
}
registerProcessor('audio_worker', AppleAudioProcessor);

View File

@ -1,53 +1,10 @@
const path = require('path');
module.exports =
{
const baseConfig = {
devtool: 'source-map',
mode: 'development',
entry: {
main2: path.resolve('js/entry2.js'),
main2e: path.resolve('js/entry2e.js')
},
output: {
path: path.resolve('dist/'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
library: {
name: 'Apple2',
type: 'umd',
export: 'Apple2',
},
},
devServer: {
compress: true,
static: {
watch: {
ignored: /(node_modules|test|\.git)/
},
directory: __dirname,
},
dev: {
publicPath: '/dist/',
},
},
module: {
rules: [
{
test: /\.2mg$/i,
use: [
{
loader: 'file-loader',
},
],
},
{
test: /\.rom$/i,
use: [
{
loader: 'raw-loader',
},
],
},
{
test: /\.ts$/i,
use: [
@ -63,3 +20,48 @@ module.exports =
extensions: ['.ts', '.js'],
},
};
module.exports = [
{
...baseConfig,
entry: {
main2: path.resolve('js/entry2.js'),
main2e: path.resolve('js/entry2e.js')
},
output: {
path: path.resolve('dist/'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
library: {
name: 'Apple2',
type: 'umd',
export: 'Apple2',
},
},
devServer: {
compress: true,
static: {
watch: {
ignored: /(node_modules|test|\.git)/
},
directory: __dirname,
},
dev: {
publicPath: '/dist/',
},
},
},
{
...baseConfig,
target: false,
entry: {
audio_worker: path.resolve('js/ui/audio_worker.ts')
},
output: {
publicPath: '/dist/',
path: path.resolve('dist/'),
filename: '[name].bundle.js',
globalObject: 'globalThis',
},
},
];