Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | // Lots of code an ideas taken from here: // https://github.com/cwilso/metronome import { SoundSystem } from "./digital/audio"; export type BeatInfo = { measure: number, beat: number, tick: number, } export type Tempo = { /** PPQ (pulses per quarter note), which is defined in the header of a midi file, once. */ ppq: number, /** Tempo (in microseconds per quarter note), which is defined by "Set Tempo" * meta events and can change during the musical piece. */ tempo: number, /** What a musician thinks of as an actual BPM (80,160,120,etc) */ bpm: number, }; export type PerTickCallback = (beatNumber: number, time: number, beatInfo: BeatInfo) => [Tempo, Signature]; /** * noteBeats / perMeasure * * 2/4: Two quarter-note beats per measure. * * 4/4: Four quarter-note beats per measure. Also known as common time and notated with a "C." * * 6/8: Six eighth-note beats per measure */ export type Signature = { /** The top number. Reveals how many beats are in each measure. Numerator of time signature */ noteBeats: number, /** The bottom number of the time signature. Indicates a certain kind of note used to count the beat */ perMeasure: number, /** Number of ticks in metronome click (from midi). For example: 24 */ ticksInMetronome?: number, /** Number of 32nd notes to the quarter note (from midi). For example: 8 */ toQuarter32nd?: number, } export type TickCallbackContainer = { /** if you want to do something to a run while playing, * add a callback here. * * _WARNING_: the last item in this array will win when changing * tempo or time signature */ tickListeners: PerTickCallback[]; } /** Master sound system */ let system: SoundSystem; /** Are we currently playing? */ let isPlaying = false; /** How frequently to call scheduling function (in milliseconds) */ let lookahead = 25.0; /** How far ahead to schedule audio (sec) * This is calculated from lookahead, and overlaps * with next interval (in case the timer is late) */ let scheduleAheadTime = 0.1; /** when the next note is due. */ let nextNoteTime = 0.0; /** The Web Worker used to fire timer messages */ let timerWorker: Worker; /** Running beat info struct. Used mostly for display * and debug as most music will use "ticks" */ let beatInfo: BeatInfo; /** Tick increment. Just goes up */ let totalTick = 0; /** Calculate when the next tick should happen we need this for * tempo and signature change */ function nextTick(tempo: Tempo, sig: Signature) { // BPM is derived from the Microseconds Per Quarter Note (from SET_TEMPO) // and the Time Signature. const secondsPerQuaterNote = (60000 / (tempo.bpm * tempo.ppq)); const millisecondsPerQuarterNote = secondsPerQuaterNote / 1000; // div faster, times slower nextNoteTime += millisecondsPerQuarterNote; } /** * Create a time signature from a "4/4" type string. */ export const toSignature = (sig: string): Signature => { const p = sig.split("/"); return createSignature(parseInt(p[0]), parseInt(p[1])); } /** * Create a time signature that can be used in song * playback. Values here match up with midi's * TimeSignature event */ export const createSignature = (numerator: number, denominator: number, quarter32nd?: number, metroTicks = 24): Signature => { return { noteBeats: numerator, perMeasure: denominator, ticksInMetronome: metroTicks, toQuarter32nd: quarter32nd ?? numerator * 2, } as Signature } export const createTempo = (ppq: number, usPerQuarter: number): Tempo => { const tempo: Tempo = { ppq: ppq, tempo: usPerQuarter, bpm: 60000000 / ((usPerQuarter > 0) ? usPerQuarter : 0), }; return tempo; } /** * This function will be called on every "tick" message from the metronome worker * TODO: should this be a custom event instead of a callback? Then multiple things * could listen to the tick counter which seems pretty useful. */ function scheduler(tempo: Tempo, sig: Signature, perTick: TickCallbackContainer) { // while there are notes that will need to play before the next interval, // schedule them and advance the pointer. while (nextNoteTime < (system.currentTime() + scheduleAheadTime)) { if (totalTick % tempo.ppq === 0) { beatInfo.beat++; beatInfo.tick = 0; } else { beatInfo.tick += 1; } if (beatInfo.beat === (sig.noteBeats + 1)) { beatInfo.measure++; beatInfo.beat = 1; beatInfo.tick = 0; } // Last one wins? for (let l = 0; l < perTick.tickListeners.length; l++) { [tempo, sig] = perTick.tickListeners[l](totalTick, nextNoteTime, beatInfo); } // TODO: Tempo change events, and time signature change are currently // only supported over the whole run. In reality, you should be // able to change each "track" independently :-/ nextTick(tempo, sig); totalTick++; } } export function playHeadToggle() { isPlaying = !isPlaying; if (isPlaying) { nextNoteTime = system.currentTime(); timerWorker.postMessage("start"); } else { timerWorker.postMessage("stop"); } } export function playHeadTo(time: number) { totalTick = time; } export function playHeadRewind() { nextNoteTime = system.currentTime(); beatInfo = { measure: 1, beat: 1, tick: 0, } totalTick = 0; } export function createPlayHeadClock( sys: SoundSystem, tempo: Tempo, sig: Signature, perTick: TickCallbackContainer) { system = sys; timerWorker = new Worker("metronome.worker.js"); playHeadRewind(); timerWorker.onmessage = function (e: MessageEvent<any>) { if (e.data === "tick") { scheduler(tempo, sig, perTick); } else { console.log("message: " + e.data); } }; timerWorker.postMessage({ "interval": lookahead }); } |