updating deps and latest MidiConvert

This commit is contained in:
Yotam Mann 2017-01-13 16:45:17 -05:00
parent a0d4af1092
commit d8a0e5d876
12 changed files with 7 additions and 942 deletions

View File

@ -11,23 +11,27 @@
"autoprefixer-loader": "^3.2.0",
"babel-core": "^6.17.0",
"babel-loader": "^6.2.5",
"babel-polyfill": "^6.20.0",
"babel-preset-es2015": "^6.16.0",
"buckets-js": "^1.98.1",
"css-loader": "^0.23.1",
"domready": "^1.0.8",
"events": "^1.1.0",
"exports-loader": "^0.6.3",
"file-loader": "^0.9.0",
"jsmidgen": "^0.1.5",
"midi-file-parser": "^1.0.0",
"midiconvert": "^0.4.1",
"node-sass": "^3.4.2",
"pepjs": "^0.4.2",
"sass-loader": "^3.2.0",
"startaudiocontext": "^1.2.0",
"style-loader": "^0.13.1",
"three": "^0.81.2",
"tone": "^0.8.0",
"three": "^0.83.0",
"tone": "^0.9.0",
"url-loader": "^0.5.7",
"webmidi": "^2.0.0-beta.1",
"webpack": "^1.12.14"
"webpack": "^1.12.14",
"youtube-iframe": "^1.0.3"
}
}

View File

