mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Use AudioWorklet where available (#82)
Use AudioWorklet where available
This commit is contained in:
parent
8087294456
commit
b4c13d7620
@ -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);
|
||||
}
|
||||
|
@ -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
58
js/ui/audio_worker.ts
Normal 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);
|
@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user