react-orchestra
Version:
A toolbox to build interactive and smart instruments on the web and mobile.
293 lines (267 loc) • 11.4 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: Instruments.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: Instruments.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>import SoundfontPlayer from 'soundfont-player';
import Promise from 'bluebird';
import _ from 'lodash';
import MidiIO from '../MidiIO';
import update from 'immutability-helper';
import Note from './Note';
import audioContext from './AudioContext';
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
import NOTES from './constants/NOTES';
import SCALES from './constants/SCALES';
import CHORDS from './constants/CHORDS';
import { getNoteNamesFromChordName } from './helpers/';
// const NOTES = ['A', 'A#', 'B', 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#'];
window.delay = ms => new Promise(resolve => setTimeout(resolve, ms));
/** Instrument Class. Use it to play music from the browser */
class Instrument {
/**
* Create an instrument. It will import audio context and prepare its internal state.
*/
constructor() {
this.instruments = {
};
this.ac = audioContext;
this.playingNotes = {};
this.isPlayingMelody = false;
}
/**
* Loads instrument from the web and stores it as a resolvable promise
* @param instrumentName. List of all instrument names in src/constants/INSTRUMENTS
* @returns Promise<musicPlayer>
*/
get(instrumentName) {
return new Promise((resolve, reject) => {
if (_.has(this.instruments, instrumentName)) {
if (this.instruments[instrumentName].promise.done) {
return resolve(this.instruments[instrumentName].instrument);
}
return this.instruments[instrumentName].promise.then(instrument =>
resolve(instrument)
);
}
this.instruments[instrumentName] = {};
this.instruments[instrumentName].isLoading = true;
this.instruments[instrumentName].instrument = {};
this.instruments[instrumentName].promise = SoundfontPlayer.instrument(
audioContext,
instrumentName,
{
nameToUrl: (name, sf, format) => {
format = format === 'ogg' ? format : 'mp3';
sf = sf === 'FluidR3_GM' ? sf : 'MusyngKite';
const url = 'https://d2ovwkm0xvqw0n.cloudfront.net/';// `https://raw.githubusercontent.com/RakanNimer/midi-js-soundfonts/master/`;// 'https://s3-eu-west-1.amazonaws.com/ut-music-player/midi-js-soundfonts/'
const fullPath = `${url}${sf}/${name}-${format}.js`;
return fullPath;
},
}
).then((instrument) => {
this.instruments[instrumentName].instrument = instrument;
this.instruments[instrumentName].promise.done = true;
this.instruments[instrumentName].isLoading = false;
resolve(instrument);
return instrument;
}).catch((err) => {
console.log('ERRROR ', err);
reject(`Couldn't load instrument ${instrumentName}. Error : ${JSON.stringify(err)}`);
});
this.instruments[instrumentName].promise.done = false;
return this.instruments[instrumentName].promise;
});
}
/**
* Start playing note, if note.payload.durationInMS is -1 it will keep playing infinitely until it is explicitly stopped.
* { payload: { id, instrumentName, startTimeInMS, instrumentName, name, gain, duration }}
* @param note
* @returns Promise<notePlayer>
*/
startPlayingNote(note) {
const noteID = note.payload.id; // `${note.payload.noteName}_${note.payload.instrumentName}`;
if (!(noteID in this.playingNotes)) {
this.playingNotes[noteID] = [];
}
return delay(note.payload.startTimeInMS).then(() => {
this.playingNotes[noteID].push(
this.get(note.payload.instrumentName).then((instrument) => {
return instrument.start(note.payload.name, null, {
loop: true,
gain: note.payload.gain,
duration: note.payload.duration === -1 ? 9999999 : note.payload.duration / 1000,
});
})
);
const currentNotePromise = this.playingNotes[noteID][this.playingNotes[noteID].length - 1];
if (note.payload.durationInMS !== -1) {
return delay(note.payload.durationInMS)
.then(() => currentNotePromise).then(notePlayer => notePlayer.stop());
}
return currentNotePromise;
});
}
/**
* Stop playing note
* { payload: { id, instrumentName, startTimeInMS, instrumentName, name, gain, duration }}
* @param note
* @returns Promise<notePlayer>
*/
stopPlayingNote(note) {
if (!note.payload) {
return Promise.resolve(false);
}
const noteID = note.payload.id; // note.payload.id ? note.payload.id : `${note.payload.noteName}_${note.payload.instrumentName}`;
if (typeof this.playingNotes[noteID] === 'undefined') {
return Promise.resolve(true);
}
// 'Cloning' promise before deleting note.
const playingNotes = this.playingNotes[noteID];
const playingNote = playingNotes[playingNotes.length - 1].then();
this.playingNotes[noteID].pop();
if (this.playingNotes[noteID].length === 0) {
delete this.playingNotes[noteID];
}
return delay(note.payload.fadeDurationInMS)
.then(() =>
playingNote
).then((notePlayer) => {
notePlayer.stop();
}).catch((error) => {
alert(`Failed stopping audio. Unsupported Browser ?${JSON.stringify(error)}`);
});
}
/**
* Start playing chord by note names
* @param noteNames
* @param note settings
* @example
* const instrument = new Instrument();
* instrument.startPlayingChordByNoteNames(['A3','C3','E3'], {instrumentName: 'acoustic_grand_piano', startTimeInMS: 0, durationInMS: 1000, endTimeInMS: 1000})
* @returns Promise<notePlayer>
*/
startPlayingChordByNoteNames(noteNames, settings) {
return Promise.map(noteNames, (noteName) => {
const note = new Note({ ...settings, name: noteName });
return this.startPlayingNote(note);
});
}
/**
* Stop playing chord by note names
* @param noteNames
* @param note settings
* @example
* const instrument = new Instrument();
* instrument.stopPlayingChordByNoteNames(['A3','C3','E3'], {instrumentName: 'acoustic_grand_piano', startTimeInMS: 0, durationInMS: 1000, endTimeInMS: 1000})
* @returns Promise<notePlayer>
*/
stopPlayingChordByNoteNames(noteNames, settings) {
return Promise.map(noteNames, (noteName) => {
const note = new Note({ ...settings, name: noteName });
return this.stopPlayingNote(note);
});
}
/**
* startPlayingChordByChordName
* @param firstNoteName
* @param chordName
* @param note settings
* @example
* const instrument = new Instrument();
* instrument.startPlayingChordByChordName('A3', 'maj', {instrumentName: 'acoustic_grand_piano', startTimeInMS: 0, durationInMS: 1000, endTimeInMS: 1000})
* @returns Promise<notePlayer>
*/
startPlayingChordByChordName(firstNoteName, chordName, noteSettings = {}) {
const noteNames = getNoteNamesFromChordName(firstNoteName, chordName);
// console.log({ firstNoteName, noteNames });
return this.startPlayingChordByNoteNames(noteNames, noteSettings);
}
/**
* stopPlayingChordByChordName
* @param firstNoteName
* @param chordName
* @param note settings
* @example
* const instrument = new Instrument();
* instrument.stopPlayingChordByChordName('A3', 'maj', {instrumentName: 'acoustic_grand_piano', startTimeInMS: 0, durationInMS: 1000, endTimeInMS: 1000})
* @returns Promise<notePlayer>
*/
stopPlayingChordByChordName(firstNoteName, chordName, noteSettings = {}) {
const noteNames = getNoteNamesFromChordName(firstNoteName, chordName);
// console.log({ firstNoteName, noteNames });
return this.stopPlayingChordByNoteNames(noteNames, noteSettings);
}
/**
* play array of notes
* @param notes
* @param meta
* @param note settings
* @returns Promise<notePlayer>
*/
play(notes, meta) {
this.isPlayingMelody = true;
const melodyStartTime = notes[0].payload.startTimeInMS;
return Promise.map(notes, (note, i) => {
const currentStartTime = note.payload.startTimeInMS - melodyStartTime;
return window.delay(currentStartTime).then(() => {
if (this.isPlayingMelody === false) {
throw new (
() => ({ code: 'STOPPED_PLAYING' })
)();
}
return this.startPlayingNote(note);
}).then(() => {
return window.delay(note.payload.durationInMS);
}).then(() => {
return this.stopPlayingNote(note);
}).catch((error) => {
if (error.code && error.code === 'STOPPED_PLAYING') {
return null;
} throw error;
});
});
}
/**
* stop playing array of notes
* @param notes
* @param meta
* @param note settings
* @returns Promise<notePlayer>
*/
stop(notes, meta) {
this.isPlayingMelody = false;
return Promise.map(notes, (note, i) => {
return this.stopPlayingNote(note.payload.instrumentName, note.payload.noteName);
});
}
}
export default Instrument;
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Instrument.html">Instrument</a></li><li><a href="Note.html">Note</a></li></ul><h3>Global</h3><ul><li><a href="global.html#addOctaveToNoteName">addOctaveToNoteName</a></li><li><a href="global.html#getInterval">getInterval</a></li><li><a href="global.html#getIntervalPermutationsFromNoteNames">getIntervalPermutationsFromNoteNames</a></li><li><a href="global.html#getJSONFromMidiURL">getJSONFromMidiURL</a></li><li><a href="global.html#getNoteNamesFromChordName">getNoteNamesFromChordName</a></li><li><a href="global.html#getNoteNamesFromIntervals">getNoteNamesFromIntervals</a></li><li><a href="global.html#getScaleNotes">getScaleNotes</a></li><li><a href="global.html#getScalesFromNoteNames">getScalesFromNoteNames</a></li><li><a href="global.html#getTracksAndMetaFromParsedMidi">getTracksAndMetaFromParsedMidi</a></li><li><a href="global.html#getTracksAndMetaFromUrl">getTracksAndMetaFromUrl</a></li><li><a href="global.html#getUniqueNoteNames">getUniqueNoteNames</a></li><li><a href="global.html#getUniqueNoteNamesNoOctave">getUniqueNoteNamesNoOctave</a></li><li><a href="global.html#isInHigherOctave">isInHigherOctave</a></li><li><a href="global.html#removeOctaveFromNoteName">removeOctaveFromNoteName</a></li><li><a href="global.html#reOrderNotes">reOrderNotes</a></li><li><a href="global.html#updateTempo">updateTempo</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.3</a> on Sun Nov 20 2016 16:43:09 GMT+0200 (EET)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>