@ -1,21 +0,0 @@
[The MIT License](http://opensource.org/licenses/MIT)
Copyright © 2016 Yotam Mann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,112 +0,0 @@
## [DEMO](https://tonejs.github.io/MidiConvert/)
MidiConvert makes it straightforward to work with MIDI files in Javascript. It uses [midi-file-parser](https://github.com/NHQ/midi-file-parser) to decode MIDI files and [jsmidgen](https://github.com/dingram/jsmidgen) to encode MIDI files.
```javascript
//load a midi file
MidiConvert.load("path/to/midi.mid", function(midi){
console.log(midi)
})
```
### Format
The data parsed from the midi file looks like this:
```javascript
{
// the transport and timing data
header : {
bpm : Number, // the tempo, e.g. 120
timeSignature : [Number, Number], // the time signature, e.g. [4, 4],
PPQ : Number // the Pulses Per Quarter of the midi file
},
// an array of midi tracks
tracks : [
{
name : String, // the track name if one was given
notes : [
{
midi : Number, // midi number, e.g. 60
time : Number, // time in seconds
note : String, // note name, e.g. "C4"
velocity : Number, // normalized 0-1 velocity
duration : String // duration between noteOn and noteOff
}
],
//midi control changes
controlChanges : {
//if there are control changes in the midi file
'91' : [
{
number : Number // the cc number
time : Number, // time in seconds
value : Number // normalized 0-1
}
],
},
instrument : String //the instrument if one is given
}
]
}
```
### Raw Midi Parsing
If you are using Node.js or have the raw binary string from the midi file, just use the `parse` method:
```javascript
fs.readFile("test.mid", "binary", function(err, midiBlob){
if (!err){
var midi = MidiConvert.parse(midiBlob)
}
})
```
### Encoding Midi
You can also create midi files from scratch of by modifying an existing file.
```javascript
//create a new midi file
var midi = MidiConvert.create()
//add a track
midi.track()
//chain note events: note, time, duration
.note(60, 0, 2)
.note(63, 1, 2)
.note(60, 2, 2)
//write the output
fs.writeFileSync("output.mid", midi.encode(), "binary")
```
### Tone.Part
The note data can be easily passed into [Tone.Part](http://tonejs.github.io/docs/#Part)
```javascript
var synth = new Tone.PolySynth(8).toMaster()
MidiConvert.load("path/to/midi.mid", function(midi){
//make sure you set the tempo before you schedule the events
Tone.Transport.bpm.value = midi.bpm
//pass in the note events from one of the tracks as the second argument to Tone.Part
var midiPart = new Tone.Part(function(time, note){
//use the events to play the synth
synth.triggerAttackRelease(note.name, note.duration, time, note.velocity)
}, midi.tracks[0].notes).start()
//start the transport to hear the events
Tone.Transport.start()
})
```
### Acknowledgment
MidiConvert uses [midi-file-parser](https://github.com/NHQ/midi-file-parser) which is ported from [jasmid](https://github.com/gasman/jasmid) for decoding MIDI data and and [jsmidgen](https://github.com/dingram/jsmidgen) for encoding MIDI data.

View File

@ -1,54 +0,0 @@
/**
* Return the index of the element at or before the given time
*/
function findElement(array, time) {
let beginning = 0
const len = array.length
let end = len
if (len > 0 && array[len - 1].time <= time){
return len - 1
}
while (beginning < end){
// calculate the midpoint for roughly equal partition
let midPoint = Math.floor(beginning + (end - beginning) / 2)
const event = array[midPoint]
const nextEvent = array[midPoint + 1]
if (event.time === time){
//choose the last one that has the same time
for (let i = midPoint; i < array.length; i++){
let testEvent = array[i]
if (testEvent.time === time){
midPoint = i
}
}
return midPoint
} else if (event.time < time && nextEvent.time > time){
return midPoint
} else if (event.time > time){
//search lower
end = midPoint
} else if (event.time < time){
//search upper
beginning = midPoint + 1
}
}
return -1
}
/**
* Does a binary search to insert the note
* in the correct spot in the array
* @param {Array} array
* @param {Object} event
* @param {Number=} offset
*/
function BinaryInsert(array, event){
if (array.length){
const index = findElement(array, event.time)
array.splice(index + 1, 0, event)
} else {
array.push(event)
}
}
export {BinaryInsert}

View File

@ -1,39 +0,0 @@
const channelNames = {
"1" : "modulationWheel",
"2" : "breath",
"4" : "footController",
"5" : "portamentoTime",
"7" : "volume",
"8" : "balance",
"10" : "pan",
"64" : "sustain",
"65" : "portamentoTime",
"66" : "sostenuto",
"67" : "softPedal",
"68" : "legatoFootswitch",
"84" : "portamentoContro"
}
class Control{
constructor(number, time, value){
this.number = number
this.time = time
this.value = value
}
/**
* The common name of the control change event
* @type {String}
* @readOnly
*/
get name(){
if (channelNames.hasOwnProperty(this.number)){
return channelNames[this.number]
}
}
}
export {Control}

View File

@ -1,29 +0,0 @@
/**
* Parse tempo and time signature from the midiJson
* @param {Object} midiJson
* @return {Object}
*/
function parseHeader(midiJson){
var ret = {
PPQ : midiJson.header.ticksPerBeat
}
for (var i = 0; i < midiJson.tracks.length; i++){
var track = midiJson.tracks[i]
for (var j = 0; j < track.length; j++){
var datum = track[j]
if (datum.type === "meta"){
if (datum.subtype === "timeSignature"){
ret.timeSignature = [datum.numerator, datum.denominator]
} else if (datum.subtype === "setTempo"){
if (!ret.bpm){
ret.bpm = 60000000 / datum.microsecondsPerBeat
}
}
}
}
}
ret.bpm = ret.bpm || 120
return ret
}
export {parseHeader}

View File

@ -1,44 +0,0 @@
function hasMoreValues(arrays, positions){
for (let i = 0; i < arrays.length; i++){
let arr = arrays[i]
let pos = positions[i]
if (arr.length > pos){
return true
}
}
return false
}
function getLowestAtPosition(arrays, positions, encoders){
let lowestIndex = 0
let lowestValue = Infinity
for (let i = 0; i < arrays.length; i++){
let arr = arrays[i]
let pos = positions[i]
if (arr[pos] && (arr[pos].time < lowestValue)){
lowestIndex = i
lowestValue = arr[pos].time
}
}
encoders[lowestIndex](arrays[lowestIndex][positions[lowestIndex]])
// increment array
positions[lowestIndex] += 1
}
/**
* Combine multiple arrays keeping the timing in order
* The arguments should alternate between the array and the encoder callback
* @param {...Array|Function} args
*/
function Merge(...args){
const arrays = args.filter((v, i) => (i % 2) === 0)
const positions = new Uint32Array(arrays.length)
const encoders = args.filter((v, i) => (i % 2) === 1)
const output = []
while(hasMoreValues(arrays, positions)){
getLowestAtPosition(arrays, positions, encoders)
}
}
export {Merge}

View File

@ -1,205 +0,0 @@
import Decoder from 'midi-file-parser'
import Encoder from 'jsmidgen'
import Util from './Util'
import {Track} from './Track'
import {parseHeader} from './Header'
/**
* @class The Midi object. Contains tracks and the header info.
*/
class Midi {
constructor(){
this.header = {
//defaults
bpm : 120,
timeSignature : [4, 4],
PPQ : 480
}
this.tracks = []
}
/**
* Load the given url and parse the midi at that url
* @param {String} url
* @param {*} data Anything that should be sent in the XHR
* @param {String} method Either GET or POST
* @return {Promise}
*/
load(url, data=null, method='GET'){
return new Promise((success, fail) => {
var request = new XMLHttpRequest()
request.open(method, url)
request.responseType = 'arraybuffer'
// decode asynchronously
request.addEventListener('load', () => {
if (request.readyState === 4 && request.status === 200){
success(this.decode(request.response))
} else {
fail(request.status)
}
})
request.addEventListener('error', fail)
request.send(data)
})
}
/**
* Decode the bytes
* @param {String|ArrayBuffer} bytes The midi file encoded as a string or ArrayBuffer
* @return {Midi} this
*/
decode(bytes){
if (bytes instanceof ArrayBuffer){
var byteArray = new Uint8Array(bytes)
bytes = String.fromCharCode.apply(null, byteArray)
}
const midiData = Decoder(bytes)
this.header = parseHeader(midiData)
//replace the previous tracks
this.tracks = []
midiData.tracks.forEach((trackData) => {
const track = new Track()
this.tracks.push(track)
let absoluteTime = 0
trackData.forEach((event) => {
absoluteTime += Util.ticksToSeconds(event.deltaTime, this.header)
if (event.type === 'meta' && event.subtype === 'trackName'){
track.name = Util.cleanName(event.text)
} else if (event.subtype === 'noteOn'){
track.noteOn(event.noteNumber, absoluteTime, event.velocity / 127)
} else if (event.subtype === 'noteOff'){
track.noteOff(event.noteNumber, absoluteTime)
} else if (event.subtype === 'controller' && event.controllerType){
track.cc(event.controllerType, absoluteTime, event.value / 127)
} else if (event.type === 'meta' && event.subtype === 'instrumentName'){
track.instrument = event.text
}
})
})
return this
}
/**
* Encode the Midi object as a Buffer String
* @returns {String}
*/
encode(){
const output = new Encoder.File({
ticks : this.header.PPQ
})
this.tracks.forEach((track, i) => {
const trackEncoder = output.addTrack()
trackEncoder.setTempo(this.bpm)
track.encode(trackEncoder, this.header)
})
return output.toBytes()
}
/**
* Convert the output encoding into an Array
* @return {Array}
*/
toArray(){
const encodedStr = this.encode()
const buffer = new Array(encodedStr.length)
for (let i = 0; i < encodedStr.length; i++){
buffer[i] = encodedStr.charCodeAt(i)
}
return buffer
}
/**
* Add a new track.
* @param {String=} name Optionally include the name of the track
* @returns {Track}
*/
track(name){
const track = new Track(name)
this.tracks.push(track)
return track
}
/**
* Get a track either by it's name or track index
* @param {Number|String} trackName
* @return {Track}
*/
get(trackName){
if (Util.isNumber(trackName)){
return this.tracks[trackName]
} else {
return this.tracks.find((t) => t.name === trackName)
}
}
/**
* Slice the midi file between the startTime and endTime. Returns a copy of the
* midi
* @param {Number} startTime
* @param {Number} endTime
* @returns {Midi} this
*/
slice(startTime=0, endTime=this.duration){
const midi = new Midi()
midi.header = this.header
midi.tracks = this.tracks.map((t) => t.slice(startTime, endTime))
return midi
}
/**
* the time of the first event
* @type {Number}
*/
get startTime(){
const startTimes = this.tracks.map((t) => t.startTime)
return Math.min.apply(Math, startTimes)
}
/**
* The bpm of the midi file in beats per minute
* @type {Number}
*/
get bpm(){
return this.header.bpm
}
set bpm(bpm){
const prevTempo = this.header.bpm
this.header.bpm = bpm
//adjust the timing of all the notes
const ratio = prevTempo / bpm
this.tracks.forEach((track) => track.scale(ratio))
}
/**
* The timeSignature of the midi file
* @type {Array}
*/
get timeSignature(){
return this.header.timeSignature
}
set timeSignature(timeSig){
this.header.timeSignature = timeSignature
}
/**
* The duration is the end time of the longest track
* @type {Number}
*/
get duration(){
const durations = this.tracks.map((t) => t.duration)
return Math.max.apply(Math, durations)
}
}
export {Midi}

View File

@ -1,69 +0,0 @@
import {Midi} from './Midi'
const MidiConvert = {
/**
* Parse all the data from the Midi file into this format:
* {
* // the transport and timing data
* header : {
* bpm : Number, // tempo, e.g. 120
* timeSignature : [Number, Number], // time signature, e.g. [4, 4],
* PPQ : Number // PPQ of the midi file
* },
* // an array for each of the midi tracks
* tracks : [
* {
* name : String, // the track name if one was given
* notes : [
* {
* time : Number, // time in seconds
* name : String, // note name, e.g. 'C4'
* midi : Number, // midi number, e.g. 60
* velocity : Number, // normalized velocity
* duration : Number // duration between noteOn and noteOff
* }
* ],
* controlChanges : { //all of the control changes
* 64 : [ //array for each cc value
* {
* number : Number, //the cc number
* time : Number, //the time of the event in seconds
* name : String, // if the cc value has a common name (e.g. 'sustain')
* value : Number, //the normalized value
* }
* ]
* }
* }
* ]
* }
* @param {Binary String} fileBlob The output from fs.readFile or FileReader
* @returns {Object} All of the options parsed from the midi file.
*/
parse : function(fileBlob){
return new Midi().decode(fileBlob)
},
/**
* Load and parse a midi file. See `parse` for what the results look like.
* @param {String} url
* @param {Function=} callback
* @returns {Promise} A promise which is invoked with the returned Midi object
*/
load : function(url, callback){
const promise = new Midi().load(url)
if (callback){
promise.then(callback)
}
return promise
},
/**
* Create an empty midi file
* @return {Midi}
*/
create : function(){
return new Midi()
}
}
export default MidiConvert
module.exports = MidiConvert

View File

@ -1,100 +0,0 @@
import Util from './Util'
class Note{
constructor(midi, time, duration=0, velocity=1){
/**
* The MIDI note number
* @type {Number}
*/
this.midi;
if (Util.isNumber(midi)){
this.midi = midi
} else if (Util.isPitch(midi)){
this.name = midi
} else {
throw new Error('the midi value must either be in Pitch Notation (e.g. C#4) or a midi value')
}
/**
* The note on time in seconds
* @type {Number}
*/
this.time = time
/**
* The duration in seconds
* @type {Number}
*/
this.duration = duration
/**
* The velocity 0-1
* @type {Number}
*/
this.velocity = velocity
}
/**
* If the note is the same as the given note
* @param {String|Number} note
* @return {Boolean}
*/
match(note){
if (Util.isNumber(note)){
return this.midi === note
} else if (Util.isPitch(note)){
return this.name.toLowerCase() === note.toLowerCase()
}
}
/**
* The note in Scientific Pitch Notation
* @type {String}
*/
get name(){
return Util.midiToPitch(this.midi)
}
set name(name){
this.midi = Util.pitchToMidi(name)
}
/**
* Alias for time
* @type {Number}
*/
get noteOn(){
return this.time
}
set noteOn(t){
this.time = t
}
/**
* The note off time
* @type {Number}
*/
get noteOff(){
return this.time + this.duration
}
set noteOff(time){
this.duration = time - this.time
}
/**
* Convert the note to JSON
* @returns {Object}
*/
toJSON(){
return {
name : this.name,
midi : this.midi,
time : this.time,
velocity : this.velocity,
duration : this.duration
}
}
}
export {Note}

View File

@ -1,213 +0,0 @@
import {Note} from './Note'
import {Control} from './Control'
import {Merge} from './Merge'
import {BinaryInsert} from './BinaryInsert'
class Track {
constructor(name='', instrument=''){
/**
* The name of the track
* @type {String}
*/
this.name = name
/**
* The note events
* @type {Array}
*/
this.notes = []
/**
* The control changes
* @type {Object}
*/
this.controlChanges = {}
/**
* The tracks insturment if one exists
* @type {String}
*/
this.instrument = ''
}
note(midi, time, duration=0, velocity=1){
const note = new Note(midi, time, duration, velocity)
BinaryInsert(this.notes, note)
return this
}
/**
* Add a note on event
* @param {Number|String} midi The midi note as either a midi number or
* Pitch Notation like ('C#4')
* @param {Number} time The time in seconds
* @param {Number} velocity The velocity value 0-1
* @return {Track} this
*/
noteOn(midi, time, velocity=1){
const note = new Note(midi, time, 0, velocity)
BinaryInsert(this.notes, note)
return this
}
/**
* Add a note off event. Go through and find an unresolved
* noteOn event with the same pitch.
* @param {String|Number} midi The midi number or note name.
* @param {Number} time The time of the event in seconds
* @return {Track} this
*/
noteOff(midi, time){
for (let i = 0; i < this.notes.length; i++){
let note = this.notes[i]
if (note.match(midi) && note.duration === 0){
note.noteOff = time
break;
}
}
return this
}
/**
* Add a CC event
* @param {Number} num The CC number
* @param {Number} time The time of the event in seconds
* @param {Number} value The value of the CC
* @return {Track} this
*/
cc(num, time, value){
if (!this.controlChanges.hasOwnProperty(num)){
this.controlChanges[num] = []
}
const cc = new Control(num, time, value)
BinaryInsert(this.controlChanges[num], cc)
return this
}
/**
* An array of all the note on events
* @type {Array<Object>}
* @readOnly
*/
get noteOns(){
const noteOns = []
this.notes.forEach((note) => {
noteOns.push({
time : note.noteOn,
midi : note.midi,
name : note.name,
velocity : note.velocity
})
})
return noteOns
}
/**
* An array of all the noteOff events
* @type {Array<Object>}
* @readOnly
*/
get noteOffs(){
const noteOffs = []
this.notes.forEach((note) => {
noteOffs.push({
time : note.noteOff,
midi : note.midi,
name : note.name
})
})
return noteOffs
}
/**
* The length in seconds of the track
* @type {Number}
*/
get length() {
return this.notes.length
}
/**
* The time of the first event in seconds
* @type {Number}
*/
get startTime() {
if (this.notes.length){
let firstNote = this.notes[0]
return firstNote.noteOn
} else {
return 0
}
}
/**
* The time of the last event in seconds
* @type {Number}
*/
get duration() {
if (this.notes.length){
let lastNote = this.notes[this.notes.length - 1]
return lastNote.noteOff
} else {
return 0
}
}
/**
* Scale the timing of all the events in the track
* @param {Number} amount The amount to scale all the values
*/
scale(amount){
this.notes.forEach((note) => {
note.time *= amount
note.duration *= amount
})
return this
}
/**
* Slice returns a new track with only events that occured between startTime and endTime.
* Modifies this track.
* @param {Number} startTime
* @param {Number} endTime
* @returns {Track}
*/
slice(startTime=0, endTime=this.duration){
// get the index before the startTime
const noteStartIndex = Math.max(this.notes.findIndex((note) => note.time >= startTime), 0)
const noteEndIndex = this.notes.findIndex((note) => note.noteOff >= endTime) + 1
const track = new Track(this.name)
track.notes = this.notes.slice(noteStartIndex, noteEndIndex)
//shift the start time
track.notes.forEach((note) => note.time = note.time - startTime)
return track
}
/**
* Write the output to the stream
*/
encode(trackEncoder, header){
const ticksPerSecond = header.PPQ / (60 / header.bpm)
let lastEventTime = 0
const CHANNEL = 0
function getDeltaTime(time){
const ticks = Math.floor(ticksPerSecond * time)
const delta = Math.max(ticks - lastEventTime, 0)
lastEventTime = ticks
return delta
}
Merge(this.noteOns, (noteOn) => {
trackEncoder.addNoteOn(CHANNEL, noteOn.name, getDeltaTime(noteOn.time), Math.floor(noteOn.velocity * 127))
}, this.noteOffs, (noteOff) => {
trackEncoder.addNoteOff(CHANNEL, noteOff.name, getDeltaTime(noteOff.time))
})
}
}
export {Track}

View File

@ -1,53 +0,0 @@
function cleanName(str){
//ableton adds some weird stuff to the track
return str.replace(/\u0000/g, '')
}
function ticksToSeconds(ticks, header){
return (60 / header.bpm) * (ticks / header.PPQ);
}
function isNumber(val){
return typeof val === 'number'
}
function isString(val){
return typeof val === 'string'
}
const isPitch = (function(){
const regexp = /^([a-g]{1}(?:b|#|x|bb)?)(-?[0-9]+)/i
return (val) => {
return isString(val) && regexp.test(val)
}
}())
function midiToPitch(midi){
const scaleIndexToNote = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
const octave = Math.floor(midi / 12) - 1;
const note = midi % 12;
return scaleIndexToNote[note] + octave;
}
const pitchToMidi = (function(){
const regexp = /^([a-g]{1}(?:b|#|x|bb)?)(-?[0-9]+)/i
const noteToScaleIndex = {
"cbb" : -2, "cb" : -1, "c" : 0, "c#" : 1, "cx" : 2,
"dbb" : 0, "db" : 1, "d" : 2, "d#" : 3, "dx" : 4,
"ebb" : 2, "eb" : 3, "e" : 4, "e#" : 5, "ex" : 6,
"fbb" : 3, "fb" : 4, "f" : 5, "f#" : 6, "fx" : 7,
"gbb" : 5, "gb" : 6, "g" : 7, "g#" : 8, "gx" : 9,
"abb" : 7, "ab" : 8, "a" : 9, "a#" : 10, "ax" : 11,
"bbb" : 9, "bb" : 10, "b" : 11, "b#" : 12, "bx" : 13,
}
return (note) => {
const split = regexp.exec(note)
const pitch = split[1]
const octave = split[2]
const index = noteToScaleIndex[pitch.toLowerCase()]
return index + (parseInt(octave) + 1) * 12
}
}())
module.exports = {cleanName, ticksToSeconds, isString, isNumber, isPitch, midiToPitch, pitchToMidi}