-
Notifications
You must be signed in to change notification settings - Fork 197
/
Copy pathmetronome.js
163 lines (137 loc) · 5.68 KB
/
metronome.js
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
var audioContext = null;
var unlocked = false;
var isPlaying = false; // Are we currently playing?
var startTime; // The start time of the entire sequence.
var current16thNote; // What note is currently last scheduled?
var tempo = 120.0; // tempo (in beats per minute)
var lookahead = 25.0; // How frequently to call scheduling function
//(in milliseconds)
var scheduleAheadTime = 0.1; // How far ahead to schedule audio (sec)
// This is calculated from lookahead, and overlaps
// with next interval (in case the timer is late)
var nextNoteTime = 0.0; // when the next note is due.
var noteResolution = 0; // 0 == 16th, 1 == 8th, 2 == quarter note
var noteLength = 0.05; // length of "beep" (in seconds)
var canvas, // the canvas element
canvasContext; // canvasContext is the canvas' context 2D
var last16thNoteDrawn = -1; // the last "box" we drew on the screen
var notesInQueue = []; // the notes that have been put into the web audio,
// and may or may not have played yet. {note, time}
var timerWorker = null; // The Web Worker used to fire timer messages
// First, let's shim the requestAnimationFrame API, with a setTimeout fallback
window.requestAnimFrame = window.requestAnimationFrame;
function nextNote() {
// Advance current note and time by a 16th note...
var secondsPerBeat = 60.0 / tempo; // Notice this picks up the CURRENT
// tempo value to calculate beat length.
nextNoteTime += 0.25 * secondsPerBeat; // Add beat length to last beat time
current16thNote++; // Advance the beat number, wrap to zero
if (current16thNote == 16) {
current16thNote = 0;
}
}
function scheduleNote( beatNumber, time ) {
// push the note on the queue, even if we're not playing.
notesInQueue.push( { note: beatNumber, time: time } );
if ( (noteResolution==1) && (beatNumber%2))
return; // we're not playing non-8th 16th notes
if ( (noteResolution==2) && (beatNumber%4))
return; // we're not playing non-quarter 8th notes
// create an oscillator
var osc = audioContext.createOscillator();
osc.connect( audioContext.destination );
if (beatNumber % 16 === 0) // beat 0 == high pitch
osc.frequency.value = 880.0;
else if (beatNumber % 4 === 0 ) // quarter notes = medium pitch
osc.frequency.value = 440.0;
else // other 16th notes = low pitch
osc.frequency.value = 220.0;
osc.start( time );
osc.stop( time + noteLength );
}
function scheduler() {
// while there are notes that will need to play before the next interval,
// schedule them and advance the pointer.
while (nextNoteTime < audioContext.currentTime + scheduleAheadTime ) {
scheduleNote( current16thNote, nextNoteTime );
nextNote();
}
}
function play() {
if (!audioContext)
audioContext = new AudioContext();
if (!unlocked) {
// play silent buffer to unlock the audio
var buffer = audioContext.createBuffer(1, 1, 22050);
var node = audioContext.createBufferSource();
node.buffer = buffer;
node.start(0);
unlocked = true;
}
isPlaying = !isPlaying;
if (isPlaying) { // start playing
current16thNote = 0;
nextNoteTime = audioContext.currentTime;
timerWorker.postMessage("start");
return "stop";
} else {
timerWorker.postMessage("stop");
return "play";
}
}
function resetCanvas (e) {
// resize the canvas - but remember - this clears the canvas too.
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
//make sure we scroll to the top left.
window.scrollTo(0,0);
}
function draw() {
var currentNote = last16thNoteDrawn;
if (audioContext) {
var currentTime = audioContext.currentTime;
while (notesInQueue.length && notesInQueue[0].time < currentTime) {
currentNote = notesInQueue[0].note;
notesInQueue.splice(0,1); // remove note from queue
}
// We only need to draw if the note has moved.
if (last16thNoteDrawn != currentNote) {
var x = Math.floor( canvas.width / 18 );
canvasContext.clearRect(0,0,canvas.width, canvas.height);
for (var i=0; i<16; i++) {
canvasContext.fillStyle = ( currentNote == i ) ?
((currentNote%4 === 0)?"red":"blue") : "black";
canvasContext.fillRect( x * (i+1), x, x/2, x/2 );
}
last16thNoteDrawn = currentNote;
}
}
// set up to draw again
requestAnimFrame(draw);
}
function init(){
var container = document.createElement( 'div' );
container.className = "container";
canvas = document.createElement( 'canvas' );
canvasContext = canvas.getContext( '2d' );
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild( container );
container.appendChild(canvas);
canvasContext.strokeStyle = "#ffffff";
canvasContext.lineWidth = 2;
window.onorientationchange = resetCanvas;
window.onresize = resetCanvas;
requestAnimFrame(draw); // start the drawing loop.
timerWorker = new Worker("js/metronomeworker.js");
timerWorker.onmessage = function(e) {
if (e.data == "tick") {
// console.log("tick!");
scheduler();
}
else
console.log("message: " + e.data);
};
timerWorker.postMessage({"interval":lookahead});
}
window.addEventListener("load", init );