qambi
Version:
MIDI sequencer, loads MIDI files, can record and playback MIDI, uses WebMIDI and WebAudio
643 lines (518 loc) • 16.3 kB
JavaScript
;
import {getNiceTime} from './util';
const
supportedTypes = 'barsandbeats barsbeats time millis ticks perc percentage',
supportedReturnTypes = 'barsandbeats barsbeats time millis ticks all',
floor = Math.floor,
round = Math.round;
let
//local
bpm,
nominator,
denominator,
ticksPerBeat,
ticksPerBar,
ticksPerSixteenth,
millisPerTick,
secondsPerTick,
numSixteenth,
ticks,
millis,
diffTicks,
diffMillis,
bar,
beat,
sixteenth,
tick,
// type,
index,
returnType = 'all',
beyondEndOfSong = true;
function getTimeEvent(song, unit, target){
// finds the time event that comes the closest before the target position
let timeEvents = song._timeEvents
//console.log(song._timeEvents, unit, target)
for(let i = timeEvents.length - 1; i >= 0; i--){
let event = timeEvents[i];
//console.log(unit, target, event)
if(event[unit] <= target){
index = i
return event
}
}
return null
}
export function millisToTicks(song, targetMillis, beos = true){
beyondEndOfSong = beos
fromMillis(song, targetMillis)
//return round(ticks);
return ticks
}
export function ticksToMillis(song, targetTicks, beos = true){
beyondEndOfSong = beos
fromTicks(song, targetTicks)
return millis
}
export function barsToMillis(song, position, beos){ // beos = beyondEndOfSong
calculatePosition(song, {
type: 'barsbeat',
position,
result: 'millis',
beos,
})
return millis
}
export function barsToTicks(song, position, beos){ // beos = beyondEndOfSong
calculatePosition(song, {
type: 'barsbeats',
position,
result: 'ticks',
beos
})
//return round(ticks);
return ticks
}
export function ticksToBars(song, target, beos = true){
beyondEndOfSong = beos
fromTicks(song, target)
calculateBarsAndBeats()
returnType = 'barsandbeats'
return getPositionData()
}
export function millisToBars(song, target, beos = true){
beyondEndOfSong = beos
fromMillis(song, target)
calculateBarsAndBeats()
returnType = 'barsandbeats'
return getPositionData()
}
// main calculation function for millis position
function fromMillis(song, targetMillis, event){
let lastEvent = song._lastEvent;
if(beyondEndOfSong === false){
if(targetMillis > lastEvent.millis){
targetMillis = lastEvent.millis;
}
}
if(typeof event === 'undefined'){
event = getTimeEvent(song, 'millis', targetMillis);
}
//console.log(event)
getDataFromEvent(event);
// if the event is not exactly at target millis, calculate the diff
if(event.millis === targetMillis){
diffMillis = 0;
diffTicks = 0;
}else{
diffMillis = targetMillis - event.millis;
diffTicks = diffMillis / millisPerTick;
}
millis += diffMillis;
ticks += diffTicks;
return ticks;
}
// main calculation function for ticks position
function fromTicks(song, targetTicks, event){
let lastEvent = song._lastEvent;
if(beyondEndOfSong === false){
if(targetTicks > lastEvent.ticks){
targetTicks = lastEvent.ticks;
}
}
if(typeof event === 'undefined'){
event = getTimeEvent(song, 'ticks', targetTicks);
}
//console.log(event)
getDataFromEvent(event);
// if the event is not exactly at target ticks, calculate the diff
if(event.ticks === targetTicks){
diffTicks = 0;
diffMillis = 0;
}else{
diffTicks = targetTicks - ticks;
diffMillis = diffTicks * millisPerTick;
}
ticks += diffTicks;
millis += diffMillis;
return millis;
}
// main calculation function for bars and beats position
function fromBars(song, targetBar, targetBeat, targetSixteenth, targetTick, event = null){
//console.time('fromBars');
let i = 0,
diffBars,
diffBeats,
diffSixteenth,
diffTick,
lastEvent = song._lastEvent;
if(beyondEndOfSong === false){
if(targetBar > lastEvent.bar){
targetBar = lastEvent.bar;
}
}
if(event === null){
event = getTimeEvent(song, 'bar', targetBar);
}
//console.log(event)
getDataFromEvent(event);
//correct wrong position data, for instance: '3,3,2,788' becomes '3,4,4,068' in a 4/4 measure at PPQ 480
while(targetTick >= ticksPerSixteenth){
targetSixteenth++;
targetTick -= ticksPerSixteenth;
}
while(targetSixteenth > numSixteenth){
targetBeat++;
targetSixteenth -= numSixteenth;
}
while(targetBeat > nominator){
targetBar++;
targetBeat -= nominator;
}
event = getTimeEvent(song, 'bar', targetBar, index);
for(i = index; i >= 0; i--){
event = song._timeEvents[i];
if(event.bar <= targetBar){
getDataFromEvent(event);
break;
}
}
// get the differences
diffTick = targetTick - tick;
diffSixteenth = targetSixteenth - sixteenth;
diffBeats = targetBeat - beat;
diffBars = targetBar - bar; //bar is always less then or equal to targetBar, so diffBars is always >= 0
//console.log('diff',diffBars,diffBeats,diffSixteenth,diffTick);
//console.log('millis',millis,ticksPerBar,ticksPerBeat,ticksPerSixteenth,millisPerTick);
// convert differences to milliseconds and ticks
diffMillis = (diffBars * ticksPerBar) * millisPerTick;
diffMillis += (diffBeats * ticksPerBeat) * millisPerTick;
diffMillis += (diffSixteenth * ticksPerSixteenth) * millisPerTick;
diffMillis += diffTick * millisPerTick;
diffTicks = diffMillis / millisPerTick;
//console.log(diffBars, ticksPerBar, millisPerTick, diffMillis, diffTicks);
// set all current position data
bar = targetBar;
beat = targetBeat;
sixteenth = targetSixteenth;
tick = targetTick;
//console.log(tick, targetTick)
millis += diffMillis;
//console.log(targetBar, targetBeat, targetSixteenth, targetTick, ' -> ', millis);
ticks += diffTicks;
//console.timeEnd('fromBars');
}
function calculateBarsAndBeats(){
// spread the difference in tick over bars, beats and sixteenth
let tmp = round(diffTicks);
while(tmp >= ticksPerSixteenth){
sixteenth++;
tmp -= ticksPerSixteenth;
while(sixteenth > numSixteenth){
sixteenth -= numSixteenth;
beat++;
while(beat > nominator){
beat -= nominator;
bar++;
}
}
}
tick = round(tmp);
}
// store properties of event in local scope
function getDataFromEvent(event){
bpm = event.bpm;
nominator = event.nominator;
denominator = event.denominator;
ticksPerBar = event.ticksPerBar;
ticksPerBeat = event.ticksPerBeat;
ticksPerSixteenth = event.ticksPerSixteenth;
numSixteenth = event.numSixteenth;
millisPerTick = event.millisPerTick;
secondsPerTick = event.secondsPerTick;
bar = event.bar;
beat = event.beat;
sixteenth = event.sixteenth;
tick = event.tick;
ticks = event.ticks;
millis = event.millis;
//console.log(bpm, event.type);
//console.log('ticks', ticks, 'millis', millis, 'bar', bar);
}
function getPositionData(song){
let timeData,
positionData = {};
switch(returnType){
case 'millis':
//positionData.millis = millis;
positionData.millis = round(millis * 1000) / 1000;
positionData.millisRounded = round(millis);
break;
case 'ticks':
//positionData.ticks = ticks;
positionData.ticks = round(ticks);
//positionData.ticksUnrounded = ticks;
break;
case 'barsbeats':
case 'barsandbeats':
positionData.bar = bar;
positionData.beat = beat;
positionData.sixteenth = sixteenth;
positionData.tick = tick;
//positionData.barsAsString = (bar + 1) + ':' + (beat + 1) + ':' + (sixteenth + 1) + ':' + tickAsString;
positionData.barsAsString = bar + ':' + beat + ':' + sixteenth + ':' + getTickAsString(tick);
break;
case 'time':
timeData = getNiceTime(millis);
positionData.hour = timeData.hour;
positionData.minute = timeData.minute;
positionData.second = timeData.second;
positionData.millisecond = timeData.millisecond;
positionData.timeAsString = timeData.timeAsString;
break;
case 'all':
// millis
//positionData.millis = millis;
positionData.millis = round(millis * 1000) / 1000;
positionData.millisRounded = round(millis);
// ticks
//positionData.ticks = ticks;
positionData.ticks = round(ticks);
//positionData.ticksUnrounded = ticks;
// barsbeats
positionData.bar = bar;
positionData.beat = beat;
positionData.sixteenth = sixteenth;
positionData.tick = tick;
//positionData.barsAsString = (bar + 1) + ':' + (beat + 1) + ':' + (sixteenth + 1) + ':' + tickAsString;
positionData.barsAsString = bar + ':' + beat + ':' + sixteenth + ':' + getTickAsString(tick);
// time
timeData = getNiceTime(millis);
positionData.hour = timeData.hour;
positionData.minute = timeData.minute;
positionData.second = timeData.second;
positionData.millisecond = timeData.millisecond;
positionData.timeAsString = timeData.timeAsString;
// extra data
positionData.bpm = round(bpm * song.playbackSpeed, 3);
positionData.nominator = nominator;
positionData.denominator = denominator;
positionData.ticksPerBar = ticksPerBar;
positionData.ticksPerBeat = ticksPerBeat;
positionData.ticksPerSixteenth = ticksPerSixteenth;
positionData.numSixteenth = numSixteenth;
positionData.millisPerTick = millisPerTick;
positionData.secondsPerTick = secondsPerTick;
// use ticks to make tempo changes visible by a faster moving playhead
positionData.percentage = ticks / song._durationTicks;
//positionData.percentage = millis / song.durationMillis;
break;
default:
return null
}
return positionData
}
function getTickAsString(t){
if(t === 0){
t = '000'
}else if(t < 10){
t = '00' + t
}else if(t < 100){
t = '0' + t
}
return t
}
// used by playhead
export function getPosition2(song, unit, target, type, event){
if(unit === 'millis'){
fromMillis(song, target, event);
}else if(unit === 'ticks'){
fromTicks(song, target, event);
}
returnType = type
if(returnType === 'all'){
calculateBarsAndBeats();
}
return getPositionData(song);
}
// improved version of getPosition
export function calculatePosition(song, settings){
let {
type, // any of barsandbeats barsbeats time millis ticks perc percentage
target, // if type is barsbeats or time, target must be an array, else if must be a number
result: result = 'all', // any of barsandbeats barsbeats time millis ticks all
beos: beos = true,
snap: snap = -1
} = settings
if(supportedReturnTypes.indexOf(result) === -1){
console.warn(`unsupported return type, 'all' used instead of '${result}'`)
result = 'all'
}
returnType = result
beyondEndOfSong = beos
if(supportedTypes.indexOf(type) === -1){
console.error(`unsupported type ${type}`)
return false
}
switch(type){
case 'barsbeats':
case 'barsandbeats':
let [targetbar = 1, targetbeat = 1, targetsixteenth = 1, targettick = 0] = target
//console.log(targetbar, targetbeat, targetsixteenth, targettick)
fromBars(song, targetbar, targetbeat, targetsixteenth, targettick)
return getPositionData(song)
case 'time':
// calculate millis out of time array: hours, minutes, seconds, millis
let [targethour = 0, targetminute = 0, targetsecond = 0, targetmillisecond = 0] = target
let millis = 0
millis += targethour * 60 * 60 * 1000 //hours
millis += targetminute * 60 * 1000 //minutes
millis += targetsecond * 1000 //seconds
millis += targetmillisecond //milliseconds
fromMillis(song, millis)
calculateBarsAndBeats()
return getPositionData(song)
case 'millis':
fromMillis(song, target)
calculateBarsAndBeats()
return getPositionData(song)
case 'ticks':
//console.log(song, target)
fromTicks(song, target)
calculateBarsAndBeats()
return getPositionData(song)
case 'perc':
case 'percentage':
//millis = position[1] * song.durationMillis;
//fromMillis(song, millis);
//console.log(millis);
ticks = target * song._durationTicks // target must be in ticks!
//console.log(ticks, song._durationTicks)
if(snap !== -1){
ticks = floor(ticks / snap) * snap;
//fromTicks(song, ticks);
//console.log(ticks);
}
fromTicks(song, ticks)
calculateBarsAndBeats()
let tmp = getPositionData(song)
//console.log('diff', position[1] - tmp.percentage);
return tmp
default:
return false
}
}
/*
//@param: 'millis', 1000, [true]
//@param: 'ticks', 1000, [true]
//@param: 'barsandbeats', 1, ['all', true]
//@param: 'barsandbeats', 60, 4, 3, 120, ['all', true]
//@param: 'barsandbeats', 60, 4, 3, 120, [true, 'all']
function checkPosition(type, args, returnType = 'all'){
beyondEndOfSong = true;
console.log('----> checkPosition:', args, typeString(args));
if(typeString(args) === 'array'){
let
numArgs = args.length,
position,
i, a, positionLength;
type = args[0];
// support for [['millis', 3000]]
if(typeString(args[0]) === 'array'){
//console.warn('this shouldn\'t happen!');
args = args[0];
type = args[0];
numArgs = args.length;
}
position = [type];
console.log('check position', args, numArgs, supportedTypes.indexOf(type));
//console.log('arg', 0, '->', type);
if(supportedTypes.indexOf(type) !== -1){
for(i = 1; i < numArgs; i++){
a = args[i];
//console.log('arg', i, '->', a);
if(a === true || a === false){
beyondEndOfSong = a;
}else if(isNaN(a)){
if(supportedReturnTypes.indexOf(a) !== -1){
returnType = a;
}else{
return false;
}
}else {
position.push(a);
}
}
//check number of arguments -> either 1 number or 4 numbers in position, e.g. ['barsbeats', 1] or ['barsbeats', 1, 1, 1, 0],
// or ['perc', 0.56, numberOfTicksToSnapTo]
positionLength = position.length;
if(positionLength !== 2 && positionLength !== 3 && positionLength !== 5){
return false;
}
//console.log(position, returnType, beyondEndOfSong);
//console.log('------------------------------------')
return position;
}
}
return false;
}
export function getPosition(song, type, args){
//console.log('getPosition', args);
if(typeof args === 'undefined'){
return {
millis: 0
}
}
let position = checkPosition(type, args),
millis, tmp, snap;
if(position === false){
error('wrong position data');
return false;
}
switch(type){
case 'barsbeats':
case 'barsandbeats':
fromBars(song, position[1], position[2], position[3], position[4]);
return getPositionData(song);
case 'time':
// calculate millis out of time array: hours, minutes, seconds, millis
millis = 0;
tmp = position[1] || 0;
millis += tmp * 60 * 60 * 1000; //hours
tmp = position[2] || 0;
millis += tmp * 60 * 1000; //minutes
tmp = position[3] || 0;
millis += tmp * 1000; //seconds
tmp = position[4] || 0;
millis += tmp; //milliseconds
fromMillis(song, millis);
calculateBarsAndBeats();
return getPositionData(song);
case 'millis':
fromMillis(song, position[1]);
calculateBarsAndBeats();
return getPositionData(song);
case 'ticks':
fromTicks(song, position[1]);
calculateBarsAndBeats();
return getPositionData(song);
case 'perc':
case 'percentage':
snap = position[2];
//millis = position[1] * song.durationMillis;
//fromMillis(song, millis);
//console.log(millis);
ticks = position[1] * song.durationTicks;
if(snap !== undefined){
ticks = floor(ticks/snap) * snap;
//fromTicks(song, ticks);
//console.log(ticks);
}
fromTicks(song, ticks);
calculateBarsAndBeats();
tmp = getPositionData(song);
//console.log('diff', position[1] - tmp.percentage);
return tmp;
}
return false;
}
*/