Skip to content

Commit

Permalink
[WIP] AudioWorklet abandonment checkpoint
Browse files Browse the repository at this point in the history
Changes to Player, Sequencer, App, PlayerParams.

PlayerParams disabled. Visualizer disabled.
This is far from working and still glitchy.

All the player implementation updates for AudioWorklet compatibility also depend on these changes in Player.js and Sequencer.js.

Some changes in this commit that are independent from AudioWorklet:

  App.js:
  this.sequencer.on('sequencerStateUpdate', this.handleSequencerStateUpdate);
  this.sequencer.on('playerError', this.handlePlayerError);

audioNode.onaudioprocess = (e) => {
            const channels = [];
            for (let channel = 0; channel < e.outputBuffer.numberOfChannels; channel++) {
              channels[channel] = e.outputBuffer.getChannelData(channel);
            }
            for (let player of players) {
              // if (player.isPaused()) continue;
              player.process(channels);
            }
            // players[4].process(channels);
          }

   ...

     mapSequencerStateToAppState()

   ...

     handleSequencerStateUpdate(sequencerState) {
       const { isEjected } = sequencerState;
  • Loading branch information
mmontag committed Jan 2, 2022
1 parent 2fc3300 commit 0438941
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 85 deletions.
4 changes: 2 additions & 2 deletions scripts/build-chip-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +455,12 @@ const flags = [
'-s', 'ASSERTIONS=0', // assertions increase runtime size about 100K
'-s', 'MODULARIZE=1',
'-s', 'EXPORT_NAME=CHIP_CORE',
'-s', 'ENVIRONMENT=web',
'-s', 'ENVIRONMENT=worker',
'-s', 'USE_ZLIB=1',
'-s', 'EXPORT_ES6=1',
'-s', 'USE_ES6_IMPORT_META=0',
'-lidbfs.js',
'-Os', // set to O0 for fast compile during development
'-O0', // set to O0 for fast compile during development
'-o', jsOutFile,

/*
Expand Down
163 changes: 138 additions & 25 deletions src/Sequencer.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import promisify from "./promisify-xhr";
import {CATALOG_PREFIX} from "./config";
// import promisify from "./promisify-xhr";
import { CATALOG_PREFIX } from './config';
import EventEmitter from 'events';
import * as Comlink from 'comlink';

// import { EventEmitter } from 'quackamole-event-emitter';
// import { EventEmitter } from '@billjs/event-emitter';
// import * as EventEmitter from 'event-emitter';
// import EventEmitter from 'eventemitter3';

export const REPEAT_OFF = 0;
export const REPEAT_ALL = 1;
export const REPEAT_ONE = 2;
export const NUM_REPEAT_MODES = 3;
export const REPEAT_LABELS = ['Off', 'All', 'One'];

export default class Sequencer {
constructor(players, onSequencerStateUpdate, onError) {
export default class Sequencer extends EventEmitter {
constructor(players, fetch) {
super();

this.playSong = this.playSong.bind(this);
this.playSongBuffer = this.playSongBuffer.bind(this);
this.playSongFile = this.playSongFile.bind(this);
Expand All @@ -26,10 +35,9 @@ export default class Sequencer {
this.getCurrIdx = this.getCurrIdx.bind(this);
this.setShuffle = this.setShuffle.bind(this);

this.fetch = fetch;
this.player = null;
this.players = players;
this.onSequencerStateUpdate = onSequencerStateUpdate;
this.onPlayerError = onError;

this.currIdx = 0;
this.context = null;
Expand All @@ -38,20 +46,49 @@ export default class Sequencer {
this.songRequest = null;
this.repeat = REPEAT_OFF;

this.players.forEach(player => {
player.on('playerStateUpdate', this.onPlayerStateUpdate);
});
// Inside AudioWorklet
// this.players.forEach(player => {
// player.on('playerStateUpdate', this.onPlayerStateUpdate);
// });

// Outside AudioWorklet
setTimeout(() => {
this.players.forEach(player => {
player.on('playerStateUpdate', Comlink.proxy((e) => {
this.onPlayerStateUpdate(e)
}));
});
}, 2000);

// EventEmitter + Comlink workaround: drop .on() return value
// TODO: this can be removed when Sequencer is moved outside of the AudioWorklet
const _on = this.on;
this.on = (...args) => {
_on.apply(this, args);
};
}

onPlayerStateUpdate(isStopped) {
onPlayerStateUpdate(playerState) {
const { isStopped } = playerState;
console.debug('Sequencer.onPlayerStateUpdate(isStopped=%s)', isStopped);
if (isStopped) {
this.currUrl = null;
if (this.context) {
this.nextSong();
}
} else {
this.onSequencerStateUpdate(false);
this.emit('sequencerStateUpdate', {
// metadata: this.getMetadata(),
// numVoices: this.getNumVoices(),
// durationMs: this.getDurationMs(),
// paramDefs: this.getParamDefs(),
...playerState,
url: this.currUrl,
isPaused: false,
hasPlayer: true,
// TODO: combine isEjected and hasPlayer
isEjected: false,
});
}
}

Expand Down Expand Up @@ -94,7 +131,7 @@ export default class Sequencer {
this.currIdx = 0;
this.context = null;
this.player.stop();
this.onSequencerStateUpdate(true);
this.emit('sequencerStateUpdate', { isEjected: true });
}
} else {
this.playSong(this.context[this.currIdx]);
Expand Down Expand Up @@ -141,7 +178,7 @@ export default class Sequencer {
return this.currUrl;
}

playSong(url) {
async playSong(url) {
if (this.player !== null) {
this.player.suspend();
}
Expand All @@ -152,36 +189,49 @@ export default class Sequencer {
// Find a player that can play this filetype
const ext = url.split('.').pop().toLowerCase();
for (let i = 0; i < this.players.length; i++) {
if (this.players[i].canPlay(ext)) {
if (await this.players[i].canPlay(ext)) {
this.player = this.players[i];
break;
}
}
if (this.player === null) {
this.onPlayerError(`The file format ".${ext}" was not recognized.`);
this.emit('playerError', `The file format ".${ext}" was not recognized.`);
return;
}

// Fetch the song file (cancelable request)
// Cancel any outstanding request so that playback doesn't happen out of order
if (this.songRequest) this.songRequest.abort();
this.songRequest = promisify(new XMLHttpRequest());
this.songRequest.responseType = 'arraybuffer';
this.songRequest.open('GET', url);
this.songRequest.send()
.then(xhr => xhr.response)
// if (this.songRequest) this.songRequest.abort();
// this.songRequest = promisify(new XMLHttpRequest());
// this.songRequest.responseType = 'arraybuffer';
// this.songRequest.open('GET', url);
// this.songRequest.send()
// .then(xhr => xhr.response)
this.fetch(url)
.then(buffer => {
this.currUrl = url;
const filepath = url.replace(CATALOG_PREFIX, '');
this.playSongBuffer(filepath, buffer)
this.playSongBuffer(filepath, buffer);
// TODO: listen for playerstateupdate and bubble as seqStateUpd.
// this.emit('sequencerStateUpdate', {
// url,
// isPaused: false,
// metadata: this.getMetadata(),
// numVoices: this.getNumVoices(),
// durationMs: this.getDurationMs(),
// paramDefs: this.getParamDefs(),
// hasPlayer: true,
// // TODO: combine isEjected and hasPlayer
// isEjected: false,
// });
})
.catch(e => {
// TODO: recover from this error
this.onSequencerStateUpdate(true);
this.emit('sequencerStateUpdate', { isEjected: true });
this.player = null;
const message = e.message || `${e.status} ${e.statusText}`;
console.error(e);
this.onPlayerError(message);
this.emit('playerError', message);
});
}

Expand All @@ -200,7 +250,7 @@ export default class Sequencer {
}
}
if (this.player === null) {
this.onPlayerError(`The file format ".${ext}" was not recognized.`);
this.emit('playerError', `The file format ".${ext}" was not recognized.`);
return;
}

Expand All @@ -219,4 +269,67 @@ export default class Sequencer {

console.debug('Sequencer.playSong(...) song request completed');
}

hasPlayer = () => {
return this.player !== null;
}
getDurationMs = () => {
if (this.player) return Promise.resolve(this.player.getDurationMs());
}
getMetadata = () => {
if (this.player) return Promise.resolve(this.player.getMetadata());
}
getNumSubtunes = () => {
if (this.player) return Promise.resolve(this.player.getNumSubtunes());
}
getNumVoices = () => {
if (this.player) return Promise.resolve(this.player.getNumVoices());
}
getParamDefs = () => {
if (this.player) return Promise.resolve(this.player.getParamDefs());
}
getParameter = (id) => {
if (this.player) return Promise.resolve(this.player.getParameter(id));
}
getPositionMs = () => {
if (this.player) return Promise.resolve(this.player.getPositionMs());
}
getSubtune = () => {
if (this.player) return Promise.resolve(this.player.getSubtune());
}
getTempo = () => {
if (this.player) return Promise.resolve(this.player.getTempo());
}
getVoiceName = (idx) => {
if (this.player) return Promise.resolve(this.player.getVoiceName(idx));
}
getVoiceMask = () => {
if (this.player) return Promise.resolve(this.player.getVoiceMask());
}
isPaused = () => {
if (this.player) return Promise.resolve(this.player.isPaused());
}
isPlaying = () => {
if (this.player) return Promise.resolve(this.player.isPlaying());
}

process = (output) => {
if (this.player) return Promise.resolve(this.player.process(output));
}

seekMs = (ms) => {
if (this.player) return Promise.resolve(this.player.seekMs(ms));
}
setParameter = (id, value) => {
if (this.player) return Promise.resolve(this.player.setParameter(id, value));
}
setTempo = (tempo) => {
if (this.player) return Promise.resolve(this.player.setTempo(tempo));
}
setVoiceMask = (mask) => {
if (this.player) return Promise.resolve(this.player.setVoiceMask(mask));
}
togglePause = () => {
if (this.player) return Promise.resolve(this.player.togglePause());
}
}
Loading

0 comments on commit 0438941

Please sign in to comment.