UNPKG

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
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;