UNPKG

pxt-common-packages

Version:
676 lines (589 loc) 23.9 kB
enum MusicOutput { AutoDetect = 0, Buzzer = 1, HeadPhones = 2, } namespace music { //% whenUsed const freqs = hex` 1f00210023002500270029002c002e003100340037003a003e004100450049004e00520057005c00620068006e00 75007b0083008b0093009c00a500af00b900c400d000dc00e900f70006011501260137014a015d01720188019f01 b801d201ee010b022a024b026e029302ba02e40210033f037003a403dc03170455049704dd0427057505c8052006 7d06e0064907b8072d08a9082d09b9094d0aea0a900b400cfa0cc00d910e6f0f5a1053115b1272139a14d4152017 8018f519801b231dde1e` //% shim=music::queuePlayInstructions function queuePlayInstructions(timeDelta: number, buf: Buffer) { } //% shim=music::stopPlaying function stopPlaying() { } //% shim=music::forceOutput export function forceOutput(buf: MusicOutput) { } let globalVolume: number = null const BUFFER_SIZE: number = 12; //% shim=music::enableAmp function enableAmp(en: number) { return // for sim } function initVolume() { if (globalVolume === null) { globalVolume = 0 setVolume(control.getConfigValue(DAL.CFG_SPEAKER_VOLUME, 128)) } } /** * Set the default output volume of the sound synthesizer. * @param volume the volume 0...255 */ //% blockId=synth_set_volume block="set volume %volume" //% parts="speaker" //% volume.min=0 volume.max=255 //% volume.defl=20 //% help=music/set-volume //% weight=70 //% group="Volume" export function setVolume(volume: number): void { globalVolume = Math.clamp(0, 255, volume | 0) enableAmp(globalVolume > 0 ? 1 : 0) } /** * Gets the current volume */ //% parts="speaker" //% weight=70 export function volume(): number { initVolume() return globalVolume; } function playNoteCore(when: number, frequency: number, ms: number) { let buf = control.createBuffer(BUFFER_SIZE) addNote(buf, 0, ms, 255, 255, 3, frequency, volume(), frequency) queuePlayInstructions(when, buf) } /** * Play a tone through the speaker for some amount of time. * @param frequency pitch of the tone to play in Hertz (Hz), eg: Note.C * @param ms tone duration in milliseconds (ms), eg: BeatFraction.Half */ //% help=music/play-tone //% blockId=mixer_play_note block="play tone|at %note=device_note|for %duration=device_beat" //% parts="headphone" async //% blockNamespace=music //% weight=76 blockGap=8 //% group="Tone" //% deprecated=1 export function playTone(frequency: number, ms: number): void { if (ms == 0) ms = 86400000 // 1 day if (ms <= 2000) { playNoteCore(0, frequency, ms) pause(ms) } else { const id = ++playToneID control.runInParallel(() => { let pos = control.millis() while (id == playToneID && ms > 0) { let now = control.millis() let d = pos - now let t = Math.min(ms, 500) ms -= t pos += t playNoteCore(d - 1, frequency, t) if (ms == 0) pause(d + t) else pause(d + t - 100) } }) } } let playToneID = 0 /** * Play a melody from the melody editor. * @param melody - string of up to eight notes [C D E F G A B C5] or rests [-] separated by spaces, * which will be played one at a time, ex: "E D G F B A C5 B " * @param tempo - number in beats per minute (bpm), dictating how long each note will play for */ //% block="play melody $melody at tempo $tempo|(bpm)" blockId=playMelody //% blockNamespace=music //% weight=85 blockGap=8 help=music/play-melody //% group="Melody" //% melody.shadow="melody_editor" //% tempo.min=40 tempo.max=500 //% tempo.defl=120 //% deprecated=1 export function playMelody(melody: string, tempo: number) { let notes: string[] = melody.split(" ").filter(n => !!n); let formattedMelody = ""; let newOctave = false; // build melody string, replace '-' with 'R' and add tempo // creates format like "C5-174 B4 A G F E D C " for (let i = 0; i < notes.length; i++) { if (notes[i] === "-") { notes[i] = "R"; } else if (notes[i] === "C5") { newOctave = true; } else if (newOctave) { // change the octave if necesary notes[i] += "4"; newOctave = false; } // add tempo after first note if (i == 0) { formattedMelody += notes[i] + "-" + tempo + " "; } else { formattedMelody += notes[i] + " "; } } const song = new Melody(formattedMelody); song.playUntilDone(); } /** * Create a melody with the melody editor. * @param melody */ //% block="$melody" blockId=melody_editor //% blockNamespace=music //% blockHidden = true //% weight=85 blockGap=8 //% help=music/melody-editor //% group="Melody" duplicateShadowOnDrag //% melody.fieldEditor="melody" //% melody.fieldOptions.decompileLiterals=true //% melody.fieldOptions.decompileIndirectFixedInstances="true" //% melody.fieldOptions.onParentBlock="true" //% shim=TD_ID export function melodyEditor(melody: string): string { return melody; } /** * Stop all sounds from playing. */ //% help=music/stop-all-sounds //% blockId=music_stop_all_sounds block="stop all sounds" //% weight=45 //% group="Sounds" export function stopAllSounds() { Melody.stopAll(); stopPlaying(); _stopPlayables(); sequencer._stopAllSongs(); } //% fixedInstances export class Melody { _text: string; private _player: MelodyPlayer; private static playingMelodies: Melody[]; static stopAll() { if (Melody.playingMelodies) { const ms = Melody.playingMelodies.slice(0, Melody.playingMelodies.length); ms.forEach(p => p.stop()); } } constructor(text: string) { this._text = text } get text() { return this._text; } /** * Stop playing a sound */ //% blockId=mixer_stop block="stop sound %sound" //% help=music/melody/stop //% parts="headphone" //% weight=92 blockGap=8 //% group="Sounds" //% deprecated=1 stop() { if (this._player) { this._player.stop() this._player = null } this.unregisterMelody(); } private registerMelody() { // keep track of the active players if (!Melody.playingMelodies) Melody.playingMelodies = []; // stop and pop melodies if too many playing if (Melody.playingMelodies.length > 4) { // stop last player (also pops) Melody.playingMelodies[Melody.playingMelodies.length - 1].stop(); } // put back the melody on top of the melody stack Melody.playingMelodies.removeElement(this); Melody.playingMelodies.push(this); } private unregisterMelody() { // remove from list if (Melody.playingMelodies) { Melody.playingMelodies.removeElement(this); // remove self } } private playCore(volume: number, loop: boolean) { this.stop() const p = this._player = new MelodyPlayer(this) this.registerMelody(); control.runInParallel(() => { while (this._player == p) { p.play(volume) if (!loop) { // Unregister the melody when done playing, but // only if it hasn't been restarted. (Looping // melodies never stop on their own, they only // get unregistered via stop().) if (this._player == p) { this.unregisterMelody(); } break } } }) } /** * Start playing a sound in a loop and don't wait for it to finish. * @param sound the melody to play */ //% help=music/melody/loop //% blockId=mixer_loop_sound block="loop sound %sound" //% parts="headphone" //% weight=93 blockGap=8 //% group="Sounds" //% deprecated=1 loop(volume = 255) { this.playCore(volume, true) } /** * Start playing a sound and don't wait for it to finish. * @param sound the melody to play */ //% help=music/melody/play //% blockId=mixer_play_sound block="play sound %sound" //% parts="headphone" //% weight=95 blockGap=8 //% group="Sounds" //% deprecated=1 play(volume = 255) { this.playCore(volume, false) } /** * Play a sound and wait until the sound is done. * @param sound the melody to play */ //% help=music/melody/play-until-done //% blockId=mixer_play_sound_until_done block="play sound %sound|until done" //% parts="headphone" //% weight=94 blockGap=8 //% group="Sounds" //% deprecated=1 playUntilDone(volume = 255) { this.stop() const p = this._player = new MelodyPlayer(this) this._player.onPlayFinished = () => { if (p == this._player) this.unregisterMelody(); } this.registerMelody(); this._player.play(volume) } toString() { return this._text; } } export function addNote(sndInstr: Buffer, sndInstrPtr: number, ms: number, beg: number, end: number, soundWave: number, hz: number, volume: number, endHz: number) { if (ms > 0) { sndInstr.setNumber(NumberFormat.UInt8LE, sndInstrPtr, soundWave) sndInstr.setNumber(NumberFormat.UInt8LE, sndInstrPtr + 1, 0) sndInstr.setNumber(NumberFormat.UInt16LE, sndInstrPtr + 2, hz) sndInstr.setNumber(NumberFormat.UInt16LE, sndInstrPtr + 4, ms) sndInstr.setNumber(NumberFormat.UInt16LE, sndInstrPtr + 6, (beg * volume) >> 6) sndInstr.setNumber(NumberFormat.UInt16LE, sndInstrPtr + 8, (end * volume) >> 6) sndInstr.setNumber(NumberFormat.UInt16LE, sndInstrPtr + 10, endHz); sndInstrPtr += BUFFER_SIZE; } sndInstr.setNumber(NumberFormat.UInt8LE, sndInstrPtr, 0) // terminate return sndInstrPtr } export class MelodyPlayer { melody: Melody; onPlayFinished: () => void; constructor(m: Melody) { this.melody = m } stop() { this.melody = null } protected queuePlayInstructions(timeDelta: number, buf: Buffer) { queuePlayInstructions(timeDelta, buf) } play(volume: number) { if (!this.melody) return volume = Math.clamp(0, 255, (volume * music.volume()) >> 8) let notes = this.melody._text let pos = 0; let duration = 4; //Default duration (Crotchet) let octave = 4; //Middle octave let tempo = 120; // default tempo let hz = 0 let endHz = -1 let ms = 0 let timePos = 0 let startTime = control.millis() let now = 0 let envA = 0 let envD = 0 let envS = 255 let envR = 0 let soundWave = 1 // triangle let sndInstr = control.createBuffer(5 * BUFFER_SIZE) let sndInstrPtr = 0 const addForm = (formDuration: number, beg: number, end: number, msOff: number) => { let freqStart = hz; let freqEnd = endHz; const envelopeWidth = ms > 0 ? ms : duration * Math.idiv(15000, tempo) + envR; if (endHz != hz && envelopeWidth != 0) { const slope = (freqEnd - freqStart) / envelopeWidth; freqStart = hz + slope * msOff; freqEnd = hz + slope * (msOff + formDuration); } sndInstrPtr = addNote(sndInstr, sndInstrPtr, formDuration, beg, end, soundWave, freqStart, volume, freqEnd); } const scanNextWord = () => { if (!this.melody) return "" // eat space while (pos < notes.length) { const c = notes[pos]; if (c != ' ' && c != '\r' && c != '\n' && c != '\t') break; pos++; } // read note let note = ""; while (pos < notes.length) { const c = notes[pos]; if (c == ' ' || c == '\r' || c == '\n' || c == '\t') break; note += c; pos++; } return note; } enum Token { Note, Octave, Beat, Tempo, Hz, EndHz, Ms, WaveForm, EnvelopeA, EnvelopeD, EnvelopeS, EnvelopeR } let token: string = ""; let tokenKind = Token.Note; // [ABCDEFG] (\d+) (:\d+) (-\d+) // note octave length tempo // R (:\d+) - rest // !\d+,\d+ - sound at frequency with given length (Hz,ms); !\d+ and !\d+,:\d+ also possible // @\d+,\d+,\d+,\d+ - ADSR envelope - ms,ms,volume,ms; volume is 0-255 // ~\d+ - wave form: // 1 - triangle // 2 - sawtooth // 3 - sine // 4 - pseudorandom square wave noise (tunable) // 5 - white noise (ignores frequency) // 11 - square 10% // 12 - square 20% // ... // 15 - square 50% // 16 - filtered square wave, cycle length 16 // 17 - filtered square wave, cycle length 32 // 18 - filtered square wave, cycle length 64 const consumeToken = () => { if (token && tokenKind != Token.Note) { const d = parseInt(token); switch (tokenKind) { case Token.Octave: octave = d; break; case Token.Beat: duration = Math.max(1, Math.min(16, d)); ms = -1; break; case Token.Tempo: tempo = Math.max(1, d); break; case Token.Hz: hz = d; tokenKind = Token.Ms; break; case Token.Ms: ms = d; break; case Token.WaveForm: soundWave = Math.clamp(1, 18, d); break; case Token.EnvelopeA: envA = d; tokenKind = Token.EnvelopeD; break; case Token.EnvelopeD: envD = d; tokenKind = Token.EnvelopeS; break; case Token.EnvelopeS: envS = Math.clamp(0, 255, d); tokenKind = Token.EnvelopeR; break; case Token.EnvelopeR: envR = d; break; case Token.EndHz: endHz = d; break; } token = ""; } } while (true) { let currNote = scanNextWord(); let prevNote: boolean = false; if (!currNote) { let timeLeft = timePos - now if (timeLeft > 0) pause(timeLeft) if (this.onPlayFinished) this.onPlayFinished(); return; } hz = -1; let note: number = 0; token = ""; tokenKind = Token.Note; for (let i = 0; i < currNote.length; i++) { let noteChar = currNote.charAt(i); switch (noteChar) { case 'c': case 'C': note = 1; prevNote = true; break; case 'd': case 'D': note = 3; prevNote = true; break; case 'e': case 'E': note = 5; prevNote = true; break; case 'f': case 'F': note = 6; prevNote = true; break; case 'g': case 'G': note = 8; prevNote = true; break; case 'a': case 'A': note = 10; prevNote = true; break; case 'B': note = 12; prevNote = true; break; case 'r': case 'R': hz = 0; prevNote = false; break; case '#': note++; prevNote = false; break; case 'b': if (prevNote) note--; else { note = 12; prevNote = true; } break; case ',': consumeToken(); prevNote = false; break; case '!': tokenKind = Token.Hz; prevNote = false; break; case '@': consumeToken(); tokenKind = Token.EnvelopeA; prevNote = false; break; case '~': consumeToken(); tokenKind = Token.WaveForm; prevNote = false; break; case ':': consumeToken(); tokenKind = Token.Beat; prevNote = false; break; case '-': consumeToken(); tokenKind = Token.Tempo; prevNote = false; break; case '^': consumeToken(); tokenKind = Token.EndHz; break; default: if (tokenKind == Token.Note) tokenKind = Token.Octave; token += noteChar; prevNote = false; break; } } consumeToken(); if (note && hz < 0) { const keyNumber = note + (12 * (octave - 1)); hz = freqs.getNumber(NumberFormat.UInt16LE, keyNumber * 2) || 0; } let currMs = ms if (currMs <= 0) { const beat = Math.idiv(15000, tempo); currMs = duration * beat } if (hz < 0) { // no frequency specified, so no duration } else if (hz == 0) { timePos += currMs } else { if (endHz < 0) { endHz = hz; } sndInstrPtr = 0 addForm(envA, 0, 255, 0) addForm(envD, 255, envS, envA) addForm(currMs - (envA + envD), envS, envS, envD + envA) addForm(envR, envS, 0, currMs) this.queuePlayInstructions(timePos - now, sndInstr.slice(0, sndInstrPtr)) endHz = -1; timePos += currMs // don't add envR - it's supposed overlap next sound } let timeLeft = timePos - now if (timeLeft > 200) { pause(timeLeft - 100) now = control.millis() - startTime } } } } //% blockId=music_song_field_editor //% block="song $song" //% song.fieldEditor=musiceditor //% song.fieldOptions.decompileLiterals=true //% song.fieldOptions.taggedTemplate="hex;assets.song" //% song.fieldOptions.decompileIndirectFixedInstances="true" //% song.fieldOptions.decompileArgumentAsString="true" //% toolboxParent=music_playable_play //% toolboxParentArgument=toPlay //% group="Songs" //% duplicateShadowOnDrag //% help=music/create-song export function createSong(song: Buffer): Playable { return new sequencer.Song(song); } export function playInstructions(when: number, instructions: Buffer) { queuePlayInstructions(when, instructions); } export function lookupFrequency(note: number) { return freqs.getNumber(NumberFormat.UInt16LE, note * 2) || 0 } //% fixedInstance whenUsed block="ba ding" export const baDing = new Melody('b5:1 e6:3') //% fixedInstance whenUsed block="wawawawaa" export const wawawawaa = new Melody('~15 e3:3 r:1 d#:3 r:1 d:4 r:1 c#:8') //% fixedInstance whenUsed block="jump up" export const jumpUp = new Melody('c5:1 d e f g') //% fixedInstance whenUsed block="jump down" export const jumpDown = new Melody('g5:1 f e d c') //% fixedInstance whenUsed block="power up" export const powerUp = new Melody('g4:1 c5 e g:2 e:1 g:3') //% fixedInstance whenUsed block="power down" export const powerDown = new Melody('g5:1 d# c g4:2 b:1 c5:3') //% fixedInstance whenUsed block="magic wand" export const magicWand = new Melody('F#6:1-300 G# A# B C7# D# F F# G# A# B:6') //A#7:1-200 A:1 A#7:1 A:1 A#7:2 //% fixedInstance whenUsed block="siren" export const siren = new Melody('a4 d5 a4 d5 a4 d5') //% fixedInstance whenUsed block="pew pew" export const pewPew = new Melody('!1200,200^50') //% fixedInstance whenUsed block="knock" export const knock = new Melody('~4 @0,0,255,150 !300,1 !211,1') //% fixedInstance whenUsed block="footstep" export const footstep = new Melody('~4 @0,0,60,50 !200,1') //% fixedInstance whenUsed block="thump" export const thump = new Melody('~4 @0,0,255,150 !100,1') //% fixedInstance whenUsed block="small crash" export const smallCrash = new Melody('~4 @10,490,0,1 !800,1') //% fixedInstance whenUsed block="big crash" export const bigCrash = new Melody('~4 @10,990,0,1 !400,1') //% fixedInstance whenUsed block="zapped" export const zapped = new Melody('~16 @10,490,0,0 !1600,500^1') //% fixedInstance whenUsed block="buzzer" export const buzzer = new Melody('~16 @10,0,255,250 !2000,300') //% fixedInstance whenUsed block="sonar" export const sonar = new Melody('~16 @10,1500,0,0 !200,1 !200,1500^190') //% fixedInstance whenUsed block="spooky" export const spooky = new Melody('~16 @700,1300,0,0 !100,1 ~18 !108,2000') //% fixedInstance whenUsed block="beam up" export const beamUp = new Melody('~18 @10,1500,0,0 !200,1500^4000') }