UNPKG

drum-machine

Version:

A simple drum machine / sequencer written in javascript

215 lines (180 loc) 6.11 kB
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;