UNPKG

10.8 kBJavaScriptView Raw
1var Board = require("./board");
2var Timer = require("nanotimer");
3
4var MICROSECONDS_PER_SECOND = 1000000;
5var priv = new Map();
6var defaultOctave = 4;
7
8function clearTimer() {
9 if (!this.timer) {
10 return;
11 }
12
13 this.timer.clearInterval();
14 delete this.timer;
15}
16
17var Controllers = {
18 /**
19 * Timer-based tone generator using digital high/low piezo.
20 */
21 DEFAULT: {
22 initialize: {
23 writable: true,
24 value: function() {
25 this.io.pinMode(this.pin, this.io.MODES.OUTPUT);
26 },
27 },
28 tone: {
29 writable: true,
30 value: function(tone, duration) {
31 if (isNaN(tone) || isNaN(duration)) {
32 // Very Bad Things happen if one tries to play a NaN tone
33 throw new Error(
34 "Piezo.tone: invalid tone or duration"
35 );
36 }
37
38 clearTimer.call(this);
39
40 var timer = this.timer = new Timer();
41 var value = 1;
42
43 timer.setInterval(function() {
44 value = value === 1 ? 0 : 1;
45 this.io.digitalWrite(this.pin, value);
46
47 if ((timer.difTime / 1000000) > duration) {
48 clearTimer.call(this);
49 }
50 }.bind(this), null, tone + "u", function() {});
51
52 return this;
53 },
54 },
55 noTone: {
56 writable: true,
57 value: function() {
58 this.io.digitalWrite(this.pin, 0);
59 clearTimer.call(this);
60
61 return this;
62 },
63 },
64 },
65
66 I2C_BACKPACK: {
67 ADDRESSES: {
68 value: [0x0A]
69 },
70 REGISTER: {
71 value: {
72 NO_TONE: 0x00,
73 TONE: 0x01,
74 },
75 },
76 initialize: {
77 value: function(opts) {
78 var state = priv.get(this);
79 var address = opts.address || this.ADDRESSES[0];
80
81 state.address = opts.address = address;
82
83 this.io.i2cConfig(opts);
84 }
85 },
86 tone: {
87 value: function(tone, duration) {
88 var state = priv.get(this);
89
90 if (isNaN(tone) || isNaN(duration)) {
91 throw new Error(
92 "Piezo.tone: invalid tone or duration"
93 );
94 }
95
96 var data = [
97 this.REGISTER.TONE,
98 this.pin,
99 (tone >> 8) & 0xff,
100 tone & 0xff,
101 (duration >> 24) & 0xff,
102 (duration >> 16) & 0xff,
103 (duration >> 8) & 0xff,
104 duration & 0xff,
105 ];
106
107 this.io.i2cWrite(state.address, data);
108
109 return this;
110 },
111 },
112 noTone: {
113 value: function() {
114 var state = priv.get(this);
115
116 var data = [
117 this.REGISTER.NO_TONE,
118 this.pin,
119 ];
120
121 this.io.i2cWrite(state.address, data);
122
123 return this;
124 },
125 },
126 },
127};
128
129function Piezo(opts) {
130
131 if (!(this instanceof Piezo)) {
132 return new Piezo(opts);
133 }
134
135 Board.Component.call(
136 this, opts = Board.Options(opts)
137 );
138
139 var controller = null;
140
141 if (opts.controller && typeof opts.controller === "string") {
142 controller = Controllers[opts.controller.toUpperCase()];
143 } else {
144 controller = opts.controller;
145 }
146
147 if (controller == null) {
148 controller = Controllers.DEFAULT;
149 }
150
151 Object.defineProperties(this, controller);
152
153 Board.Controller.call(this, controller, opts);
154
155 // Piezo instance properties
156 var state = {
157 isPlaying: false,
158 timeout: null,
159 address: null,
160 };
161
162 priv.set(this, state);
163
164 Object.defineProperties(this, {
165 isPlaying: {
166 get: function() {
167 return state.isPlaying;
168 }
169 }
170 });
171
172 if (typeof this.initialize === "function") {
173 this.initialize(opts);
174 }
175}
176
177// These notes are rounded up at .5 otherwise down.
178Piezo.Notes = {
179 "c0": 16,
180 "c#0": 17,
181 "d0": 18,
182 "d#0": 19,
183 "e0": 21,
184 "f0": 22,
185 "f#0": 23,
186 "g0": 25,
187 "g#0": 26,
188 "a0": 28,
189 "a#0": 29,
190 "b0": 31,
191 "c1": 33,
192 "c#1": 35,
193 "d1": 37,
194 "d#1": 39,
195 "e1": 41,
196 "f1": 44,
197 "f#1": 47,
198 "g1": 49,
199 "g#1": 52,
200 "a1": 55,
201 "a#1": 58,
202 "b1": 62,
203 "c2": 65,
204 "c#2": 69,
205 "d2": 73,
206 "d#2": 78,
207 "e2": 82,
208 "f2": 87,
209 "f#2": 93,
210 "g2": 98,
211 "g#2": 104,
212 "a2": 110,
213 "a#2": 117,
214 "b2": 124,
215 "c3": 131,
216 "c#3": 139,
217 "d3": 147,
218 "d#3": 156,
219 "e3": 165,
220 "f3": 175,
221 "f#3": 185,
222 "g3": 196,
223 "g#3": 208,
224 "a3": 220,
225 "a#3": 233,
226 "b3": 247,
227 "c4": 262,
228 "c#4": 277,
229 "d4": 294,
230 "d#4": 311,
231 "e4": 330,
232 "f4": 349,
233 "f#4": 370,
234 "g4": 392,
235 "g#4": 415,
236 "a4": 440,
237 "a#4": 466,
238 "b4": 494,
239 "c5": 523,
240 "c#5": 554,
241 "d5": 587,
242 "d#5": 622,
243 "e5": 659,
244 "f5": 698,
245 "f#5": 740,
246 "g5": 784,
247 "g#5": 831,
248 "a5": 880,
249 "a#5": 932,
250 "b5": 988,
251 "c6": 1047,
252 "c#6": 1109,
253 "d6": 1175,
254 "d#6": 1245,
255 "e6": 1319,
256 "f6": 1397,
257 "f#6": 1480,
258 "g6": 1568,
259 "g#6": 1661,
260 "a6": 1760,
261 "a#6": 1865,
262 "b6": 1976,
263 "c7": 2093,
264 "c#7": 2217,
265 "d7": 2349,
266 "d#7": 2489,
267 "e7": 2637,
268 "f7": 2794,
269 "f#7": 2960,
270 "g7": 3136,
271 "g#7": 3322,
272 "a7": 3520,
273 "a#7": 3729,
274 "b7": 3951,
275 "c8": 4186,
276 "c#8": 4435,
277 "d8": 4699,
278 "d#8": 4978,
279 "e8": 5274,
280 "f8": 5588,
281 "f#8": 5920,
282 "g8": 6272,
283 "g#8": 6645,
284 "a8": 7040,
285 "a#8": 7459,
286 "b8": 7902,
287};
288
289Piezo.Frequencies = Object.keys(Piezo.Notes).reduce(function(accum, note) {
290 accum[Piezo.Notes[note]] = note;
291 return accum;
292}, {});
293
294Piezo.Parsers = {};
295/**
296 * Get the tone from the current note. note
297 * could be an int, string, array or null.
298 * If int or null, leave alone. Otherwise,
299 * derive what the tone should be.
300 * @return int | null
301 */
302Piezo.Parsers.hzFromInput = function(input) {
303 var output = input;
304
305 if (Array.isArray(input)) {
306 output = input[0];
307 }
308
309 // Is it a valid frequency?
310 if (typeof output === "number" &&
311 Piezo.Frequencies[output]) {
312 return output;
313 }
314
315 // See above: Piezo.Notes { ... }
316 if (typeof output === "string") {
317 output = output.toLowerCase().trim();
318
319 // Example: c#, c
320 if (output.endsWith("#") || output.length === 1) {
321 output += defaultOctave;
322 }
323
324 // There will never be a 0 tone
325 output = Piezo.Notes[output] || null;
326 }
327
328 // Normalize NaN, null & undefined to null
329 if (isNaN(output)) {
330 output = null;
331 }
332
333 return output;
334};
335
336/**
337 * Obtain the beat/duration count from the current
338 * note. This is either an int or undefined. Default
339 * to 1.
340 * @return int (default 1)
341 */
342Piezo.Parsers.beatFromNote = function(note) {
343 var beat = 1;
344 if (Array.isArray(note) && note[1] !== undefined) {
345 // If extant, beat will be second element of note
346 beat = note[1];
347 }
348 return beat;
349};
350
351/**
352 * Validate the octave provided to ensure the value is
353 * supported and won't crash the board.
354 * @return bool
355 */
356Piezo.isValidOctave = function(octave) {
357 return typeof octave === "number" && (octave >= 0 && octave <= 8);
358};
359
360/**
361 * Set or get a default octave for all notes
362 * @return number
363 */
364Piezo.defaultOctave = function(octave) {
365 if (Piezo.isValidOctave(octave)) {
366 defaultOctave = octave;
367 }
368
369 return defaultOctave;
370};
371
372Piezo.ToFrequency = function(tone) {
373 var toneSeconds = tone / MICROSECONDS_PER_SECOND;
374 var period = toneSeconds * 2;
375 return Math.round(1 / period);
376};
377
378Piezo.ToTone = function(frequency) {
379 var period = 1 / frequency;
380 var duty = period / 2;
381 return Math.round(duty * MICROSECONDS_PER_SECOND);
382};
383
384Piezo.ToSong = function(stringSong, beats) {
385 beats = beats || 1;
386 var notes = stringSong.split(" ");
387 var song = [];
388 var note, lastNote;
389 while (notes.length) {
390 note = notes.shift();
391 if (/^[0-9]+$/.test(note)) {
392 note = parseInt(note, 10);
393 }
394 lastNote = song[song.length - 1];
395 if (lastNote && lastNote[0] === note) {
396 lastNote[1] += beats;
397 } else {
398 song.push([note, beats]);
399 }
400 }
401 return song;
402};
403
404/**
405 * Play a note for a duration.
406 * @param {string} note - see Piezo.Notes. Case-insensitive.
407 * If a note name without an octave number is given (e.g. "C#" instead of
408 * "C#4") then the configured default octave will be used.
409 * @see Piezo.prototype.defaultOctave
410 * @param {number} duration - in milliseconds.
411 */
412Piezo.prototype.note = function(note, duration) {
413 return this.frequency(Piezo.Parsers.hzFromInput(note), duration);
414};
415
416/**
417 * Play a tone for a duration.
418 * This is a lower-level method than frequency (which does
419 * the translation from frequency to tone for you). Most of
420 * the time you likely want to use frequency.
421 * @param {number} tone - Given as a computed duty-cycle,
422 * in microseconds. Larger values produce lower tones.
423 * See https://en.wikipedia.org/wiki/Duty_cycle
424 * @param {number} duration - in milliseconds.
425 */
426Piezo.prototype.tone = function(tone, duration) {
427 return this.frequency(Piezo.ToFrequency(tone), duration);
428};
429
430/**
431 * Play a frequency for a duration.
432 * @param {number} frequency - in Hz
433 * @param {number} duration - in milliseconds
434 */
435Piezo.prototype.frequency = function(frequency, duration) {
436 return this.tone(Piezo.ToTone(frequency), duration);
437};
438
439
440Piezo.prototype.play = function(tune, callback) {
441 if (typeof tune !== "object") {
442 tune = {
443 song: tune
444 };
445 }
446
447 if (typeof tune.song === "string") {
448 tune.song = Piezo.ToSong(tune.song, tune.beats);
449 }
450
451 if (tune.song && !Array.isArray(tune.song)) {
452 /*
453 If `tune.song` was present and not falsy,
454 but also is not a string (above), or an array
455 (presently), then it is likely a Hz value, so
456 normalize song to the appropriate array format:
457 */
458 tune.song = [tune.song];
459 /*
460 Note: This path is taken for calls that look
461 like this:
462
463 piezo.play({
464 song: 262,
465 }, ...)
466
467 Where 262 is a frequency in Hz
468 */
469 }
470
471 var state = priv.get(this);
472 var tempo = tune.tempo || 250;
473 // Length for a single beat in ms
474 var beatDuration = Math.round(60000 / tempo);
475 var song = tune.song || [];
476 var duration;
477 var nextNoteIndex = 0;
478
479 var next = function() {
480 if (nextNoteIndex === song.length) {
481 // No more notes in song:
482 // Song is over
483 state.isPlaying = false;
484 if (typeof callback === "function") {
485 callback(tune);
486 }
487 return;
488 }
489
490 var note = song[nextNoteIndex];
491 var hz = Piezo.Parsers.hzFromInput(note);
492 var beat = Piezo.Parsers.beatFromNote(note);
493
494 duration = beat * beatDuration;
495 nextNoteIndex++;
496
497 if (hz === null) {
498 this.noTone();
499 } else {
500 this.frequency(hz, duration);
501 }
502
503 state.timeout = setTimeout(next, duration);
504 }.bind(this);
505
506 // We are playing a song
507 state.isPlaying = true;
508
509 next();
510
511 return this;
512};
513
514Piezo.prototype.off = function() {
515 return this.noTone();
516};
517
518Piezo.prototype.stop = function() {
519 var state = priv.get(this);
520
521 /* istanbul ignore else */
522 if (state.timeout) {
523 clearTimeout(state.timeout);
524 state.timeout = null;
525 }
526
527 return this;
528};
529
530
531module.exports = Piezo;