Hack timidity to allow for access to the audio context/destination

This commit is contained in:
Jordan Eldredge 2019-09-06 06:35:43 -07:00
parent 49b94f956c
commit db76cef239
3 changed files with 429 additions and 439 deletions

View file

@ -1,402 +1,411 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Timidity = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
const Debug = require('debug')
const EventEmitter = require('events').EventEmitter
const Debug = require("debug");
const EventEmitter = require("events").EventEmitter;
const LibTimidity = require('./libtimidity')
const LibTimidity = require("./libtimidity");
const debug = Debug('timidity')
const debugVerbose = Debug('timidity:verbose')
const debug = Debug("timidity");
const debugVerbose = Debug("timidity:verbose");
// Inlined at build time by 'brfs' browserify transform
const TIMIDITY_CFG = "\ndrumset 0\n\n 25\tDrum_000/025_Snare_Roll.pat \n 26\tDrum_000/026_Snap.pat \n 27\tDrum_000/027_High_Q.pat \n 31\tDrum_000/031_Sticks.pat \n 32\tDrum_000/032_Square_Click.pat \n 33\tDrum_000/033_Metronome_Click.pat \n 34\tDrum_000/034_Metronome_Bell.pat \n 35\tDrum_000/035_Kick_1.pat amp=100\n 36\tDrum_000/036_Kick_2.pat amp=100\n 37\tDrum_000/037_Stick_Rim.pat \n 38\tDrum_000/038_Snare_1.pat \n 39\tDrum_000/039_Clap_Hand.pat amp=100\n 40\tDrum_000/040_Snare_2.pat \n 41\tDrum_000/041_Tom_Low_2.pat amp=100\n 42\tDrum_000/042_Hi-Hat_Closed.pat \n 43\tDrum_000/043_Tom_Low_1.pat amp=100\n 44\tDrum_000/044_Hi-Hat_Pedal.pat \n 45\tDrum_000/045_Tom_Mid_2.pat amp=100\n 46\tDrum_000/046_Hi-Hat_Open.pat \n 47\tDrum_000/047_Tom_Mid_1.pat amp=100\n 48\tDrum_000/048_Tom_High_2.pat amp=100\n 49\tDrum_000/049_Cymbal_Crash_1.pat \n 50\tDrum_000/050_Tom_High_1.pat amp=100\n 51\tDrum_000/051_Cymbal_Ride_1.pat \n 52\tDrum_000/052_Cymbal_Chinese.pat \n 53\tDrum_000/053_Cymbal_Ride_Bell.pat amp=100\n 54\tDrum_000/054_Tombourine.pat \n 55\tDrum_000/055_Cymbal_Splash.pat \n 56\tDrum_000/056_Cow_Bell.pat \n 57\tDrum_000/057_Cymbal_Crash_2.pat \n 58\tDrum_000/058_Vibra-Slap.pat \n 59\tDrum_000/059_Cymbal_Ride_2.pat \n 60\tDrum_000/060_Bongo_High.pat \n 61\tDrum_000/061_Bongo_Low.pat \n 62\tDrum_000/062_Conga_High_1_Mute.pat \n 63\tDrum_000/063_Conga_High_2_Open.pat \n 64\tDrum_000/064_Conga_Low.pat \n 65\tDrum_000/065_Timbale_High.pat \n 66\tDrum_000/066_Timbale_Low.pat \n 67\tDrum_000/067_Agogo_High.pat \n 68\tDrum_000/068_Agogo_Low.pat \n 69\tDrum_000/069_Cabasa.pat amp=100\n 70\tDrum_000/070_Maracas.pat \n 71\tDrum_000/071_Whistle_1_High_Short.pat \n 72\tDrum_000/072_Whistle_2_Low_Long.pat \n 73\tDrum_000/073_Guiro_1_Short.pat \n 74\tDrum_000/074_Guiro_2_Long.pat \n 75\tDrum_000/075_Claves.pat amp=100\n 76\tDrum_000/076_Wood_Block_1_High.pat \n 77\tDrum_000/077_Wood_Block_2_Low.pat \n 78\tDrum_000/078_Cuica_1_Mute.pat amp=100\n 79\tDrum_000/079_Cuica_2_Open.pat amp=100\n 80\tDrum_000/080_Triangle_1_Mute.pat \n 81\tDrum_000/081_Triangle_2_Open.pat \n 82\tDrum_000/082_Shaker.pat \n 84\tDrum_000/084_Belltree.pat \n\nbank 0\n\n 0\tTone_000/000_Acoustic_Grand_Piano.pat amp=120 pan=center\n 1\tTone_000/001_Acoustic_Brite_Piano.pat \n 2\tTone_000/002_Electric_Grand_Piano.pat \n 4\tTone_000/004_Electric_Piano_1_Rhodes.pat \n 5\tTone_000/005_Electric_Piano_2_Chorused_Yamaha_DX.pat \n 6\tTone_000/006_Harpsichord.pat \n 7\tTone_000/007_Clavinet.pat \n 8\tTone_000/008_Celesta.pat \n 9\tTone_000/009_Glockenspiel.pat \n 13\tTone_000/013_Xylophone.pat \n 14\tTone_000/014_Tubular_Bells.pat \n 15\tTone_000/015_Dulcimer.pat \n 16\tTone_000/016_Hammond_Organ.pat \n 19\tTone_000/019_Church_Organ.pat \n 21\tTone_000/021_Accordion.pat \n 23\tTone_000/023_Tango_Accordion.pat \n 24\tTone_000/024_Nylon_Guitar.pat \n 25\tTone_000/025_Steel_Guitar.pat \n 26\tTone_000/026_Jazz_Guitar.pat \n 27\tTone_000/027_Clean_Electric_Guitar.pat \n 28\tTone_000/028_Muted_Electric_Guitar.pat \n 29\tTone_000/029_Overdriven_Guitar.pat \n 30\tTone_000/030_Distortion_Guitar.pat \n 32\tTone_000/032_Acoustic_Bass.pat \n 33\tTone_000/033_Finger_Bass.pat \n 34\tTone_000/034_Pick_Bass.pat \n 35\tTone_000/035_Fretless_Bass.pat \n 36\tTone_000/036_Slap_Bass_1.pat \n 37\tTone_000/037_Slap_Bass_2.pat \n 38\tTone_000/038_Synth_Bass_1.pat \n 40\tTone_000/040_Violin.pat \n 42\tTone_000/042_Cello.pat \n 44\tTone_000/044_Tremolo_Strings.pat \n 45\tTone_000/045_Pizzicato_Strings.pat \n 46\tTone_000/046_Harp.pat \n 47\tTone_000/047_Timpani.pat \n 48\tTone_000/048_String_Ensemble_1_Marcato.pat \n 53\tTone_000/053_Voice_Oohs.pat \n 56\tTone_000/056_Trumpet.pat \n 57\tTone_000/057_Trombone.pat \n 58\tTone_000/058_Tuba.pat \n 59\tTone_000/059_Muted_Trumpet.pat \n 60\tTone_000/060_French_Horn.pat \n 61\tTone_000/061_Brass_Section.pat \n 64\tTone_000/064_Soprano_Sax.pat \n 65\tTone_000/065_Alto_Sax.pat \n 66\tTone_000/066_Tenor_Sax.pat \n 67\tTone_000/067_Baritone_Sax.pat \n 68\tTone_000/068_Oboe.pat \n 69\tTone_000/069_English_Horn.pat \n 70\tTone_000/070_Bassoon.pat \n 71\tTone_000/071_Clarinet.pat \n 72\tTone_000/072_Piccolo.pat \n 73\tTone_000/073_Flute.pat \n 74\tTone_000/074_Recorder.pat \n 75\tTone_000/075_Pan_Flute.pat \n 76\tTone_000/076_Bottle_Blow.pat \n 79\tTone_000/079_Ocarina.pat \n 80\tTone_000/080_Square_Wave.pat \n 84\tTone_000/084_Charang.pat \n 88\tTone_000/088_New_Age.pat \n 94\tTone_000/094_Halo_Pad.pat \n 95\tTone_000/095_Sweep_Pad.pat \n 98\tTone_000/098_Crystal.pat \n 101\tTone_000/101_Goblins--Unicorn.pat \n 102\tTone_000/102_Echo_Voice.pat \n 104\tTone_000/104_Sitar.pat \n 114\tTone_000/114_Steel_Drums.pat \n 115\tTone_000/115_Wood_Block.pat \n 120\tTone_000/120_Guitar_Fret_Noise.pat \n 122\tTone_000/122_Seashore.pat \n 125\tTone_000/125_Helicopter.pat \n\n"
const TIMIDITY_CFG = "\ndrumset 0\n\n 25\tDrum_000/025_Snare_Roll.pat \n 26\tDrum_000/026_Snap.pat \n 27\tDrum_000/027_High_Q.pat \n 31\tDrum_000/031_Sticks.pat \n 32\tDrum_000/032_Square_Click.pat \n 33\tDrum_000/033_Metronome_Click.pat \n 34\tDrum_000/034_Metronome_Bell.pat \n 35\tDrum_000/035_Kick_1.pat amp=100\n 36\tDrum_000/036_Kick_2.pat amp=100\n 37\tDrum_000/037_Stick_Rim.pat \n 38\tDrum_000/038_Snare_1.pat \n 39\tDrum_000/039_Clap_Hand.pat amp=100\n 40\tDrum_000/040_Snare_2.pat \n 41\tDrum_000/041_Tom_Low_2.pat amp=100\n 42\tDrum_000/042_Hi-Hat_Closed.pat \n 43\tDrum_000/043_Tom_Low_1.pat amp=100\n 44\tDrum_000/044_Hi-Hat_Pedal.pat \n 45\tDrum_000/045_Tom_Mid_2.pat amp=100\n 46\tDrum_000/046_Hi-Hat_Open.pat \n 47\tDrum_000/047_Tom_Mid_1.pat amp=100\n 48\tDrum_000/048_Tom_High_2.pat amp=100\n 49\tDrum_000/049_Cymbal_Crash_1.pat \n 50\tDrum_000/050_Tom_High_1.pat amp=100\n 51\tDrum_000/051_Cymbal_Ride_1.pat \n 52\tDrum_000/052_Cymbal_Chinese.pat \n 53\tDrum_000/053_Cymbal_Ride_Bell.pat amp=100\n 54\tDrum_000/054_Tombourine.pat \n 55\tDrum_000/055_Cymbal_Splash.pat \n 56\tDrum_000/056_Cow_Bell.pat \n 57\tDrum_000/057_Cymbal_Crash_2.pat \n 58\tDrum_000/058_Vibra-Slap.pat \n 59\tDrum_000/059_Cymbal_Ride_2.pat \n 60\tDrum_000/060_Bongo_High.pat \n 61\tDrum_000/061_Bongo_Low.pat \n 62\tDrum_000/062_Conga_High_1_Mute.pat \n 63\tDrum_000/063_Conga_High_2_Open.pat \n 64\tDrum_000/064_Conga_Low.pat \n 65\tDrum_000/065_Timbale_High.pat \n 66\tDrum_000/066_Timbale_Low.pat \n 67\tDrum_000/067_Agogo_High.pat \n 68\tDrum_000/068_Agogo_Low.pat \n 69\tDrum_000/069_Cabasa.pat amp=100\n 70\tDrum_000/070_Maracas.pat \n 71\tDrum_000/071_Whistle_1_High_Short.pat \n 72\tDrum_000/072_Whistle_2_Low_Long.pat \n 73\tDrum_000/073_Guiro_1_Short.pat \n 74\tDrum_000/074_Guiro_2_Long.pat \n 75\tDrum_000/075_Claves.pat amp=100\n 76\tDrum_000/076_Wood_Block_1_High.pat \n 77\tDrum_000/077_Wood_Block_2_Low.pat \n 78\tDrum_000/078_Cuica_1_Mute.pat amp=100\n 79\tDrum_000/079_Cuica_2_Open.pat amp=100\n 80\tDrum_000/080_Triangle_1_Mute.pat \n 81\tDrum_000/081_Triangle_2_Open.pat \n 82\tDrum_000/082_Shaker.pat \n 84\tDrum_000/084_Belltree.pat \n\nbank 0\n\n 0\tTone_000/000_Acoustic_Grand_Piano.pat amp=120 pan=center\n 1\tTone_000/001_Acoustic_Brite_Piano.pat \n 2\tTone_000/002_Electric_Grand_Piano.pat \n 4\tTone_000/004_Electric_Piano_1_Rhodes.pat \n 5\tTone_000/005_Electric_Piano_2_Chorused_Yamaha_DX.pat \n 6\tTone_000/006_Harpsichord.pat \n 7\tTone_000/007_Clavinet.pat \n 8\tTone_000/008_Celesta.pat \n 9\tTone_000/009_Glockenspiel.pat \n 13\tTone_000/013_Xylophone.pat \n 14\tTone_000/014_Tubular_Bells.pat \n 15\tTone_000/015_Dulcimer.pat \n 16\tTone_000/016_Hammond_Organ.pat \n 19\tTone_000/019_Church_Organ.pat \n 21\tTone_000/021_Accordion.pat \n 23\tTone_000/023_Tango_Accordion.pat \n 24\tTone_000/024_Nylon_Guitar.pat \n 25\tTone_000/025_Steel_Guitar.pat \n 26\tTone_000/026_Jazz_Guitar.pat \n 27\tTone_000/027_Clean_Electric_Guitar.pat \n 28\tTone_000/028_Muted_Electric_Guitar.pat \n 29\tTone_000/029_Overdriven_Guitar.pat \n 30\tTone_000/030_Distortion_Guitar.pat \n 32\tTone_000/032_Acoustic_Bass.pat \n 33\tTone_000/033_Finger_Bass.pat \n 34\tTone_000/034_Pick_Bass.pat \n 35\tTone_000/035_Fretless_Bass.pat \n 36\tTone_000/036_Slap_Bass_1.pat \n 37\tTone_000/037_Slap_Bass_2.pat \n 38\tTone_000/038_Synth_Bass_1.pat \n 40\tTone_000/040_Violin.pat \n 42\tTone_000/042_Cello.pat \n 44\tTone_000/044_Tremolo_Strings.pat \n 45\tTone_000/045_Pizzicato_Strings.pat \n 46\tTone_000/046_Harp.pat \n 47\tTone_000/047_Timpani.pat \n 48\tTone_000/048_String_Ensemble_1_Marcato.pat \n 53\tTone_000/053_Voice_Oohs.pat \n 56\tTone_000/056_Trumpet.pat \n 57\tTone_000/057_Trombone.pat \n 58\tTone_000/058_Tuba.pat \n 59\tTone_000/059_Muted_Trumpet.pat \n 60\tTone_000/060_French_Horn.pat \n 61\tTone_000/061_Brass_Section.pat \n 64\tTone_000/064_Soprano_Sax.pat \n 65\tTone_000/065_Alto_Sax.pat \n 66\tTone_000/066_Tenor_Sax.pat \n 67\tTone_000/067_Baritone_Sax.pat \n 68\tTone_000/068_Oboe.pat \n 69\tTone_000/069_English_Horn.pat \n 70\tTone_000/070_Bassoon.pat \n 71\tTone_000/071_Clarinet.pat \n 72\tTone_000/072_Piccolo.pat \n 73\tTone_000/073_Flute.pat \n 74\tTone_000/074_Recorder.pat \n 75\tTone_000/075_Pan_Flute.pat \n 76\tTone_000/076_Bottle_Blow.pat \n 79\tTone_000/079_Ocarina.pat \n 80\tTone_000/080_Square_Wave.pat \n 84\tTone_000/084_Charang.pat \n 88\tTone_000/088_New_Age.pat \n 94\tTone_000/094_Halo_Pad.pat \n 95\tTone_000/095_Sweep_Pad.pat \n 98\tTone_000/098_Crystal.pat \n 101\tTone_000/101_Goblins--Unicorn.pat \n 102\tTone_000/102_Echo_Voice.pat \n 104\tTone_000/104_Sitar.pat \n 114\tTone_000/114_Steel_Drums.pat \n 115\tTone_000/115_Wood_Block.pat \n 120\tTone_000/120_Guitar_Fret_Noise.pat \n 122\tTone_000/122_Seashore.pat \n 125\tTone_000/125_Helicopter.pat \n\n";
const SAMPLE_RATE = 44100
const AUDIO_FORMAT = 0x8010 // format of the rendered audio 's16'
const NUM_CHANNELS = 2 // stereo (2 channels)
const BYTES_PER_SAMPLE = 2 * NUM_CHANNELS
const BUFFER_SIZE = 16384 // buffer size for each render() call
const SAMPLE_RATE = 44100;
const AUDIO_FORMAT = 0x8010; // format of the rendered audio 's16'
const NUM_CHANNELS = 2; // stereo (2 channels)
const BYTES_PER_SAMPLE = 2 * NUM_CHANNELS;
const BUFFER_SIZE = 16384; // buffer size for each render() call
const AudioContext = typeof window !== 'undefined' &&
(window.AudioContext || window.webkitAudioContext)
const AudioContext =
typeof window !== "undefined" &&
(window.AudioContext || window.webkitAudioContext);
class Timidity extends EventEmitter {
constructor (baseUrl = '/') {
super()
constructor({ baseUrl = "/", audioContext, destination }) {
super();
this.destroyed = false
this.destroyed = false;
if (!baseUrl.endsWith('/')) baseUrl += '/'
this._baseUrl = new URL(baseUrl, window.location.origin).href
if (!baseUrl.endsWith("/")) baseUrl += "/";
this._baseUrl = new URL(baseUrl, window.location.origin).href;
this._ready = false
this._playing = false
this._pendingFetches = {} // instrument -> fetch
this._songPtr = 0
this._bufferPtr = 0
this._array = new Int16Array(BUFFER_SIZE * 2)
this._currentUrlOrBuf = null // currently loading url or buf
this._interval = null
this._ready = false;
this._playing = false;
this._pendingFetches = {}; // instrument -> fetch
this._songPtr = 0;
this._bufferPtr = 0;
this._array = new Int16Array(BUFFER_SIZE * 2);
this._currentUrlOrBuf = null; // currently loading url or buf
this._interval = null;
this._startInterval = this._startInterval.bind(this)
this._stopInterval = this._stopInterval.bind(this)
this._startInterval = this._startInterval.bind(this);
this._stopInterval = this._stopInterval.bind(this);
// If the Timidity constructor was not invoked inside a user-initiated event
// handler, then the AudioContext will be suspended. See:
// https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
this._audioContext = new AudioContext()
this._audioContext = audioContext || new AudioContext();
// Start the 'onaudioprocess' events flowing
this._node = this._audioContext.createScriptProcessor(
BUFFER_SIZE,
0,
NUM_CHANNELS
)
this._onAudioProcess = this._onAudioProcess.bind(this)
this._node.addEventListener('audioprocess', this._onAudioProcess)
this._node.connect(this._audioContext.destination)
);
this._onAudioProcess = this._onAudioProcess.bind(this);
this._node.addEventListener("audioprocess", this._onAudioProcess);
this._node.connect(destination || this._audioContext.destination);
this._lib = LibTimidity({
locateFile: file => new URL(file, this._baseUrl).href,
onRuntimeInitialized: () => this._onLibReady()
})
onRuntimeInitialized: () => this._onLibReady(),
});
}
_onLibReady () {
this._lib.FS.writeFile('/timidity.cfg', TIMIDITY_CFG)
_onLibReady() {
this._lib.FS.writeFile("/timidity.cfg", TIMIDITY_CFG);
const result = this._lib._mid_init('/timidity.cfg')
const result = this._lib._mid_init("/timidity.cfg");
if (result !== 0) {
return this._destroy(new Error('Failed to initialize libtimidity'))
return this._destroy(new Error("Failed to initialize libtimidity"));
}
this._bufferPtr = this._lib._malloc(BUFFER_SIZE * BYTES_PER_SAMPLE)
this._bufferPtr = this._lib._malloc(BUFFER_SIZE * BYTES_PER_SAMPLE);
debugVerbose('Initialized libtimidity')
this._ready = true
this.emit('_ready')
debugVerbose("Initialized libtimidity");
this._ready = true;
this.emit("_ready");
}
async load (urlOrBuf) {
debug('load %o', urlOrBuf)
if (this.destroyed) throw new Error('load() called after destroy()')
async load(urlOrBuf) {
debug("load %o", urlOrBuf);
if (this.destroyed) throw new Error("load() called after destroy()");
// If the Timidity constructor was not invoked inside a user-initiated event
// handler, then the AudioContext will be suspended. Attempt to resume it.
this._audioContext.resume()
this._audioContext.resume();
// If a song already exists, destroy it before starting a new one
if (this._songPtr) this._destroySong()
if (this._songPtr) this._destroySong();
this.emit('unstarted')
this._stopInterval()
this.emit("unstarted");
this._stopInterval();
if (!this._ready) return this.once('_ready', () => this.load(urlOrBuf))
if (!this._ready) return this.once("_ready", () => this.load(urlOrBuf));
this.emit('buffering')
this.emit("buffering");
// Save the url or buf to load. Allows detection of when a new interleaved
// load() starts so we can abort this load.
this._currentUrlOrBuf = urlOrBuf
this._currentUrlOrBuf = urlOrBuf;
let midiBuf
if (typeof urlOrBuf === 'string') {
midiBuf = await this._fetch(new URL(urlOrBuf, this._baseUrl))
let midiBuf;
if (typeof urlOrBuf === "string") {
midiBuf = await this._fetch(new URL(urlOrBuf, this._baseUrl));
// If another load() started while awaiting, abort this load
if (this._currentUrlOrBuf !== urlOrBuf) return
if (this._currentUrlOrBuf !== urlOrBuf) return;
} else if (urlOrBuf instanceof Uint8Array) {
midiBuf = urlOrBuf
midiBuf = urlOrBuf;
} else {
throw new Error('load() expects a `string` or `Uint8Array` argument')
throw new Error("load() expects a `string` or `Uint8Array` argument");
}
let songPtr = this._loadSong(midiBuf)
let songPtr = this._loadSong(midiBuf);
// Are we missing instrument files?
let missingCount = this._lib._mid_get_load_request_count(songPtr)
let missingCount = this._lib._mid_get_load_request_count(songPtr);
if (missingCount > 0) {
let missingInstruments = this._getMissingInstruments(songPtr, missingCount)
debugVerbose('Fetching instruments: %o', missingInstruments)
let missingInstruments = this._getMissingInstruments(
songPtr,
missingCount
);
debugVerbose("Fetching instruments: %o", missingInstruments);
// Wait for all instruments to load
await Promise.all(
missingInstruments.map(instrument => this._fetchInstrument(instrument))
)
);
// If another load() started while awaiting, abort this load
if (this._currentUrlOrBuf !== urlOrBuf) return
if (this._currentUrlOrBuf !== urlOrBuf) return;
// Retry the song load, now that instruments have been loaded
this._lib._mid_song_free(songPtr)
songPtr = this._loadSong(midiBuf)
this._lib._mid_song_free(songPtr);
songPtr = this._loadSong(midiBuf);
// Are we STILL missing instrument files? Then our General MIDI soundset
// is probably missing instrument files.
missingCount = this._lib._mid_get_load_request_count(songPtr)
missingCount = this._lib._mid_get_load_request_count(songPtr);
// Print out missing instrument names
if (missingCount > 0) {
missingInstruments = this._getMissingInstruments(songPtr, missingCount)
debug('Playing with missing instruments: %o', missingInstruments)
missingInstruments = this._getMissingInstruments(songPtr, missingCount);
debug("Playing with missing instruments: %o", missingInstruments);
}
}
this._songPtr = songPtr
this._lib._mid_song_start(this._songPtr)
debugVerbose('Song and instruments are loaded')
this._songPtr = songPtr;
this._lib._mid_song_start(this._songPtr);
debugVerbose("Song and instruments are loaded");
}
_getMissingInstruments (songPtr, missingCount) {
const missingInstruments = []
_getMissingInstruments(songPtr, missingCount) {
const missingInstruments = [];
for (let i = 0; i < missingCount; i++) {
const instrumentPtr = this._lib._mid_get_load_request(songPtr, i)
const instrument = this._lib.UTF8ToString(instrumentPtr)
missingInstruments.push(instrument)
const instrumentPtr = this._lib._mid_get_load_request(songPtr, i);
const instrument = this._lib.UTF8ToString(instrumentPtr);
missingInstruments.push(instrument);
}
return missingInstruments
return missingInstruments;
}
_loadSong (midiBuf) {
_loadSong(midiBuf) {
const optsPtr = this._lib._mid_alloc_options(
SAMPLE_RATE,
AUDIO_FORMAT,
NUM_CHANNELS,
BUFFER_SIZE
)
);
// Copy the MIDI buffer into the heap
const midiBufPtr = this._lib._malloc(midiBuf.byteLength)
this._lib.HEAPU8.set(midiBuf, midiBufPtr)
const midiBufPtr = this._lib._malloc(midiBuf.byteLength);
this._lib.HEAPU8.set(midiBuf, midiBufPtr);
// Create a stream
const iStreamPtr = this._lib._mid_istream_open_mem(midiBufPtr, midiBuf.byteLength)
const iStreamPtr = this._lib._mid_istream_open_mem(
midiBufPtr,
midiBuf.byteLength
);
// Load the song
const songPtr = this._lib._mid_song_load(iStreamPtr, optsPtr)
const songPtr = this._lib._mid_song_load(iStreamPtr, optsPtr);
// Free resources no longer needed
this._lib._mid_istream_close(iStreamPtr)
this._lib._free(optsPtr)
this._lib._free(midiBufPtr)
this._lib._mid_istream_close(iStreamPtr);
this._lib._free(optsPtr);
this._lib._free(midiBufPtr);
if (songPtr === 0) {
return this._destroy(new Error('Failed to load MIDI file'))
return this._destroy(new Error("Failed to load MIDI file"));
}
return songPtr
return songPtr;
}
async _fetchInstrument (instrument) {
async _fetchInstrument(instrument) {
if (this._pendingFetches[instrument]) {
// If this instrument is already in the process of being fetched, return
// the existing promise to prevent duplicate fetches.
return this._pendingFetches[instrument]
return this._pendingFetches[instrument];
}
const url = new URL(instrument, this._baseUrl)
const bufPromise = this._fetch(url)
this._pendingFetches[instrument] = bufPromise
const url = new URL(instrument, this._baseUrl);
const bufPromise = this._fetch(url);
this._pendingFetches[instrument] = bufPromise;
const buf = await bufPromise
this._writeInstrumentFile(instrument, buf)
const buf = await bufPromise;
this._writeInstrumentFile(instrument, buf);
delete this._pendingFetches[instrument]
delete this._pendingFetches[instrument];
return buf
return buf;
}
_writeInstrumentFile (instrument, buf) {
_writeInstrumentFile(instrument, buf) {
const folderPath = instrument
.split('/')
.split("/")
.slice(0, -1) // remove basename
.join('/')
this._mkdirp(folderPath)
this._lib.FS.writeFile(instrument, buf, { encoding: 'binary' })
.join("/");
this._mkdirp(folderPath);
this._lib.FS.writeFile(instrument, buf, { encoding: "binary" });
}
_mkdirp (folderPath) {
const pathParts = folderPath.split('/')
let dirPath = '/'
_mkdirp(folderPath) {
const pathParts = folderPath.split("/");
let dirPath = "/";
for (let i = 0; i < pathParts.length; i++) {
const curPart = pathParts[i]
const curPart = pathParts[i];
try {
this._lib.FS.mkdir(`${dirPath}${curPart}`)
this._lib.FS.mkdir(`${dirPath}${curPart}`);
} catch (err) {}
dirPath += `${curPart}/`
dirPath += `${curPart}/`;
}
}
async _fetch (url) {
async _fetch(url) {
const opts = {
mode: 'cors',
credentials: 'same-origin'
}
const response = await window.fetch(url, opts)
if (response.status !== 200) throw new Error(`Could not load ${url}`)
mode: "cors",
credentials: "same-origin",
};
const response = await window.fetch(url, opts);
if (response.status !== 200) throw new Error(`Could not load ${url}`);
const arrayBuffer = await response.arrayBuffer()
const buf = new Uint8Array(arrayBuffer)
return buf
const arrayBuffer = await response.arrayBuffer();
const buf = new Uint8Array(arrayBuffer);
return buf;
}
play () {
debug('play')
if (this.destroyed) throw new Error('play() called after destroy()')
play() {
debug("play");
if (this.destroyed) throw new Error("play() called after destroy()");
// If the Timidity constructor was not invoked inside a user-initiated event
// handler, then the AudioContext will be suspended. Attempt to resume it.
this._audioContext.resume()
this._audioContext.resume();
this._playing = true
this._playing = true;
if (this._ready && !this._currentUrlOrBuf) {
this.emit('playing')
this._startInterval()
this.emit("playing");
this._startInterval();
}
}
_onAudioProcess (event) {
const sampleCount = (this._songPtr && this._playing)
? this._readMidiData()
: 0
_onAudioProcess(event) {
const sampleCount =
this._songPtr && this._playing ? this._readMidiData() : 0;
if (sampleCount > 0 && this._currentUrlOrBuf) {
this._currentUrlOrBuf = null
this.emit('playing')
this._startInterval()
this._currentUrlOrBuf = null;
this.emit("playing");
this._startInterval();
}
const output0 = event.outputBuffer.getChannelData(0)
const output1 = event.outputBuffer.getChannelData(1)
const output0 = event.outputBuffer.getChannelData(0);
const output1 = event.outputBuffer.getChannelData(1);
for (let i = 0; i < sampleCount; i++) {
output0[i] = this._array[i * 2] / 0x7FFF
output1[i] = this._array[i * 2 + 1] / 0x7FFF
output0[i] = this._array[i * 2] / 0x7fff;
output1[i] = this._array[i * 2 + 1] / 0x7fff;
}
for (let i = sampleCount; i < BUFFER_SIZE; i++) {
output0[i] = 0
output1[i] = 0
output0[i] = 0;
output1[i] = 0;
}
if (this._songPtr && this._playing && sampleCount === 0) {
// Reached the end of the file
this.seek(0)
this.pause()
this._lib._mid_song_start(this._songPtr)
this.emit('ended')
this.seek(0);
this.pause();
this._lib._mid_song_start(this._songPtr);
this.emit("ended");
}
}
_readMidiData () {
_readMidiData() {
const byteCount = this._lib._mid_song_read_wave(
this._songPtr,
this._bufferPtr,
BUFFER_SIZE * BYTES_PER_SAMPLE
)
const sampleCount = byteCount / BYTES_PER_SAMPLE
);
const sampleCount = byteCount / BYTES_PER_SAMPLE;
// Was anything output? If not, don't bother copying anything
if (sampleCount === 0) {
return 0
return 0;
}
this._array.set(
this._lib.HEAP16.subarray(this._bufferPtr / 2, (this._bufferPtr + byteCount) / 2)
)
this._lib.HEAP16.subarray(
this._bufferPtr / 2,
(this._bufferPtr + byteCount) / 2
)
);
return sampleCount
return sampleCount;
}
pause () {
debug('pause')
if (this.destroyed) throw new Error('pause() called after destroy()')
pause() {
debug("pause");
if (this.destroyed) throw new Error("pause() called after destroy()");
this._playing = false
this._stopInterval()
this.emit('paused')
this._playing = false;
this._stopInterval();
this.emit("paused");
}
seek (time) {
debug('seek %d', time)
if (this.destroyed) throw new Error('seek() called after destroy()')
if (!this._songPtr) return // ignore seek if there is no song loaded yet
seek(time) {
debug("seek %d", time);
if (this.destroyed) throw new Error("seek() called after destroy()");
if (!this._songPtr) return; // ignore seek if there is no song loaded yet
const timeMs = Math.floor(time * 1000)
this._lib._mid_song_seek(this._songPtr, timeMs)
this._onTimeupdate()
const timeMs = Math.floor(time * 1000);
this._lib._mid_song_seek(this._songPtr, timeMs);
this._onTimeupdate();
}
get currentTime () {
if (this.destroyed || !this._songPtr) return 0
return this._lib._mid_song_get_time(this._songPtr) / 1000
get currentTime() {
if (this.destroyed || !this._songPtr) return 0;
return this._lib._mid_song_get_time(this._songPtr) / 1000;
}
get duration () {
if (this.destroyed || !this._songPtr) return 1
return this._lib._mid_song_get_total_time(this._songPtr) / 1000
get duration() {
if (this.destroyed || !this._songPtr) return 1;
return this._lib._mid_song_get_total_time(this._songPtr) / 1000;
}
/**
* This event fires when the time indicated by the `currentTime` property
* has been updated.
*/
_onTimeupdate () {
this.emit('timeupdate', this.currentTime)
_onTimeupdate() {
this.emit("timeupdate", this.currentTime);
}
_startInterval () {
this._onTimeupdate()
this._interval = setInterval(() => this._onTimeupdate(), 1000)
_startInterval() {
this._onTimeupdate();
this._interval = setInterval(() => this._onTimeupdate(), 1000);
}
_stopInterval () {
this._onTimeupdate()
clearInterval(this._interval)
this._interval = null
_stopInterval() {
this._onTimeupdate();
clearInterval(this._interval);
this._interval = null;
}
destroy () {
debug('destroy')
if (this.destroyed) throw new Error('destroy() called after destroy()')
this._destroy()
destroy() {
debug("destroy");
if (this.destroyed) throw new Error("destroy() called after destroy()");
this._destroy();
}
_destroy (err) {
if (this.destroyed) return
this.destroyed = true
_destroy(err) {
if (this.destroyed) return;
this.destroyed = true;
this._stopInterval()
this._stopInterval();
this._array = null
this._array = null;
if (this._songPtr) {
this._destroySong()
this._destroySong();
}
if (this._bufferPtr) {
this._lib._free(this._bufferPtr)
this._bufferPtr = 0
this._lib._free(this._bufferPtr);
this._bufferPtr = 0;
}
if (this._node) {
this._node.disconnect()
this._node.removeEventListener('audioprocess', this._onAudioProcess)
this._node.disconnect();
this._node.removeEventListener("audioprocess", this._onAudioProcess);
}
if (this._audioContext) {
this._audioContext.close()
this._audioContext.close();
}
if (err) this.emit('error', err)
debug('destroyed (err %o)', err)
if (err) this.emit("error", err);
debug("destroyed (err %o)", err);
}
_destroySong () {
this._lib._mid_song_free(this._songPtr)
this._songPtr = 0
_destroySong() {
this._lib._mid_song_free(this._songPtr);
this._songPtr = 0;
}
}
module.exports = Timidity
module.exports = Timidity;
},{"./libtimidity":2,"debug":3,"events":5}],2:[function(require,module,exports){

View file

@ -1,401 +1,410 @@
const Debug = require('debug')
const EventEmitter = require('events').EventEmitter
const fs = require('fs')
const LibTimidity = require('./libtimidity')
const Debug = require("debug");
const EventEmitter = require("events").EventEmitter;
const fs = require("fs");
const LibTimidity = require("./libtimidity");
const debug = Debug('timidity')
const debugVerbose = Debug('timidity:verbose')
const debug = Debug("timidity");
const debugVerbose = Debug("timidity:verbose");
// Inlined at build time by 'brfs' browserify transform
const TIMIDITY_CFG = fs.readFileSync(
__dirname + '/freepats.cfg', // eslint-disable-line no-path-concat
'utf8'
)
__dirname + "/freepats.cfg", // eslint-disable-line no-path-concat
"utf8"
);
const SAMPLE_RATE = 44100
const AUDIO_FORMAT = 0x8010 // format of the rendered audio 's16'
const NUM_CHANNELS = 2 // stereo (2 channels)
const BYTES_PER_SAMPLE = 2 * NUM_CHANNELS
const BUFFER_SIZE = 16384 // buffer size for each render() call
const SAMPLE_RATE = 44100;
const AUDIO_FORMAT = 0x8010; // format of the rendered audio 's16'
const NUM_CHANNELS = 2; // stereo (2 channels)
const BYTES_PER_SAMPLE = 2 * NUM_CHANNELS;
const BUFFER_SIZE = 16384; // buffer size for each render() call
const AudioContext = typeof window !== 'undefined' &&
(window.AudioContext || window.webkitAudioContext)
const AudioContext =
typeof window !== "undefined" &&
(window.AudioContext || window.webkitAudioContext);
class Timidity extends EventEmitter {
constructor (baseUrl = '/') {
super()
constructor({ baseUrl = "/", audioContext, destination }) {
super();
this.destroyed = false
this.destroyed = false;
if (!baseUrl.endsWith('/')) baseUrl += '/'
this._baseUrl = new URL(baseUrl, window.location.origin).href
if (!baseUrl.endsWith("/")) baseUrl += "/";
this._baseUrl = new URL(baseUrl, window.location.origin).href;
this._ready = false
this._playing = false
this._pendingFetches = {} // instrument -> fetch
this._songPtr = 0
this._bufferPtr = 0
this._array = new Int16Array(BUFFER_SIZE * 2)
this._currentUrlOrBuf = null // currently loading url or buf
this._interval = null
this._ready = false;
this._playing = false;
this._pendingFetches = {}; // instrument -> fetch
this._songPtr = 0;
this._bufferPtr = 0;
this._array = new Int16Array(BUFFER_SIZE * 2);
this._currentUrlOrBuf = null; // currently loading url or buf
this._interval = null;
this._startInterval = this._startInterval.bind(this)
this._stopInterval = this._stopInterval.bind(this)
this._startInterval = this._startInterval.bind(this);
this._stopInterval = this._stopInterval.bind(this);
// If the Timidity constructor was not invoked inside a user-initiated event
// handler, then the AudioContext will be suspended. See:
// https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
this._audioContext = new AudioContext()
this._audioContext = audioContext || new AudioContext();
// Start the 'onaudioprocess' events flowing
this._node = this._audioContext.createScriptProcessor(
BUFFER_SIZE,
0,
NUM_CHANNELS
)
this._onAudioProcess = this._onAudioProcess.bind(this)
this._node.addEventListener('audioprocess', this._onAudioProcess)
this._node.connect(this._audioContext.destination)
);
this._onAudioProcess = this._onAudioProcess.bind(this);
this._node.addEventListener("audioprocess", this._onAudioProcess);
this._node.connect(destination || this._audioContext.destination);
this._lib = LibTimidity({
locateFile: file => new URL(file, this._baseUrl).href,
onRuntimeInitialized: () => this._onLibReady()
})
onRuntimeInitialized: () => this._onLibReady(),
});
}
_onLibReady () {
this._lib.FS.writeFile('/timidity.cfg', TIMIDITY_CFG)
_onLibReady() {
this._lib.FS.writeFile("/timidity.cfg", TIMIDITY_CFG);
const result = this._lib._mid_init('/timidity.cfg')
const result = this._lib._mid_init("/timidity.cfg");
if (result !== 0) {
return this._destroy(new Error('Failed to initialize libtimidity'))
return this._destroy(new Error("Failed to initialize libtimidity"));
}
this._bufferPtr = this._lib._malloc(BUFFER_SIZE * BYTES_PER_SAMPLE)
this._bufferPtr = this._lib._malloc(BUFFER_SIZE * BYTES_PER_SAMPLE);
debugVerbose('Initialized libtimidity')
this._ready = true
this.emit('_ready')
debugVerbose("Initialized libtimidity");
this._ready = true;
this.emit("_ready");
}
async load (urlOrBuf) {
debug('load %o', urlOrBuf)
if (this.destroyed) throw new Error('load() called after destroy()')
async load(urlOrBuf) {
debug("load %o", urlOrBuf);
if (this.destroyed) throw new Error("load() called after destroy()");
// If the Timidity constructor was not invoked inside a user-initiated event
// handler, then the AudioContext will be suspended. Attempt to resume it.
this._audioContext.resume()
this._audioContext.resume();
// If a song already exists, destroy it before starting a new one
if (this._songPtr) this._destroySong()
if (this._songPtr) this._destroySong();
this.emit('unstarted')
this._stopInterval()
this.emit("unstarted");
this._stopInterval();
if (!this._ready) return this.once('_ready', () => this.load(urlOrBuf))
if (!this._ready) return this.once("_ready", () => this.load(urlOrBuf));
this.emit('buffering')
this.emit("buffering");
// Save the url or buf to load. Allows detection of when a new interleaved
// load() starts so we can abort this load.
this._currentUrlOrBuf = urlOrBuf
this._currentUrlOrBuf = urlOrBuf;
let midiBuf
if (typeof urlOrBuf === 'string') {
midiBuf = await this._fetch(new URL(urlOrBuf, this._baseUrl))
let midiBuf;
if (typeof urlOrBuf === "string") {
midiBuf = await this._fetch(new URL(urlOrBuf, this._baseUrl));
// If another load() started while awaiting, abort this load
if (this._currentUrlOrBuf !== urlOrBuf) return
if (this._currentUrlOrBuf !== urlOrBuf) return;
} else if (urlOrBuf instanceof Uint8Array) {
midiBuf = urlOrBuf
midiBuf = urlOrBuf;
} else {
throw new Error('load() expects a `string` or `Uint8Array` argument')
throw new Error("load() expects a `string` or `Uint8Array` argument");
}
let songPtr = this._loadSong(midiBuf)
let songPtr = this._loadSong(midiBuf);
// Are we missing instrument files?
let missingCount = this._lib._mid_get_load_request_count(songPtr)
let missingCount = this._lib._mid_get_load_request_count(songPtr);
if (missingCount > 0) {
let missingInstruments = this._getMissingInstruments(songPtr, missingCount)
debugVerbose('Fetching instruments: %o', missingInstruments)
let missingInstruments = this._getMissingInstruments(
songPtr,
missingCount
);
debugVerbose("Fetching instruments: %o", missingInstruments);
// Wait for all instruments to load
await Promise.all(
missingInstruments.map(instrument => this._fetchInstrument(instrument))
)
);
// If another load() started while awaiting, abort this load
if (this._currentUrlOrBuf !== urlOrBuf) return
if (this._currentUrlOrBuf !== urlOrBuf) return;
// Retry the song load, now that instruments have been loaded
this._lib._mid_song_free(songPtr)
songPtr = this._loadSong(midiBuf)
this._lib._mid_song_free(songPtr);
songPtr = this._loadSong(midiBuf);
// Are we STILL missing instrument files? Then our General MIDI soundset
// is probably missing instrument files.
missingCount = this._lib._mid_get_load_request_count(songPtr)
missingCount = this._lib._mid_get_load_request_count(songPtr);
// Print out missing instrument names
if (missingCount > 0) {
missingInstruments = this._getMissingInstruments(songPtr, missingCount)
debug('Playing with missing instruments: %o', missingInstruments)
missingInstruments = this._getMissingInstruments(songPtr, missingCount);
debug("Playing with missing instruments: %o", missingInstruments);
}
}
this._songPtr = songPtr
this._lib._mid_song_start(this._songPtr)
debugVerbose('Song and instruments are loaded')
this._songPtr = songPtr;
this._lib._mid_song_start(this._songPtr);
debugVerbose("Song and instruments are loaded");
}
_getMissingInstruments (songPtr, missingCount) {
const missingInstruments = []
_getMissingInstruments(songPtr, missingCount) {
const missingInstruments = [];
for (let i = 0; i < missingCount; i++) {
const instrumentPtr = this._lib._mid_get_load_request(songPtr, i)
const instrument = this._lib.UTF8ToString(instrumentPtr)
missingInstruments.push(instrument)
const instrumentPtr = this._lib._mid_get_load_request(songPtr, i);
const instrument = this._lib.UTF8ToString(instrumentPtr);
missingInstruments.push(instrument);
}
return missingInstruments
return missingInstruments;
}
_loadSong (midiBuf) {
_loadSong(midiBuf) {
const optsPtr = this._lib._mid_alloc_options(
SAMPLE_RATE,
AUDIO_FORMAT,
NUM_CHANNELS,
BUFFER_SIZE
)
);
// Copy the MIDI buffer into the heap
const midiBufPtr = this._lib._malloc(midiBuf.byteLength)
this._lib.HEAPU8.set(midiBuf, midiBufPtr)
const midiBufPtr = this._lib._malloc(midiBuf.byteLength);
this._lib.HEAPU8.set(midiBuf, midiBufPtr);
// Create a stream
const iStreamPtr = this._lib._mid_istream_open_mem(midiBufPtr, midiBuf.byteLength)
const iStreamPtr = this._lib._mid_istream_open_mem(
midiBufPtr,
midiBuf.byteLength
);
// Load the song
const songPtr = this._lib._mid_song_load(iStreamPtr, optsPtr)
const songPtr = this._lib._mid_song_load(iStreamPtr, optsPtr);
// Free resources no longer needed
this._lib._mid_istream_close(iStreamPtr)
this._lib._free(optsPtr)
this._lib._free(midiBufPtr)
this._lib._mid_istream_close(iStreamPtr);
this._lib._free(optsPtr);
this._lib._free(midiBufPtr);
if (songPtr === 0) {
return this._destroy(new Error('Failed to load MIDI file'))
return this._destroy(new Error("Failed to load MIDI file"));
}
return songPtr
return songPtr;
}
async _fetchInstrument (instrument) {
async _fetchInstrument(instrument) {
if (this._pendingFetches[instrument]) {
// If this instrument is already in the process of being fetched, return
// the existing promise to prevent duplicate fetches.
return this._pendingFetches[instrument]
return this._pendingFetches[instrument];
}
const url = new URL(instrument, this._baseUrl)
const bufPromise = this._fetch(url)
this._pendingFetches[instrument] = bufPromise
const url = new URL(instrument, this._baseUrl);
const bufPromise = this._fetch(url);
this._pendingFetches[instrument] = bufPromise;
const buf = await bufPromise
this._writeInstrumentFile(instrument, buf)
const buf = await bufPromise;
this._writeInstrumentFile(instrument, buf);
delete this._pendingFetches[instrument]
delete this._pendingFetches[instrument];
return buf
return buf;
}
_writeInstrumentFile (instrument, buf) {
_writeInstrumentFile(instrument, buf) {
const folderPath = instrument
.split('/')
.split("/")
.slice(0, -1) // remove basename
.join('/')
this._mkdirp(folderPath)
this._lib.FS.writeFile(instrument, buf, { encoding: 'binary' })
.join("/");
this._mkdirp(folderPath);
this._lib.FS.writeFile(instrument, buf, { encoding: "binary" });
}
_mkdirp (folderPath) {
const pathParts = folderPath.split('/')
let dirPath = '/'
_mkdirp(folderPath) {
const pathParts = folderPath.split("/");
let dirPath = "/";
for (let i = 0; i < pathParts.length; i++) {
const curPart = pathParts[i]
const curPart = pathParts[i];
try {
this._lib.FS.mkdir(`${dirPath}${curPart}`)
this._lib.FS.mkdir(`${dirPath}${curPart}`);
} catch (err) {}
dirPath += `${curPart}/`
dirPath += `${curPart}/`;
}
}
async _fetch (url) {
async _fetch(url) {
const opts = {
mode: 'cors',
credentials: 'same-origin'
}
const response = await window.fetch(url, opts)
if (response.status !== 200) throw new Error(`Could not load ${url}`)
mode: "cors",
credentials: "same-origin",
};
const response = await window.fetch(url, opts);
if (response.status !== 200) throw new Error(`Could not load ${url}`);
const arrayBuffer = await response.arrayBuffer()
const buf = new Uint8Array(arrayBuffer)
return buf
const arrayBuffer = await response.arrayBuffer();
const buf = new Uint8Array(arrayBuffer);
return buf;
}
play () {
debug('play')
if (this.destroyed) throw new Error('play() called after destroy()')
play() {
debug("play");
if (this.destroyed) throw new Error("play() called after destroy()");
// If the Timidity constructor was not invoked inside a user-initiated event
// handler, then the AudioContext will be suspended. Attempt to resume it.
this._audioContext.resume()
this._audioContext.resume();
this._playing = true
this._playing = true;
if (this._ready && !this._currentUrlOrBuf) {
this.emit('playing')
this._startInterval()
this.emit("playing");
this._startInterval();
}
}
_onAudioProcess (event) {
const sampleCount = (this._songPtr && this._playing)
? this._readMidiData()
: 0
_onAudioProcess(event) {
const sampleCount =
this._songPtr && this._playing ? this._readMidiData() : 0;
if (sampleCount > 0 && this._currentUrlOrBuf) {
this._currentUrlOrBuf = null
this.emit('playing')
this._startInterval()
this._currentUrlOrBuf = null;
this.emit("playing");
this._startInterval();
}
const output0 = event.outputBuffer.getChannelData(0)
const output1 = event.outputBuffer.getChannelData(1)
const output0 = event.outputBuffer.getChannelData(0);
const output1 = event.outputBuffer.getChannelData(1);
for (let i = 0; i < sampleCount; i++) {
output0[i] = this._array[i * 2] / 0x7FFF
output1[i] = this._array[i * 2 + 1] / 0x7FFF
output0[i] = this._array[i * 2] / 0x7fff;
output1[i] = this._array[i * 2 + 1] / 0x7fff;
}
for (let i = sampleCount; i < BUFFER_SIZE; i++) {
output0[i] = 0
output1[i] = 0
output0[i] = 0;
output1[i] = 0;
}
if (this._songPtr && this._playing && sampleCount === 0) {
// Reached the end of the file
this.seek(0)
this.pause()
this._lib._mid_song_start(this._songPtr)
this.emit('ended')
this.seek(0);
this.pause();
this._lib._mid_song_start(this._songPtr);
this.emit("ended");
}
}
_readMidiData () {
_readMidiData() {
const byteCount = this._lib._mid_song_read_wave(
this._songPtr,
this._bufferPtr,
BUFFER_SIZE * BYTES_PER_SAMPLE
)
const sampleCount = byteCount / BYTES_PER_SAMPLE
);
const sampleCount = byteCount / BYTES_PER_SAMPLE;
// Was anything output? If not, don't bother copying anything
if (sampleCount === 0) {
return 0
return 0;
}
this._array.set(
this._lib.HEAP16.subarray(this._bufferPtr / 2, (this._bufferPtr + byteCount) / 2)
)
this._lib.HEAP16.subarray(
this._bufferPtr / 2,
(this._bufferPtr + byteCount) / 2
)
);
return sampleCount
return sampleCount;
}
pause () {
debug('pause')
if (this.destroyed) throw new Error('pause() called after destroy()')
pause() {
debug("pause");
if (this.destroyed) throw new Error("pause() called after destroy()");
this._playing = false
this._stopInterval()
this.emit('paused')
this._playing = false;
this._stopInterval();
this.emit("paused");
}
seek (time) {
debug('seek %d', time)
if (this.destroyed) throw new Error('seek() called after destroy()')
if (!this._songPtr) return // ignore seek if there is no song loaded yet
seek(time) {
debug("seek %d", time);
if (this.destroyed) throw new Error("seek() called after destroy()");
if (!this._songPtr) return; // ignore seek if there is no song loaded yet
const timeMs = Math.floor(time * 1000)
this._lib._mid_song_seek(this._songPtr, timeMs)
this._onTimeupdate()
const timeMs = Math.floor(time * 1000);
this._lib._mid_song_seek(this._songPtr, timeMs);
this._onTimeupdate();
}
get currentTime () {
if (this.destroyed || !this._songPtr) return 0
return this._lib._mid_song_get_time(this._songPtr) / 1000
get currentTime() {
if (this.destroyed || !this._songPtr) return 0;
return this._lib._mid_song_get_time(this._songPtr) / 1000;
}
get duration () {
if (this.destroyed || !this._songPtr) return 1
return this._lib._mid_song_get_total_time(this._songPtr) / 1000
get duration() {
if (this.destroyed || !this._songPtr) return 1;
return this._lib._mid_song_get_total_time(this._songPtr) / 1000;
}
/**
* This event fires when the time indicated by the `currentTime` property
* has been updated.
*/
_onTimeupdate () {
this.emit('timeupdate', this.currentTime)
_onTimeupdate() {
this.emit("timeupdate", this.currentTime);
}
_startInterval () {
this._onTimeupdate()
this._interval = setInterval(() => this._onTimeupdate(), 1000)
_startInterval() {
this._onTimeupdate();
this._interval = setInterval(() => this._onTimeupdate(), 1000);
}
_stopInterval () {
this._onTimeupdate()
clearInterval(this._interval)
this._interval = null
_stopInterval() {
this._onTimeupdate();
clearInterval(this._interval);
this._interval = null;
}
destroy () {
debug('destroy')
if (this.destroyed) throw new Error('destroy() called after destroy()')
this._destroy()
destroy() {
debug("destroy");
if (this.destroyed) throw new Error("destroy() called after destroy()");
this._destroy();
}
_destroy (err) {
if (this.destroyed) return
this.destroyed = true
_destroy(err) {
if (this.destroyed) return;
this.destroyed = true;
this._stopInterval()
this._stopInterval();
this._array = null
this._array = null;
if (this._songPtr) {
this._destroySong()
this._destroySong();
}
if (this._bufferPtr) {
this._lib._free(this._bufferPtr)
this._bufferPtr = 0
this._lib._free(this._bufferPtr);
this._bufferPtr = 0;
}
if (this._node) {
this._node.disconnect()
this._node.removeEventListener('audioprocess', this._onAudioProcess)
this._node.disconnect();
this._node.removeEventListener("audioprocess", this._onAudioProcess);
}
if (this._audioContext) {
this._audioContext.close()
this._audioContext.close();
}
if (err) this.emit('error', err)
debug('destroyed (err %o)', err)
if (err) this.emit("error", err);
debug("destroyed (err %o)", err);
}
_destroySong () {
this._lib._mid_song_free(this._songPtr)
this._songPtr = 0
_destroySong() {
this._lib._mid_song_free(this._songPtr);
this._songPtr = 0;
}
}
module.exports = Timidity
module.exports = Timidity;

View file

@ -1,5 +1,4 @@
import Emitter from "../emitter";
import { clamp } from "../utils";
import { MEDIA_STATUS } from "../constants";
import { MediaStatus } from "../types";
import Timidity from "../../demo/timidity/bundle.js";
@ -7,9 +6,7 @@ import Timidity from "../../demo/timidity/bundle.js";
export default class ElementSource {
_emitter: Emitter;
_context: AudioContext;
_source: AudioNode;
_destination: AudioNode;
_audio: HTMLAudioElement;
_stalled: boolean;
_status: MediaStatus;
_player: Timidity;
@ -22,12 +19,14 @@ export default class ElementSource {
this._emitter = new Emitter();
this._context = context;
this._destination = destination;
this._audio = document.createElement("audio");
this._audio.crossOrigin = "anonymous";
this._stalled = false;
this._status = MEDIA_STATUS.STOPPED;
this._player = new Timidity("/demo/timidity/");
this._player = new Timidity({
baseUrl: "/demo/timidity/",
audioContext: context,
destination,
});
// TODO: #leak
this._player.on("unstarted", () => {
@ -45,44 +44,17 @@ export default class ElementSource {
});
// TODO: #leak
this._audio.addEventListener("ended", () => {
this._player.on("ended", () => {
this._emitter.trigger("ended");
this._setStatus(MEDIA_STATUS.STOPPED);
});
// TODO: #leak
this._audio.addEventListener("error", e => {
switch (this._audio.error!.code) {
case 1:
// The fetching of the associated resource was aborted by the user's request.
console.error("MEDIA_ERR_ABORTED", e);
break;
case 2:
console.error("MEDIA_ERR_NETWORK", e);
// Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.
break;
case 3:
// Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.
// There is a bug in Chrome where improperly terminated mp3s can cuase this error.
// https://bugs.chromium.org/p/chromium/issues/detail?id=794782
// Related: Commit f44e826c83c74fef04c2c448af30cfb353b28312
console.error("PIPELINE_ERROR_DECODE", e);
break;
case 4:
console.error("MEDIA_ERR_SRC_NOT_SUPPORTED", e);
// The associated resource or media provider object (such as a MediaStream) has been found to be unsuitable.
break;
}
// Rather than just geting stuck in this error state, we can just pretend this is
// the end of the track.
this._player.on("error", e => {
console.error("Timidity error", e);
this._emitter.trigger("ended");
this._setStatus(MEDIA_STATUS.STOPPED);
});
this._source = this._context.createMediaElementSource(this._audio);
this._source.connect(destination);
}
_setStalled(stalled: boolean) {
@ -91,7 +63,7 @@ export default class ElementSource {
}
disconnect() {
this._source.disconnect();
this._player._node.disconnect();
}
// Async for now, for compatibility with BufferAudioSource
@ -144,8 +116,8 @@ export default class ElementSource {
}
dispose() {
// TODO: Dispose subscriptions to this.audio
this.stop();
this._emitter.dispose();
this._player.destroy();
}
}