qambi
Version:
MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio
364 lines (285 loc) • 8.96 kB
JavaScript
import {Instrument} from './instrument';
import {Track} from './track'
import {Part} from './part'
import {parseEvents, parseMIDINotes} from './parse_events'
import {MIDIEvent} from './midi_event'
import {checkMIDINumber} from './util'
import {calculatePosition} from './position'
import {Sampler} from './sampler'
import {getInitData} from './init_audio'
import {MIDIEventTypes} from './constants'
import {sortEvents} from './util'
let
methodMap = new Map([
['volume', 'setVolume'],
['instrument', 'setInstrument'],
['noteNumberAccentedTick', 'setNoteNumberAccentedTick'],
['noteNumberNonAccentedTick', 'setNoteNumberNonAccentedTick'],
['velocityAccentedTick', 'setVelocityAccentedTick'],
['velocityNonAccentedTick', 'setVelocityNonAccentedTick'],
['noteLengthAccentedTick', 'setNoteLengthAccentedTick'],
['noteLengthNonAccentedTick', 'setNoteLengthNonAccentedTick']
]);
export class Metronome{
constructor(song){
this.song = song
this.track = new Track({name: this.song.id + '_metronome'})
this.part = new Part()
this.track.addParts(this.part)
this.track._gainNode.connect(this.song._gainNode)
this.events = []
this.precountEvents = []
this.precountDuration = 0
this.bars = 0
this.index = 0
this.index2 = 0
this.precountIndex = 0
this.reset();
}
reset(){
let data = getInitData()
let instrument = new Sampler('metronome')
instrument.updateSampleData({
note: 60,
buffer: data.lowtick,
}, {
note: 61,
buffer: data.hightick,
})
this.track.setInstrument(instrument)
this.volume = 1
this.noteNumberAccented = 61
this.noteNumberNonAccented = 60
this.velocityAccented = 100
this.velocityNonAccented = 100
this.noteLengthAccented = this.song.ppq / 4 // sixteenth notes -> don't make this too short if your sample has a long attack!
this.noteLengthNonAccented = this.song.ppq / 4
}
createEvents(startBar, endBar, id = 'init'){
let i, j
let position
let velocity
let noteLength
let noteNumber
let beatsPerBar
let ticksPerBeat
let ticks = 0
let noteOn, noteOff
let events = []
//console.log(startBar, endBar);
for(i = startBar; i <= endBar; i++){
position = calculatePosition(this.song, {
type: 'barsbeats',
target: [i],
})
beatsPerBar = position.nominator
ticksPerBeat = position.ticksPerBeat
ticks = position.ticks
for(j = 0; j < beatsPerBar; j++){
noteNumber = j === 0 ? this.noteNumberAccented : this.noteNumberNonAccented
noteLength = j === 0 ? this.noteLengthAccented : this.noteLengthNonAccented
velocity = j === 0 ? this.velocityAccented : this.velocityNonAccented
noteOn = new MIDIEvent(ticks, 144, noteNumber, velocity)
noteOff = new MIDIEvent(ticks + noteLength, 128, noteNumber, 0)
if(id === 'precount'){
noteOn._track = this.track
noteOff._track = this.track
noteOn._part = {}
noteOff._part = {}
}
events.push(noteOn, noteOff)
ticks += ticksPerBeat
}
}
return events
}
getEvents(startBar = 1, endBar = this.song.bars, id = 'init'){
this.part.removeEvents(this.part.getEvents())
this.events = this.createEvents(startBar, endBar, id)
this.part.addEvents(...this.events)
this.bars = this.song.bars
//console.log('getEvents %O', this.events)
this.allEvents = [...this.events, ...this.song._timeEvents]
// console.log(this.allEvents)
sortEvents(this.allEvents)
parseMIDINotes(this.events)
return this.events
}
setIndex2(millis){
this.index2 = 0
}
getEvents2(maxtime, timeStamp){
let result = []
for(let i = this.index2, maxi = this.allEvents.length; i < maxi; i++){
let event = this.allEvents[i]
if(event.type === MIDIEventTypes.TEMPO || event.type === MIDIEventTypes.TIME_SIGNATURE){
if(event.millis < maxtime){
this.millisPerTick = event.millisPerTick
this.index2++
}else{
break
}
}else{
let millis = event.ticks * this.millisPerTick
if(millis < maxtime){
event.time = millis + timeStamp
event.millis = millis
result.push(event)
this.index2++
}else{
break
}
}
}
return result
}
addEvents(startBar = 1, endBar = this.song.bars, id = 'add'){
// console.log(startBar, endBar)
let events = this.createEvents(startBar, endBar, id)
this.events.push(...events)
this.part.addEvents(...events)
this.bars = endBar
//console.log('getEvents %O', this.events, endBar)
return events
}
createPrecountEvents(startBar, endBar, timeStamp){
this.timeStamp = timeStamp
// let songStartPosition = this.song.getPosition()
let songStartPosition = calculatePosition(this.song, {
type: 'barsbeats',
target: [startBar],
result: 'millis',
})
//console.log('starBar', songStartPosition.bar)
let endPos = calculatePosition(this.song, {
type: 'barsbeats',
//target: [songStartPosition.bar + precount, songStartPosition.beat, songStartPosition.sixteenth, songStartPosition.tick],
target: [endBar],
result: 'millis',
})
//console.log(songStartPosition, endPos)
this.precountIndex = 0
this.startMillis = songStartPosition.millis
this.endMillis = endPos.millis
this.precountDuration = endPos.millis - this.startMillis
// do this so you can start precounting at any position in the song
this.timeStamp -= this.startMillis
//console.log(this.precountDuration, this.startMillis, this.endMillis)
this.precountEvents = this.createEvents(startBar, endBar - 1, 'precount');
this.precountEvents = parseEvents([...this.song._timeEvents, ...this.precountEvents])
//console.log(songStartPosition.bar, endPos.bar, precount, this.precountEvents.length);
//console.log(this.precountEvents.length, this.precountDuration);
return this.precountDuration
}
setPrecountIndex(millis){
let i = 0;
for(let event of this.events){
if(event.millis >= millis){
this.precountIndex = i;
break;
}
i++;
}
console.log(this.precountIndex)
}
// called by scheduler.js
getPrecountEvents(maxtime){
let events = this.precountEvents,
maxi = events.length, i, evt,
result = [];
//maxtime += this.precountDuration
for(i = this.precountIndex; i < maxi; i++){
evt = events[i];
//console.log(event.millis, maxtime, this.millis);
if(evt.millis < maxtime){
evt.time = this.timeStamp + evt.millis
result.push(evt)
this.precountIndex++
}else{
break;
}
}
//console.log(result.length);
return result;
}
mute(flag){
this.track.muted = flag
}
allNotesOff(){
this.track._instrument.allNotesOff()
}
// =========== CONFIGURATION ===========
updateConfig(){
this.init(1, this.bars, 'update')
this.allNotesOff()
this.song.update()
}
// added to public API: Song.configureMetronome({})
configure(config){
Object.keys(config).forEach(function(key){
this[methodMap.get(key)](config.key);
}, this);
this.updateConfig();
}
setInstrument(instrument){
if(!instrument instanceof Instrument){
console.warn('not an instance of Instrument')
return
}
this.track.setInstrument(instrument)
this.updateConfig();
}
setNoteLengthAccentedTick(value){
if(isNaN(value)){
console.warn('please provide a number');
}
this.noteLengthAccented = value;
this.updateConfig();
}
setNoteLengthNonAccentedTick(value){
if(isNaN(value)){
console.warn('please provide a number');
}
this.noteLengthNonAccented = value;
this.updateConfig();
}
setVelocityAccentedTick(value){
value = checkMIDINumber(value);
if(value !== false){
this.velocityAccented = value;
}else{
console.warn('please provide a number');
}
this.updateConfig();
}
setVelocityNonAccentedTick(value){
value = checkMIDINumber(value);
if(value !== false){
this.velocityNonAccented = value;
}else{
console.warn('please provide a number');
}
this.updateConfig();
}
setNoteNumberAccentedTick(value){
value = checkMIDINumber(value);
if(value !== false){
this.noteNumberAccented = value;
}else{
console.warn('please provide a number');
}
this.updateConfig();
}
setNoteNumberNonAccentedTick(value){
value = checkMIDINumber(value);
if(value !== false){
this.noteNumberNonAccented = value;
}else{
console.warn('please provide a number');
}
this.updateConfig();
}
setVolume(value){
this.track.setVolume(value);
}
}