1 | var Board = require("./board");
|
2 | var Timer = require("nanotimer");
|
3 |
|
4 | var MICROSECONDS_PER_SECOND = 1000000;
|
5 | var priv = new Map();
|
6 | var defaultOctave = 4;
|
7 |
|
8 | function clearTimer() {
|
9 | if (!this.timer) {
|
10 | return;
|
11 | }
|
12 |
|
13 | this.timer.clearInterval();
|
14 | delete this.timer;
|
15 | }
|
16 |
|
17 | var Controllers = {
|
18 | |
19 |
|
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 |
|
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 |
|
129 | function 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 |
|
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 |
|
178 | Piezo.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 |
|
289 | Piezo.Frequencies = Object.keys(Piezo.Notes).reduce(function(accum, note) {
|
290 | accum[Piezo.Notes[note]] = note;
|
291 | return accum;
|
292 | }, {});
|
293 |
|
294 | Piezo.Parsers = {};
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 | Piezo.Parsers.hzFromInput = function(input) {
|
303 | var output = input;
|
304 |
|
305 | if (Array.isArray(input)) {
|
306 | output = input[0];
|
307 | }
|
308 |
|
309 |
|
310 | if (typeof output === "number" &&
|
311 | Piezo.Frequencies[output]) {
|
312 | return output;
|
313 | }
|
314 |
|
315 |
|
316 | if (typeof output === "string") {
|
317 | output = output.toLowerCase().trim();
|
318 |
|
319 |
|
320 | if (output.endsWith("#") || output.length === 1) {
|
321 | output += defaultOctave;
|
322 | }
|
323 |
|
324 |
|
325 | output = Piezo.Notes[output] || null;
|
326 | }
|
327 |
|
328 |
|
329 | if (isNaN(output)) {
|
330 | output = null;
|
331 | }
|
332 |
|
333 | return output;
|
334 | };
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 | Piezo.Parsers.beatFromNote = function(note) {
|
343 | var beat = 1;
|
344 | if (Array.isArray(note) && note[1] !== undefined) {
|
345 |
|
346 | beat = note[1];
|
347 | }
|
348 | return beat;
|
349 | };
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 | Piezo.isValidOctave = function(octave) {
|
357 | return typeof octave === "number" && (octave >= 0 && octave <= 8);
|
358 | };
|
359 |
|
360 |
|
361 |
|
362 |
|
363 |
|
364 | Piezo.defaultOctave = function(octave) {
|
365 | if (Piezo.isValidOctave(octave)) {
|
366 | defaultOctave = octave;
|
367 | }
|
368 |
|
369 | return defaultOctave;
|
370 | };
|
371 |
|
372 | Piezo.ToFrequency = function(tone) {
|
373 | var toneSeconds = tone / MICROSECONDS_PER_SECOND;
|
374 | var period = toneSeconds * 2;
|
375 | return Math.round(1 / period);
|
376 | };
|
377 |
|
378 | Piezo.ToTone = function(frequency) {
|
379 | var period = 1 / frequency;
|
380 | var duty = period / 2;
|
381 | return Math.round(duty * MICROSECONDS_PER_SECOND);
|
382 | };
|
383 |
|
384 | Piezo.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 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 | Piezo.prototype.note = function(note, duration) {
|
413 | return this.frequency(Piezo.Parsers.hzFromInput(note), duration);
|
414 | };
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 | Piezo.prototype.tone = function(tone, duration) {
|
427 | return this.frequency(Piezo.ToFrequency(tone), duration);
|
428 | };
|
429 |
|
430 |
|
431 |
|
432 |
|
433 |
|
434 |
|
435 | Piezo.prototype.frequency = function(frequency, duration) {
|
436 | return this.tone(Piezo.ToTone(frequency), duration);
|
437 | };
|
438 |
|
439 |
|
440 | Piezo.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 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 | tune.song = [tune.song];
|
459 | |
460 |
|
461 |
|
462 |
|
463 |
|
464 |
|
465 |
|
466 |
|
467 |
|
468 |
|
469 | }
|
470 |
|
471 | var state = priv.get(this);
|
472 | var tempo = tune.tempo || 250;
|
473 |
|
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 |
|
482 |
|
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 |
|
507 | state.isPlaying = true;
|
508 |
|
509 | next();
|
510 |
|
511 | return this;
|
512 | };
|
513 |
|
514 | Piezo.prototype.off = function() {
|
515 | return this.noTone();
|
516 | };
|
517 |
|
518 | Piezo.prototype.stop = function() {
|
519 | var state = priv.get(this);
|
520 |
|
521 |
|
522 | if (state.timeout) {
|
523 | clearTimeout(state.timeout);
|
524 | state.timeout = null;
|
525 | }
|
526 |
|
527 | return this;
|
528 | };
|
529 |
|
530 |
|
531 | module.exports = Piezo;
|