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 _currentDrive: DriveNumber = 1;
|
||||||
let _e: boolean;
|
let _e: boolean;
|
||||||
|
|
||||||
|
let ready: Promise<[void, void]>;
|
||||||
|
|
||||||
export const driveLights = new DriveLights();
|
export const driveLights = new DriveLights();
|
||||||
|
|
||||||
export function dumpAppleSoftProgram() {
|
export function dumpAppleSoftProgram() {
|
||||||
|
@ -213,7 +215,7 @@ function loadingStop() {
|
||||||
MicroModal.close('loading-modal');
|
MicroModal.close('loading-modal');
|
||||||
|
|
||||||
if (!paused) {
|
if (!paused) {
|
||||||
_apple2.ready.then(() => {
|
ready.then(() => {
|
||||||
_apple2.run();
|
_apple2.run();
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
@ -762,7 +764,7 @@ export function updateUI() {
|
||||||
export function pauseRun() {
|
export function pauseRun() {
|
||||||
const label = document.querySelector<HTMLElement>('#pause-run i')!;
|
const label = document.querySelector<HTMLElement>('#pause-run i')!;
|
||||||
if (paused) {
|
if (paused) {
|
||||||
_apple2.ready.then(() => {
|
ready.then(() => {
|
||||||
_apple2.run();
|
_apple2.run();
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
label.classList.remove('fa-play');
|
label.classList.remove('fa-play');
|
||||||
|
@ -831,6 +833,8 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
|
||||||
optionsModal.addOptions(audio);
|
optionsModal.addOptions(audio);
|
||||||
initSoundToggle();
|
initSoundToggle();
|
||||||
|
|
||||||
|
ready = Promise.all([audio.ready, apple2.ready]);
|
||||||
|
|
||||||
MicroModal.init();
|
MicroModal.init();
|
||||||
|
|
||||||
keyboard = new KeyBoard(cpu, io, e);
|
keyboard = new KeyBoard(cpu, io, e);
|
||||||
|
@ -859,7 +863,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('copy', (event) => {
|
window.addEventListener('copy', (event: Event) => {
|
||||||
event.clipboardData!.setData('text/plain', vm.getText());
|
event.clipboardData!.setData('text/plain', vm.getText());
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
@ -879,7 +883,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
|
||||||
_apple2.stop();
|
_apple2.stop();
|
||||||
processHash(hash);
|
processHash(hash);
|
||||||
} else {
|
} else {
|
||||||
_apple2.ready.then(() => {
|
ready.then(() => {
|
||||||
_apple2.run();
|
_apple2.run();
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { debug } from '../util';
|
||||||
* Audio Handling
|
* Audio Handling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const QUANTUM_SIZE = 128;
|
||||||
const SAMPLE_SIZE = 1024;
|
const SAMPLE_SIZE = 1024;
|
||||||
const SAMPLE_RATE = 44000;
|
const SAMPLE_RATE = 44000;
|
||||||
|
|
||||||
|
@ -36,45 +37,66 @@ export class Audio implements OptionHandler {
|
||||||
|
|
||||||
private audioContext;
|
private audioContext;
|
||||||
private audioNode;
|
private audioNode;
|
||||||
|
private workletNode: AudioWorkletNode;
|
||||||
private started = false;
|
private started = false;
|
||||||
|
|
||||||
|
ready: Promise<void>;
|
||||||
|
|
||||||
constructor(io: Apple2IO) {
|
constructor(io: Apple2IO) {
|
||||||
this.audioContext = new AudioContext({
|
this.audioContext = new AudioContext({
|
||||||
sampleRate: SAMPLE_RATE
|
sampleRate: SAMPLE_RATE
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO(flan): MDN says that createScriptProcessor is deprecated and
|
if (window.AudioWorklet) {
|
||||||
// replaced by AudioWorklet. FF and Chrome support AudioWorklet, but
|
this.ready = this.audioContext.audioWorklet.addModule('./dist/audio_worker.bundle.js');
|
||||||
// Safari does not (yet).
|
this.ready
|
||||||
this.audioNode = this.audioContext.createScriptProcessor(SAMPLE_SIZE, 1, 1);
|
.then(() => {
|
||||||
|
this.workletNode = new AudioWorkletNode(this.audioContext, 'audio_worker');
|
||||||
|
|
||||||
this.audioNode.onaudioprocess = (event) => {
|
io.sampleRate(this.audioContext.sampleRate, QUANTUM_SIZE);
|
||||||
const data = event.outputBuffer.getChannelData(0);
|
io.addSampleListener((sample) => {
|
||||||
const sample = this.samples.shift();
|
if (this.sound && this.audioContext.state === 'running') {
|
||||||
let idx = 0;
|
this.workletNode.port.postMessage(sample);
|
||||||
let len = data.length;
|
}
|
||||||
|
});
|
||||||
|
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) {
|
this.audioNode.onaudioprocess = (event) => {
|
||||||
len = Math.min(sample.length, len);
|
const data = event.outputBuffer.getChannelData(0);
|
||||||
for (; idx < len; idx++) {
|
const sample = this.samples.shift();
|
||||||
data[idx] = sample[idx];
|
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++) {
|
for (; idx < data.length; idx++) {
|
||||||
data[idx] = 0.0;
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
});
|
|
||||||
|
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);
|
window.addEventListener('keydown', this.autoStart);
|
||||||
if (window.ontouchstart !== undefined) {
|
if (window.ontouchstart !== undefined) {
|
||||||
|
@ -85,7 +107,6 @@ export class Audio implements OptionHandler {
|
||||||
debug('Sound initialized');
|
debug('Sound initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
autoStart = () => {
|
autoStart = () => {
|
||||||
if (this.audioContext && !this.started) {
|
if (this.audioContext && !this.started) {
|
||||||
this.samples = [];
|
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');
|
const path = require('path');
|
||||||
|
|
||||||
module.exports =
|
const baseConfig = {
|
||||||
{
|
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
mode: 'development',
|
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: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
|
||||||
test: /\.2mg$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.rom$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'raw-loader',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.ts$/i,
|
test: /\.ts$/i,
|
||||||
use: [
|
use: [
|
||||||
|
@ -63,3 +20,48 @@ module.exports =
|
||||||
extensions: ['.ts', '.js'],
|
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