johnny-five
Version:
The JavaScript Robotics and Hardware Programming Framework. Use with: Arduino (all models), Electric Imp, Beagle Bone, Intel Galileo & Edison, Linino One, Pinoccio, pcDuino3, Raspberry Pi, Particle/Spark Core & Photon, Tessel 2, TI Launchpad and more!
511 lines (436 loc) • 9.9 kB
JavaScript
var Board = require("./board");
var Timer = require("nanotimer");
var MICROSECONDS_PER_SECOND = 1000000;
var priv = new Map();
var defaultOctave = 4;
function clearTimer() {
if (!this.timer) {
return;
}
this.timer.clearInterval();
delete this.timer;
}
var Controllers = {
/**
* Timer-based tone generator using digital high/low piezo.
*/
DEFAULT: {
initialize: {
writable: true,
value: function() {
this.io.pinMode(this.pin, this.io.MODES.OUTPUT);
},
},
tone: {
writable: true,
value: function(tone, duration) {
if (isNaN(tone) || isNaN(duration)) {
// Very Bad Things happen if one tries to play a NaN tone
throw new Error(
"Piezo.tone: invalid tone or duration"
);
}
clearTimer.call(this);
var timer = this.timer = new Timer();
var value = 1;
timer.setInterval(function() {
value = value === 1 ? 0 : 1;
this.io.digitalWrite(this.pin, value);
if ((timer.difTime / 1000000) > duration) {
clearTimer.call(this);
}
}.bind(this), null, tone + "u", function() {});
return this;
},
},
noTone: {
writable: true,
value: function() {
this.io.digitalWrite(this.pin, 0);
clearTimer.call(this);
return this;
},
},
},
I2C_BACKPACK: {
ADDRESSES: {
value: [0x0A]
},
REGISTER: {
value: {
NO_TONE: 0x00,
TONE: 0x01,
},
},
initialize: {
value: function(opts) {
var state = priv.get(this);
var address = opts.address || this.ADDRESSES[0];
state.address = opts.address = address;
this.io.i2cConfig(opts);
}
},
tone: {
value: function(tone, duration) {
var state = priv.get(this);
if (isNaN(tone) || isNaN(duration)) {
throw new Error(
"Piezo.tone: invalid tone or duration"
);
}
var data = [
this.REGISTER.TONE,
this.pin,
(tone >> 8) & 0xff,
tone & 0xff,
(duration >> 24) & 0xff,
(duration >> 16) & 0xff,
(duration >> 8) & 0xff,
duration & 0xff,
];
this.io.i2cWrite(state.address, data);
return this;
},
},
noTone: {
value: function() {
var state = priv.get(this);
var data = [
this.REGISTER.NO_TONE,
this.pin,
];
this.io.i2cWrite(state.address, data);
return this;
},
},
},
};
function Piezo(opts) {
if (!(this instanceof Piezo)) {
return new Piezo(opts);
}
Board.Component.call(
this, opts = Board.Options(opts)
);
var controller = null;
if (opts.controller && typeof opts.controller === "string") {
controller = Controllers[opts.controller.toUpperCase()];
} else {
controller = opts.controller;
}
if (controller == null) {
controller = Controllers.DEFAULT;
}
Object.defineProperties(this, controller);
Board.Controller.call(this, controller, opts);
// Piezo instance properties
var state = {
isPlaying: false,
timeout: null,
address: null,
};
priv.set(this, state);
Object.defineProperties(this, {
isPlaying: {
get: function() {
return state.isPlaying;
}
}
});
if (typeof this.initialize === "function") {
this.initialize(opts);
}
}
// These notes are rounded up at .5 otherwise down.
Piezo.Notes = {
"c0": 16,
"c#0": 17,
"d0": 18,
"d#0": 19,
"e0": 21,
"f0": 22,
"f#0": 23,
"g0": 25,
"g#0": 26,
"a0": 28,
"a#0": 29,
"b0": 31,
"c1": 33,
"c#1": 35,
"d1": 37,
"d#1": 39,
"e1": 41,
"f1": 44,
"f#1": 47,
"g1": 49,
"g#1": 52,
"a1": 55,
"a#1": 58,
"b1": 62,
"c2": 65,
"c#2": 69,
"d2": 73,
"d#2": 78,
"e2": 82,
"f2": 87,
"f#2": 93,
"g2": 98,
"g#2": 104,
"a2": 110,
"a#2": 117,
"b2": 124,
"c3": 131,
"c#3": 139,
"d3": 147,
"d#3": 156,
"e3": 165,
"f3": 175,
"f#3": 185,
"g3": 196,
"g#3": 208,
"a3": 220,
"a#3": 233,
"b3": 247,
"c4": 262,
"c#4": 277,
"d4": 294,
"d#4": 311,
"e4": 330,
"f4": 349,
"f#4": 370,
"g4": 392,
"g#4": 415,
"a4": 440,
"a#4": 466,
"b4": 494,
"c5": 523,
"c#5": 554,
"d5": 587,
"d#5": 622,
"e5": 659,
"f5": 698,
"f#5": 740,
"g5": 784,
"g#5": 831,
"a5": 880,
"a#5": 932,
"b5": 988,
"c6": 1047,
"c#6": 1109,
"d6": 1175,
"d#6": 1245,
"e6": 1319,
"f6": 1397,
"f#6": 1480,
"g6": 1568,
"g#6": 1661,
"a6": 1760,
"a#6": 1865,
"b6": 1976,
"c7": 2093,
"c#7": 2217,
"d7": 2349,
"d#7": 2489,
"e7": 2637,
"f7": 2794,
"f#7": 2960,
"g7": 3136,
"g#7": 3322,
"a7": 3520,
"a#7": 3729,
"b7": 3951,
"c8": 4186,
"c#8": 4435,
"d8": 4699,
"d#8": 4978,
"e8": 5274,
"f8": 5588,
"f#8": 5920,
"g8": 6272,
"g#8": 6645,
"a8": 7040,
"a#8": 7459,
"b8": 7902,
};
Piezo.Frequencies = Object.keys(Piezo.Notes).reduce(function(accum, note) {
accum[Piezo.Notes[note]] = note;
return accum;
}, {});
Piezo.Parsers = {};
/**
* Get the tone from the current note. note
* could be an int, string, array or null.
* If int or null, leave alone. Otherwise,
* derive what the tone should be.
* @return int | null
*/
Piezo.Parsers.hzFromInput = function(input) {
var output = input;
if (Array.isArray(input)) {
output = input[0];
}
// Is it a valid frequency?
if (typeof output === "number" &&
Piezo.Frequencies[output]) {
return output;
}
// See above: Piezo.Notes { ... }
if (typeof output === "string") {
output = output.toLowerCase().trim();
// Example: c#, c
if (output.endsWith("#") || output.length === 1) {
output += defaultOctave;
}
// There will never be a 0 tone
output = Piezo.Notes[output] || null;
}
// Normalize NaN, null & undefined to null
if (isNaN(output)) {
output = null;
}
return output;
};
/**
* Obtain the beat/duration count from the current
* note. This is either an int or undefined. Default
* to 1.
* @return int (default 1)
*/
Piezo.Parsers.beatFromNote = function(note) {
var beat = 1;
if (Array.isArray(note) && note[1] !== undefined) {
// If extant, beat will be second element of note
beat = note[1];
}
return beat;
};
/**
* Validate the octave provided to ensure the value is
* supported and won't crash the board.
* @return bool
*/
Piezo.isValidOctave = function(octave) {
return typeof octave === "number" && (octave >= 0 && octave <= 8);
};
/**
* Set or get a default octave for all notes
* @return number
*/
Piezo.defaultOctave = function(octave) {
if (Piezo.isValidOctave(octave)) {
defaultOctave = octave;
}
return defaultOctave;
};
Piezo.ToFrequency = function(tone) {
var toneSeconds = tone / MICROSECONDS_PER_SECOND;
var period = toneSeconds * 2;
return Math.round(1 / period);
};
Piezo.ToTone = function(frequency) {
var period = 1 / frequency;
var duty = period / 2;
return Math.round(duty * MICROSECONDS_PER_SECOND);
};
Piezo.ToSong = function(stringSong, beats) {
beats = beats || 1;
var notes = stringSong.split(" ");
var song = [];
var note, lastNote;
while (notes.length) {
note = notes.shift();
if (/^[0-9]+$/.test(note)) {
note = parseInt(note, 10);
}
lastNote = song[song.length - 1];
if (lastNote && lastNote[0] === note) {
lastNote[1] += beats;
} else {
song.push([note, beats]);
}
}
return song;
};
Piezo.prototype.note = function(note, duration) {
var tone = Piezo.Parsers.hzFromInput(note);
return this.tone(tone, duration);
};
Piezo.prototype.tone = function(tone, duration) {
return this.frequency(Piezo.ToFrequency(tone), duration);
};
Piezo.prototype.frequency = function(frequency, duration) {
return this.tone(Piezo.ToTone(frequency), duration);
};
Piezo.prototype.play = function(tune, callback) {
if (typeof tune !== "object") {
tune = {
song: tune
};
}
if (typeof tune.song === "string") {
tune.song = Piezo.ToSong(tune.song, tune.beats);
}
if (tune.song && !Array.isArray(tune.song)) {
/*
If `tune.song` was present and not falsy,
but also is not a string (above), or an array
(presently), then it is likely a Hz value, so
normalize song to the appropriate array format:
*/
tune.song = [tune.song];
/*
Note: This path is taken for calls that look
like this:
piezo.play({
song: 262,
}, ...)
Where 262 is a frequency in Hz
*/
}
var state = priv.get(this);
var tempo = tune.tempo || 250;
// Length for a single beat in ms
var beatDuration = Math.round(60000 / tempo);
var song = tune.song || [];
var duration;
var nextNoteIndex = 0;
var next = function() {
if (nextNoteIndex === song.length) {
// No more notes in song:
// Song is over
state.isPlaying = false;
if (typeof callback === "function") {
callback(tune);
}
return;
}
var note = song[nextNoteIndex];
var hz = Piezo.Parsers.hzFromInput(note);
var beat = Piezo.Parsers.beatFromNote(note);
duration = beat * beatDuration;
nextNoteIndex++;
if (hz === null) {
this.noTone();
} else {
this.frequency(hz, duration);
}
state.timeout = setTimeout(next, duration);
}.bind(this);
// We are playing a song
state.isPlaying = true;
next();
return this;
};
Piezo.prototype.off = function() {
return this.noTone();
};
Piezo.prototype.stop = function() {
var state = priv.get(this);
/* istanbul ignore else */
if (state.timeout) {
clearTimeout(state.timeout);
state.timeout = null;
}
return this;
};
module.exports = Piezo;