drum-machine
Version:
A simple drum machine / sequencer written in javascript
215 lines (180 loc) • 6.11 kB
JavaScript
const WAAClock = require('waaclock');
const trackerTable = require('./tracker-table');
const hasClass = require('has-class');
/**
* Construct object
* @param {audioContext} ctx
* @param {function} scheduleAudioBeat funtion when an audio is played
*/
function tracker(ctx, scheduleAudioBeat) {
this.measureLength = 16;
this.scheduleAudioBeat = scheduleAudioBeat;
this.scheduleForward = 0.1;
this.current = 0;
this.eventMap = {};
this.clock = new WAAClock(ctx);
this.clock.start();
this.running = false;
/**
* Draw a tracker table by numRows and numCols
*/
this.drawTracker = function(numRows, numCols, data) {
let htmlTable = new trackerTable();
htmlTable.setRows(numRows, numCols, data);
let str = htmlTable.getTable();
let t = document.getElementById('tracker-parent');
t.innerHTML = '';
t.insertAdjacentHTML('afterbegin', str);
}
/**
* Push current beat one forward
*/
this.next = function () {
this.current++;
if (this.current >= this.measureLength) {
this.current = 0;
}
};
/**
* Calculate milli seconds per beat
*/
this.milliPerBeat = function (beats) {
if (!beats) {
beats = 60;
}
return 1000 * 60 / beats;
};
/**
* Get a tracker row from a cell-id
*/
this.getTrackerRowValues = function (colId) {
let values = [];
let selector = `[data-col-id="${colId}"]`;
let elems = document.querySelectorAll(selector);
elems.forEach((el) => {
let val = Object.assign({}, el.dataset);
val.enabled = el.classList.contains('tracker-enabled');
values.push(val);
});
return values;
};
/**
* Schedule a beat column
*/
this.schedule = function () {
let beatColumn = this.getTrackerRowValues(this.current);
let now = ctx.currentTime;
let selector = `[data-col-id="${this.current}"]`;
let event = this.clock.callbackAtTime(() => {
let elems = document.querySelectorAll(selector);
elems.forEach( (e) => {
e.classList.add('tracker-current')
})
}, now + this.scheduleForward);
this.clock.callbackAtTime(() => {
let elems = document.querySelectorAll(selector);
elems.forEach( (e) => {
e.classList.remove('tracker-current')
})
}, now + this.scheduleForward + this.milliPerBeat(this.bpm) / 1000);
beatColumn.forEach((beat) => {
this.scheduleBeat(beat, now);
});
};
this.scheduleBeat = function (beat, now) {
let triggerTime = now + this.scheduleForward;
this.scheduleMap[beat.colId] = triggerTime;
if (beat.enabled) {
this.eventMap[this.getEventKey(beat)] = this.clock.callbackAtTime(() => {
this.scheduleAudioBeat(beat, triggerTime);
}, now);
}
};
this.scheduleMap = {};
this.scheduleAudioBeatNow = function (beat) {
if (beat.enabled) {
let beatEvent = this.eventMap[this.getEventKey(beat)];
if (beatEvent) {
beatEvent.clear();
delete this.eventMap[this.getEventKey(beat)];
}
return;
}
let triggerTime = this.scheduleMap[0] + beat.colId * this.milliPerBeat(this.bpm) / 1000;
let now = ctx.currentTime;
this.eventMap[this.getEventKey(beat)] = this.clock.callbackAtTime(() => {
this.scheduleAudioBeat(beat, triggerTime);
}, now);
};
this.interval;
this.runSchedule = function (bpm) {
this.running = true;
this.bpm = bpm;
let interval = this.milliPerBeat(bpm);
setTimeout(() => {
this.schedule();
this.next();
}, 0);
this.interval = setInterval(() => {
this.schedule();
this.next();
}, interval);
};
this.stop = function () {
this.running = false;
clearInterval(this.interval);
};
this.getEventKey = function getEventKey(beat) {
return beat.rowId + beat.colId;
};
/**
* Get tracker values
*/
this.getTrackerValues = function () {
let values = [];
let elems = document.querySelectorAll('.tracker-cell');
elems.forEach(function (e) {
let val = Object.assign({}, e.dataset);
val.enabled = hasClass(e, "tracker-enabled");
values.push(val);
});
return values;
};
/**
* Load tracker values in JSON format
*/
this.loadTrackerValues = function (json) {
let elems = document.querySelectorAll('.tracker-enabled');
elems.forEach(function(e) {
e.classList.remove('tracker-enabled');
});
json.forEach(function (data) {
if (data.enabled === true) {
let selector = `.tracker-cell[data-row-id="${data.rowId}"][data-col-id="${data.colId}"]`;
let elem = document.querySelector(selector);
if (elem) {
elem.classList.add("tracker-enabled");
}
}
});
};
/**
* Listen on tracker-cell
* Schedule if cell is clicked and toggle css class
*/
this.setupEvents = function () {
let elems = document.querySelectorAll('.tracker-cell');
elems.forEach(function (e) {
e.addEventListener('click', function(e) {
let val = Object.assign({}, e.target.dataset);
val.enabled = hasClass(e.target, "tracker-enabled");
let currentBeat = e.target.dataset.colId;
if (val.colId > currentBeat) {
this.scheduleAudioBeatNow(val);
}
e.target.classList.toggle('tracker-enabled');
})
})
}
}
module.exports = tracker;