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!
512 lines (447 loc) • 10.4 kB
JavaScript
const Board = require("./board");
const Timer = require("nanotimer");
const MICROSECONDS_PER_SECOND = 1000000;
const priv = new Map();
let defaultOctave = 4;
function clearTimer(target) {
if (!target.timer) {
return target;
}
target.timer.clearInterval();
delete target.timer;
return target;
}
const Controllers = {
/**
* Timer-based tone generator using digital high/low piezo.
*/
DEFAULT: {
initialize: {
writable: true,
value() {
this.io.pinMode(this.pin, this.io.MODES.OUTPUT);
},
},
tone: {
writable: true,
value(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(this);
this.timer = new Timer();
let value = 1;
this.timer.setInterval(() => {
value = value === 1 ? 0 : 1;
this.io.digitalWrite(this.pin, value);
if ((this.timer.difTime / 1000000) > duration) {
clearTimer(this);
}
}, null, `${tone}u`, () => {});
return this;
},
},
noTone: {
writable: true,
value() {
this.io.digitalWrite(this.pin, 0);
return clearTimer(this);
},
},
},
I2C_BACKPACK: {
ADDRESSES: {
value: [0x0A]
},
REGISTER: {
value: {
NO_TONE: 0x00,
TONE: 0x01,
},
},
initialize: {
writable: true,
value(options) {
const { Drivers } = require("./sip");
const address = Drivers.addressResolver(this, options);
const state = priv.get(this);
this.io.i2cConfig(options);
state.address = address;
}
},
tone: {
writable: true,
value(tone, duration) {
const state = priv.get(this);
if (isNaN(tone) || isNaN(duration)) {
throw new Error(
"Piezo.tone: invalid tone or duration"
);
}
this.io.i2cWrite(state.address, [
this.REGISTER.TONE,
this.pin,
(tone >> 8) & 0xff,
tone & 0xff,
(duration >> 24) & 0xff,
(duration >> 16) & 0xff,
(duration >> 8) & 0xff,
duration & 0xff,
]);
return this;
},
},
noTone: {
writable: true,
value() {
const state = priv.get(this);
this.io.i2cWrite(state.address, [
this.REGISTER.NO_TONE,
this.pin,
]);
return this;
},
},
},
};
class Piezo {
constructor(options) {
Board.Component.call(
this, options = Board.Options(options)
);
Board.Controller.call(this, Controllers, options);
// Piezo instance properties
const state = {
isPlaying: false,
timeout: null,
address: null,
};
priv.set(this, state);
Object.defineProperties(this, {
isPlaying: {
get() {
return state.isPlaying;
}
}
});
if (typeof this.initialize === "function") {
this.initialize(options);
}
}
/**
* Play a note for a duration.
* @param {string} note - see Piezo.Notes. Case-insensitive.
* If a note name without an octave number is given (e.g. "C#" instead of
* "C#4") then the configured default octave will be used.
* @see Piezo.prototype.defaultOctave
* @param {number} duration - in milliseconds.
*/
note(note, duration) {
return this.frequency(Piezo.Parsers.hzFromInput(note), duration);
}
/**
* Play a tone for a duration.
* This is a lower-level method than frequency (which does
* the translation from frequency to tone for you). Most of
* the time you likely want to use frequency.
* @param {number} tone - Given as a computed duty-cycle,
* in microseconds. Larger values produce lower tones.
* See https://en.wikipedia.org/wiki/Duty_cycle
* @param {number} duration - in milliseconds.
*/
tone(tone, duration) {
return this.frequency(Piezo.ToFrequency(tone), duration);
}
/**
* Play a frequency for a duration.
* @param {number} frequency - in Hz
* @param {number} duration - in milliseconds
*/
frequency(frequency, duration) {
return this.tone(Piezo.ToTone(frequency), duration);
}
play(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
*/
}
const state = priv.get(this);
const tempo = tune.tempo || 250;
// Length for a single beat in ms
const beatDuration = Math.round(60000 / tempo);
const song = tune.song || [];
let duration;
let nextNoteIndex = 0;
const next = () => {
if (nextNoteIndex === song.length) {
// No more notes in song:
// Song is over
state.isPlaying = false;
if (typeof callback === "function") {
callback(tune);
}
return;
}
const note = song[nextNoteIndex];
const hz = Piezo.Parsers.hzFromInput(note);
const beat = Piezo.Parsers.beatFromNote(note);
duration = beat * beatDuration;
nextNoteIndex++;
if (hz === null) {
this.noTone();
} else {
this.frequency(hz, duration);
}
state.timeout = setTimeout(next, duration);
};
// We are playing a song
state.isPlaying = true;
next();
return this;
}
off() {
return this.noTone();
}
stop() {
const state = priv.get(this);
/* istanbul ignore else */
if (state.timeout) {
clearTimeout(state.timeout);
state.timeout = null;
}
return this;
}
}
// 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((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 = input => {
let 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 = note => {
let 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 = octave => typeof octave === "number" && (octave >= 0 && octave <= 8);
/**
* Set or get a default octave for all notes
* @return number
*/
Piezo.defaultOctave = octave => {
if (Piezo.isValidOctave(octave)) {
defaultOctave = octave;
}
return defaultOctave;
};
Piezo.ToFrequency = tone => {
const toneSeconds = tone / MICROSECONDS_PER_SECOND;
const period = toneSeconds * 2;
return Math.round(1 / period);
};
Piezo.ToTone = frequency => {
const period = 1 / frequency;
const duty = period / 2;
return Math.round(duty * MICROSECONDS_PER_SECOND);
};
Piezo.ToSong = (stringSong, beats = 1) => {
const notes = stringSong.split(" ");
const song = [];
let note;
let 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;
};
module.exports = Piezo;