updating deps and latest MidiConvert
This commit is contained in:
parent
a0d4af1092
commit
d8a0e5d876
@ -11,23 +11,27 @@
|
|||||||
"autoprefixer-loader": "^3.2.0",
|
"autoprefixer-loader": "^3.2.0",
|
||||||
"babel-core": "^6.17.0",
|
"babel-core": "^6.17.0",
|
||||||
"babel-loader": "^6.2.5",
|
"babel-loader": "^6.2.5",
|
||||||
|
"babel-polyfill": "^6.20.0",
|
||||||
"babel-preset-es2015": "^6.16.0",
|
"babel-preset-es2015": "^6.16.0",
|
||||||
"buckets-js": "^1.98.1",
|
"buckets-js": "^1.98.1",
|
||||||
"css-loader": "^0.23.1",
|
"css-loader": "^0.23.1",
|
||||||
"domready": "^1.0.8",
|
"domready": "^1.0.8",
|
||||||
"events": "^1.1.0",
|
"events": "^1.1.0",
|
||||||
|
"exports-loader": "^0.6.3",
|
||||||
"file-loader": "^0.9.0",
|
"file-loader": "^0.9.0",
|
||||||
"jsmidgen": "^0.1.5",
|
"jsmidgen": "^0.1.5",
|
||||||
"midi-file-parser": "^1.0.0",
|
"midi-file-parser": "^1.0.0",
|
||||||
|
"midiconvert": "^0.4.1",
|
||||||
"node-sass": "^3.4.2",
|
"node-sass": "^3.4.2",
|
||||||
"pepjs": "^0.4.2",
|
"pepjs": "^0.4.2",
|
||||||
"sass-loader": "^3.2.0",
|
"sass-loader": "^3.2.0",
|
||||||
"startaudiocontext": "^1.2.0",
|
"startaudiocontext": "^1.2.0",
|
||||||
"style-loader": "^0.13.1",
|
"style-loader": "^0.13.1",
|
||||||
"three": "^0.81.2",
|
"three": "^0.83.0",
|
||||||
"tone": "^0.8.0",
|
"tone": "^0.9.0",
|
||||||
"url-loader": "^0.5.7",
|
"url-loader": "^0.5.7",
|
||||||
"webmidi": "^2.0.0-beta.1",
|
"webmidi": "^2.0.0-beta.1",
|
||||||
"webpack": "^1.12.14"
|
"webpack": "^1.12.14",
|
||||||
|
"youtube-iframe": "^1.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
static/third_party/MidiConvert/LICENSE.md
vendored
21
static/third_party/MidiConvert/LICENSE.md
vendored
@ -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.
|
|
112
static/third_party/MidiConvert/README.md
vendored
112
static/third_party/MidiConvert/README.md
vendored
@ -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.
|
|
@ -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}
|
|
39
static/third_party/MidiConvert/src/Control.js
vendored
39
static/third_party/MidiConvert/src/Control.js
vendored
@ -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}
|
|
29
static/third_party/MidiConvert/src/Header.js
vendored
29
static/third_party/MidiConvert/src/Header.js
vendored
@ -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}
|
|
44
static/third_party/MidiConvert/src/Merge.js
vendored
44
static/third_party/MidiConvert/src/Merge.js
vendored
@ -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}
|
|
205
static/third_party/MidiConvert/src/Midi.js
vendored
205
static/third_party/MidiConvert/src/Midi.js
vendored
@ -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}
|
|
@ -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
|
|
100
static/third_party/MidiConvert/src/Note.js
vendored
100
static/third_party/MidiConvert/src/Note.js
vendored
@ -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}
|
|
213
static/third_party/MidiConvert/src/Track.js
vendored
213
static/third_party/MidiConvert/src/Track.js
vendored
@ -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}
|
|
53
static/third_party/MidiConvert/src/Util.js
vendored
53
static/third_party/MidiConvert/src/Util.js
vendored
@ -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}
|
|
Loading…
Reference in New Issue
Block a user