From 5a6cbf26394e153d367de021f7a8aa76ebb7a115 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Thu, 14 Dec 2017 10:34:04 -0800 Subject: [PATCH 1/2] upgrading music APIs (#12) --- libs/core/_locales/core-jsdoc-strings.json | 17 +- libs/core/_locales/core-strings.json | 39 +++ libs/core/melodies.ts | 119 ++++++++++ libs/core/music.cpp | 5 +- libs/core/music.ts | 263 +++++++++++++++++++-- libs/core/pxt.json | 1 + libs/core/shims.d.ts | 7 +- pxtarget.json | 3 +- sim/state/music.ts | 2 +- 9 files changed, 420 insertions(+), 36 deletions(-) create mode 100644 libs/core/melodies.ts diff --git a/libs/core/_locales/core-jsdoc-strings.json b/libs/core/_locales/core-jsdoc-strings.json index bc0c14f7..ef827718 100644 --- a/libs/core/_locales/core-jsdoc-strings.json +++ b/libs/core/_locales/core-jsdoc-strings.json @@ -321,21 +321,30 @@ "motors.motorCommand": "Send break, coast or sleep commands to the motor. Has no effect in dual-motor mode.", "motors.motorPower": "Turns on the motor at a certain percent of power. Switches to single motor mode!", "motors.motorPower|param|power": "%percent of power sent to the motor. Negative power goes backward. eg: 50", - "music": "Generation of music tones through pin ``P0``.", + "music": "Generation of music tones.", "music.beat": "Returns the duration of a beat in milli-seconds", + "music.beginMelody": "Starts playing a melody.\nNotes are expressed as a string of characters with this format: NOTE[octave][:duration]", + "music.beginMelody|param|melodyArray": "the melody array to play, eg: ['g5:1']", + "music.beginMelody|param|options": "melody options, once / forever, in the foreground / background", + "music.builtInMelody": "Gets the melody array of a built-in melody.", "music.changeTempoBy": "Change the tempo by the specified amount", "music.changeTempoBy|param|bpm": "The change in beats per minute to the tempo, eg: 20", "music.noteFrequency": "Gets the frequency of a note.", - "music.noteFrequency|param|name": "the note name", - "music.playTone": "Plays a tone through ``speaker`` for the given duration.", + "music.noteFrequency|param|name": "the note name, eg: Note.C", + "music.onEvent": "Registers code to run on various melody events", + "music.playTone": "Plays a tone through pin ``P0`` for the given duration.", "music.playTone|param|frequency": "pitch of the tone to play in Hertz (Hz)", "music.playTone|param|ms": "tone duration in milliseconds (ms)", "music.rest": "Rests (plays nothing) for a specified time through pin ``P0``.", "music.rest|param|ms": "rest duration in milliseconds (ms)", - "music.ringTone": "Plays a tone through ``speaker``.", + "music.ringTone": "Plays a tone through pin ``P0``.", "music.ringTone|param|frequency": "pitch of the tone to play in Hertz (Hz)", + "music.setPlayTone": "Sets a custom playTone function for playing melodies", "music.setTempo": "Sets the tempo to the specified amount", "music.setTempo|param|bpm": "The new tempo in beats per minute, eg: 120", + "music.speakerPlayTone": "Plays a tone through ``speaker`` for the given duration.", + "music.speakerPlayTone|param|frequency": "pitch of the tone to play in Hertz (Hz)", + "music.speakerPlayTone|param|ms": "tone duration in milliseconds (ms)", "music.tempo": "Returns the tempo in beats per minute. Tempo is the speed (bpm = beats per minute) at which notes play. The larger the tempo value, the faster the notes will play.", "parseInt": "Convert A string to an integer.", "pins": "Control currents in Pins for analog/digital signals, servos, i2c, ...", diff --git a/libs/core/_locales/core-strings.json b/libs/core/_locales/core-strings.json index a56084e3..809e95ac 100644 --- a/libs/core/_locales/core-strings.json +++ b/libs/core/_locales/core-strings.json @@ -20,6 +20,8 @@ "BaudRate.BaudRate115200|block": "115200", "BaudRate.BaudRate56700|block": "57600", "BaudRate.BaudRate9600|block": "9600", + "BeatFraction.Breve|block": "4", + "BeatFraction.Double|block": "2", "BeatFraction.Eighth|block": "1/8", "BeatFraction.Half|block": "1/2", "BeatFraction.Quarter|block": "1/4", @@ -121,10 +123,44 @@ "Math.randomBoolean|block": "pick random true or false", "Math.random|block": "pick random 0 to %limit", "Math|block": "Math", + "Melodies.BaDing|block": "ba ding", + "Melodies.Baddy|block": "baddy", + "Melodies.Birthday|block": "birthday", + "Melodies.Blues|block": "blues", + "Melodies.Chase|block": "chase", + "Melodies.Dadadadum|block": "dadadum", + "Melodies.Entertainer|block": "entertainer", + "Melodies.Funeral|block": "funereal", + "Melodies.Funk|block": "funk", + "Melodies.JumpDown|block": "jump down", + "Melodies.JumpUp|block": "jump up", + "Melodies.Nyan|block": "nyan", + "Melodies.Ode|block": "ode", + "Melodies.PowerDown|block": "power down", + "Melodies.PowerUp|block": "power up", + "Melodies.Prelude|block": "prelude", + "Melodies.Punchline|block": "punchline", + "Melodies.Ringtone|block": "ringtone", + "Melodies.Wawawawaa|block": "wawawawaa", + "Melodies.Wedding|block": "wedding", + "MelodyOptions.ForeverInBackground|block": "forever in background", + "MelodyOptions.Forever|block": "forever", + "MelodyOptions.OnceInBackground|block": "once in background", + "MelodyOptions.Once|block": "once", "Motor.AB|block": "A and B", "MotorCommand.Break|block": "break", "MotorCommand.Coast|block": "coast", "MotorCommand.Sleep|block": "sleep", + "MusicEvent.BackgroundMelodyEnded|block": "background melody ended", + "MusicEvent.BackgroundMelodyNotePlayed|block": "background melody note played", + "MusicEvent.BackgroundMelodyPaused|block": "background melody paused", + "MusicEvent.BackgroundMelodyRepeated|block": "background melody repeated", + "MusicEvent.BackgroundMelodyResumed|block": "background melody resumed", + "MusicEvent.BackgroundMelodyStarted|block": "background melody started", + "MusicEvent.MelodyEnded|block": "melody ended", + "MusicEvent.MelodyNotePlayed|block": "melody note played", + "MusicEvent.MelodyRepeated|block": "melody repeated", + "MusicEvent.MelodyStarted|block": "melody started", "Note.CSharp3|block": "C#3", "Note.CSharp4|block": "C#4", "Note.CSharp5|block": "C#5", @@ -225,8 +261,11 @@ "motors.motorPower|block": "motor on at %percent", "motors|block": "motors", "music.beat|block": "%fraction|beat", + "music.beginMelody|block": "start melody %melody=device_builtin_melody| repeating %options", + "music.builtInMelody|block": "%melody", "music.changeTempoBy|block": "change tempo by (bpm)|%value", "music.noteFrequency|block": "%note", + "music.onEvent|block": "music on %value", "music.playTone|block": "play|tone %note=device_note|for %duration=device_beat", "music.rest|block": "rest(ms)|%duration=device_beat", "music.ringTone|block": "ring tone (Hz)|%note=device_note", diff --git a/libs/core/melodies.ts b/libs/core/melodies.ts new file mode 100644 index 00000000..bbeed368 --- /dev/null +++ b/libs/core/melodies.ts @@ -0,0 +1,119 @@ +/* +The MIT License (MIT) + +Copyright (c) 2013-2016 The MicroPython-on-micro:bit Developers, as listed +in the accompanying AUTHORS file + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Melodies from file microbitmusictunes.c https://github.com/bbcmicrobit/MicroPython + +enum Melodies { + //% block="dadadum" blockIdentity=music.builtInMelody + Dadadadum = 0, + //% block="entertainer" blockIdentity=music.builtInMelody + Entertainer, + //% block="prelude" blockIdentity=music.builtInMelody + Prelude, + //% block="ode" blockIdentity=music.builtInMelody + Ode, + //% block="nyan" blockIdentity=music.builtInMelody + Nyan, + //% block="ringtone" blockIdentity=music.builtInMelody + Ringtone, + //% block="funk" blockIdentity=music.builtInMelody + Funk, + //% block="blues" blockIdentity=music.builtInMelody + Blues, + //% block="birthday" blockIdentity=music.builtInMelody + Birthday, + //% block="wedding" blockIdentity=music.builtInMelody + Wedding, + //% block="funereal" blockIdentity=music.builtInMelody + Funeral, + //% block="punchline" blockIdentity=music.builtInMelody + Punchline, + //% block="baddy" blockIdentity=music.builtInMelody + Baddy, + //% block="chase" blockIdentity=music.builtInMelody + Chase, + //% block="ba ding" blockIdentity=music.builtInMelody + BaDing, + //% block="wawawawaa" blockIdentity=music.builtInMelody + Wawawawaa, + //% block="jump up" blockIdentity=music.builtInMelody + JumpUp, + //% block="jump down" blockIdentity=music.builtInMelody + JumpDown, + //% block="power up" blockIdentity=music.builtInMelody + PowerUp, + //% block="power down" blockIdentity=music.builtInMelody + PowerDown, +} + +namespace music { + + export function getMelody(melody: Melodies): string[] { + switch (melody) { + case Melodies.Dadadadum: + return ['r4:2', 'g', 'g', 'g', 'eb:8', 'r:2', 'f', 'f', 'f', 'd:8']; + case Melodies.Entertainer: + return ['d4:1', 'd#', 'e', 'c5:2', 'e4:1', 'c5:2', 'e4:1', 'c5:3', 'c:1', 'd', 'd#', 'e', 'c', 'd', 'e:2', 'b4:1', 'd5:2', 'c:4']; + case Melodies.Prelude: + return ['c4:1', 'e', 'g', 'c5', 'e', 'g4', 'c5', 'e', 'c4', 'e', 'g', 'c5', 'e', 'g4', 'c5', 'e', 'c4', 'd', 'g', 'd5', 'f', 'g4', 'd5', 'f', 'c4', 'd', 'g', 'd5', 'f', 'g4', 'd5', 'f', 'b3', 'd4', 'g', 'd5', 'f', 'g4', 'd5', 'f', 'b3', 'd4', 'g', 'd5', 'f', 'g4', 'd5', 'f', 'c4', 'e', 'g', 'c5', 'e', 'g4', 'c5', 'e', 'c4', 'e', 'g', 'c5', 'e', 'g4', 'c5', 'e']; + case Melodies.Ode: + return ['e4', 'e', 'f', 'g', 'g', 'f', 'e', 'd', 'c', 'c', 'd', 'e', 'e:6', 'd:2', 'd:8', 'e:4', 'e', 'f', 'g', 'g', 'f', 'e', 'd', 'c', 'c', 'd', 'e', 'd:6', 'c:2', 'c:8']; + case Melodies.Nyan: + return ['f#5:2', 'g#', 'c#:1', 'd#:2', 'b4:1', 'd5:1', 'c#', 'b4:2', 'b', 'c#5', 'd', 'd:1', 'c#', 'b4:1', 'c#5:1', 'd#', 'f#', 'g#', 'd#', 'f#', 'c#', 'd', 'b4', 'c#5', 'b4', 'd#5:2', 'f#', 'g#:1', 'd#', 'f#', 'c#', 'd#', 'b4', 'd5', 'd#', 'd', 'c#', 'b4', 'c#5', 'd:2', 'b4:1', 'c#5', 'd#', 'f#', 'c#', 'd', 'c#', 'b4', 'c#5:2', 'b4', 'c#5', 'b4', 'f#:1', 'g#', 'b:2', 'f#:1', 'g#', 'b', 'c#5', 'd#', 'b4', 'e5', 'd#', 'e', 'f#', 'b4:2', 'b', 'f#:1', 'g#', 'b', 'f#', 'e5', 'd#', 'c#', 'b4', 'f#', 'd#', 'e', 'f#', 'b:2', 'f#:1', 'g#', 'b:2', 'f#:1', 'g#', 'b', 'b', 'c#5', 'd#', 'b4', 'f#', 'g#', 'f#', 'b:2', 'b:1', 'a#', 'b', 'f#', 'g#', 'b', 'e5', 'd#', 'e', 'f#', 'b4:2', 'c#5']; + case Melodies.Ringtone: + return ['c4:1', 'd', 'e:2', 'g', 'd:1', 'e', 'f:2', 'a', 'e:1', 'f', 'g:2', 'b', 'c5:4']; + case Melodies.Funk: + return ['c2:2', 'c', 'd#', 'c:1', 'f:2', 'c:1', 'f:2', 'f#', 'g', 'c', 'c', 'g', 'c:1', 'f#:2', 'c:1', 'f#:2', 'f', 'd#']; + case Melodies.Blues: + return ['c2:2', 'e', 'g', 'a', 'a#', 'a', 'g', 'e', 'c2:2', 'e', 'g', 'a', 'a#', 'a', 'g', 'e', 'f', 'a', 'c3', 'd', 'd#', 'd', 'c', 'a2', 'c2:2', 'e', 'g', 'a', 'a#', 'a', 'g', 'e', 'g', 'b', 'd3', 'f', 'f2', 'a', 'c3', 'd#', 'c2:2', 'e', 'g', 'e', 'g', 'f', 'e', 'd']; + case Melodies.Birthday: + return ['c4:3', 'c:1', 'd:4', 'c:4', 'f', 'e:8', 'c:3', 'c:1', 'd:4', 'c:4', 'g', 'f:8', 'c:3', 'c:1', 'c5:4', 'a4', 'f', 'e', 'd', 'a#:3', 'a#:1', 'a:4', 'f', 'g', 'f:8']; + case Melodies.Wedding: + return ['c4:4', 'f:3', 'f:1', 'f:8', 'c:4', 'g:3', 'e:1', 'f:8', 'c:4', 'f:3', 'a:1', 'c5:4', 'a4:3', 'f:1', 'f:4', 'e:3', 'f:1', 'g:8']; + case Melodies.Funeral: + return ['c3:4', 'c:3', 'c:1', 'c:4', 'd#:3', 'd:1', 'd:3', 'c:1', 'c:3', 'b2:1', 'c3:4']; + case Melodies.Punchline: + return ['c4:3', 'g3:1', 'f#', 'g', 'g#:3', 'g', 'r', 'b', 'c4']; + case Melodies.Baddy: + return ['c3:3', 'r', 'd:2', 'd#', 'r', 'c', 'r', 'f#:8']; + case Melodies.Chase: + return ['a4:1', 'b', 'c5', 'b4', 'a:2', 'r', 'a:1', 'b', 'c5', 'b4', 'a:2', 'r', 'a:2', 'e5', 'd#', 'e', 'f', 'e', 'd#', 'e', 'b4:1', 'c5', 'd', 'c', 'b4:2', 'r', 'b:1', 'c5', 'd', 'c', 'b4:2', 'r', 'b:2', 'e5', 'd#', 'e', 'f', 'e', 'd#', 'e']; + case Melodies.BaDing: + return ['b5:1', 'e6:3']; + case Melodies.Wawawawaa: + return ['e3:3', 'r:1', 'd#:3', 'r:1', 'd:4', 'r:1', 'c#:8']; + case Melodies.JumpUp: + return ['c5:1', 'd', 'e', 'f', 'g']; + case Melodies.JumpDown: + return ['g5:1', 'f', 'e', 'd', 'c']; + case Melodies.PowerUp: + return ['g4:1', 'c5', 'e', 'g:2', 'e:1', 'g:3']; + case Melodies.PowerDown: + return ['g5:1', 'd#', 'c', 'g4:2', 'b:1', 'c5:3']; + default: + return []; + } + } +} \ No newline at end of file diff --git a/libs/core/music.cpp b/libs/core/music.cpp index 5a7e710f..48fddccf 100644 --- a/libs/core/music.cpp +++ b/libs/core/music.cpp @@ -6,10 +6,9 @@ namespace music { * @param frequency pitch of the tone to play in Hertz (Hz) * @param ms tone duration in milliseconds (ms) */ - //% help=music/play-tone weight=90 - //% blockId=device_play_note block="play|tone %note=device_note|for %duration=device_beat" icon="\uf025" blockGap=8 + //% //% parts="speaker" async useEnumVal=1 - void playTone(int frequency, int ms) { + void speakerPlayTone(int frequency, int ms) { if(frequency > 0) uBit.soundmotor.soundOn(frequency); else uBit.soundmotor.soundOff(); if(ms > 0) { diff --git a/libs/core/music.ts b/libs/core/music.ts index 249cfeed..d0d13070 100644 --- a/libs/core/music.ts +++ b/libs/core/music.ts @@ -119,15 +119,82 @@ enum BeatFraction { //% block="1/8" Eighth = 8, //% block="1/16" - Sixteenth = 16 + Sixteenth = 16, + //% block="2" + Double = 32, + //% block="4", + Breve = 64 +} + +enum MelodyOptions { + //% block="once"" + Once = 1, + //% block="forever" + Forever = 2, + //% block="once in background" + OnceInBackground = 4, + //% block="forever in background" + ForeverInBackground = 8 +} + +enum MusicEvent { + //% block="melody note played" + MelodyNotePlayed = 1, + //% block="melody started" + MelodyStarted = 2, + //% block="melody ended" + MelodyEnded = 3, + //% block="melody repeated" + MelodyRepeated = 4, + //% block="background melody note played" + BackgroundMelodyNotePlayed = MelodyNotePlayed | 0xf0, + //% block="background melody started" + BackgroundMelodyStarted = MelodyStarted | 0xf0, + //% block="background melody ended" + BackgroundMelodyEnded = MelodyEnded | 0xf0, + //% block="background melody repeated" + BackgroundMelodyRepeated = MelodyRepeated | 0xf0, + //% block="background melody paused" + BackgroundMelodyPaused = 5 | 0xf0, + //% block="background melody resumed" + BackgroundMelodyResumed = 6 | 0xf0 } /** - * Generation of music tones through pin ``P0``. + * Generation of music tones. */ //% color=#DF4600 weight=98 icon="\uf025" namespace music { let beatsPerMinute: number = 120; + let freqTable: number[] = []; + let _playTone: (frequency: number, duration: number) => void; + const MICROBIT_MELODY_ID = 2000; + + /** + * Plays a tone through pin ``P0`` for the given duration. + * @param frequency pitch of the tone to play in Hertz (Hz) + * @param ms tone duration in milliseconds (ms) + */ + //% help=music/play-tone weight=90 + //% blockId=device_play_note block="play|tone %note=device_note|for %duration=device_beat" blockGap=8 + //% parts="headphone" + //% useEnumVal=1 + export function playTone(frequency: number, ms: number): void { + if (_playTone) _playTone(frequency, ms); + else speakerPlayTone(frequency, ms); + } + + /** + * Plays a tone through pin ``P0``. + * @param frequency pitch of the tone to play in Hertz (Hz) + */ + //% help=music/ring-tone weight=80 + //% blockId=device_ring block="ring tone (Hz)|%note=device_note" blockGap=8 + //% parts="headphone" + //% useEnumVal=1 + export function ringTone(frequency: number): void { + playTone(frequency, 0); + } /** * Rests (plays nothing) for a specified time through pin ``P0``. @@ -135,38 +202,28 @@ namespace music { */ //% help=music/rest weight=79 //% blockId=device_rest block="rest(ms)|%duration=device_beat" - //% parts="speaker" + //% parts="headphone" export function rest(ms: number): void { playTone(0, ms); } - /** - * Plays a tone through ``speaker``. - * @param frequency pitch of the tone to play in Hertz (Hz) - */ - //% help=music/ring-tone weight=80 - //% blockId=device_ring block="ring tone (Hz)|%note=device_note" blockGap=8 - //% parts="speaker" async - //% useEnumVal=1 - export function ringTone(frequency: number) { - playTone(frequency, 0); - } /** * Gets the frequency of a note. - * @param name the note name + * @param name the note name, eg: Note.C */ //% weight=50 help=music/note-frequency //% blockId=device_note block="%note" - //% shim=TD_ID blockHidden=true - //% blockFieldEditor="note_editor" - //% useEnumVal = 1 + //% shim=TD_ID + //% note.fieldEditor="note" note.defl="262" + //% useEnumVal=1 export function noteFrequency(name: Note): number { return name; } function init() { if (beatsPerMinute <= 0) beatsPerMinute = 120; + if (freqTable.length == 0) freqTable = [31, 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, 65, 69, 73, 78, 82, 87, 92, 98, 104, 110, 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, 4186] } /** @@ -178,11 +235,15 @@ namespace music { init(); if (fraction == null) fraction = BeatFraction.Whole; let beat = 60000 / beatsPerMinute; - if (fraction == BeatFraction.Whole) return beat; - else if (fraction == BeatFraction.Half) return beat / 2; - else if (fraction == BeatFraction.Quarter) return beat / 4; - else if (fraction == BeatFraction.Eighth) return beat / 8; - else return beat / 16; + switch (fraction) { + case BeatFraction.Half: return beat / 2; + case BeatFraction.Quarter: return beat / 4; + case BeatFraction.Eighth: return beat / 8; + case BeatFraction.Sixteenth: return beat / 16; + case BeatFraction.Double: return beat * 2; + case BeatFraction.Breve: return beat * 4; + default: return beat; + } } /** @@ -212,10 +273,166 @@ namespace music { */ //% help=music/set-tempo weight=38 //% blockId=device_set_tempo block="set tempo to (bpm)|%value" + //% bpm.min=4 bpm.max=400 export function setTempo(bpm: number): void { init(); if (bpm > 0) { beatsPerMinute = Math.max(1, bpm); } } + + let currentMelody: Melody; + let currentBackgroundMelody: Melody; + + /** + * Gets the melody array of a built-in melody. + * @param name the note name, eg: Note.C + */ + //% weight=50 help=music/builtin-melody + //% blockId=device_builtin_melody block="%melody" + //% blockHidden=true + export function builtInMelody(melody: Melodies): string[] { + return getMelody(melody); + } + + /** + * Registers code to run on various melody events + */ + //% blockId=melody_on_event block="music on %value" + //% help=music/on-event weight=59 + export function onEvent(value: MusicEvent, handler: Action) { + control.onEvent(MICROBIT_MELODY_ID, value, handler); + } + + /** + * Starts playing a melody. + * Notes are expressed as a string of characters with this format: NOTE[octave][:duration] + * @param melodyArray the melody array to play, eg: ['g5:1'] + * @param options melody options, once / forever, in the foreground / background + */ + //% help=music/begin-melody weight=60 blockGap=8 + //% blockId=device_start_melody block="start melody %melody=device_builtin_melody| repeating %options" + //% parts="headphone" + export function beginMelody(melodyArray: string[], options: MelodyOptions = 1) { + init(); + if (currentMelody != undefined) { + if (((options & MelodyOptions.OnceInBackground) == 0) + && ((options & MelodyOptions.ForeverInBackground) == 0) + && currentMelody.background) { + currentBackgroundMelody = currentMelody; + currentMelody = null; + control.raiseEvent(MICROBIT_MELODY_ID, MusicEvent.BackgroundMelodyPaused); + } + if (currentMelody) + control.raiseEvent(MICROBIT_MELODY_ID, currentMelody.background ? MusicEvent.BackgroundMelodyEnded : MusicEvent.MelodyEnded); + currentMelody = new Melody(melodyArray, options); + control.raiseEvent(MICROBIT_MELODY_ID, currentMelody.background ? MusicEvent.BackgroundMelodyStarted : MusicEvent.MelodyStarted); + } else { + currentMelody = new Melody(melodyArray, options); + control.raiseEvent(MICROBIT_MELODY_ID, currentMelody.background ? MusicEvent.BackgroundMelodyStarted : MusicEvent.MelodyStarted); + // Only start the fiber once + control.inBackground(() => { + while (currentMelody.hasNextNote()) { + playNextNote(currentMelody); + if (!currentMelody.hasNextNote() && currentBackgroundMelody) { + // Swap the background melody back + currentMelody = currentBackgroundMelody; + currentBackgroundMelody = null; + control.raiseEvent(MICROBIT_MELODY_ID, MusicEvent.MelodyEnded); + control.raiseEvent(MICROBIT_MELODY_ID, MusicEvent.BackgroundMelodyResumed); + } + } + control.raiseEvent(MICROBIT_MELODY_ID, currentMelody.background ? MusicEvent.BackgroundMelodyEnded : MusicEvent.MelodyEnded); + currentMelody = null; + }) + } + } + + /** + * Sets a custom playTone function for playing melodies + */ + //% help=music/set-play-tone + //% advanced=true + export function setPlayTone(f: (frequency: number, duration: number) => void) { + _playTone = f; + } + + function playNextNote(melody: Melody): void { + // cache elements + let currNote = melody.nextNote(); + let currentPos = melody.currentPos; + let currentDuration = melody.currentDuration; + let currentOctave = melody.currentOctave; + + let note: number; + let isrest: boolean = false; + let beatPos: number; + let parsingOctave: boolean = true; + + for (let pos = 0; pos < currNote.length; pos++) { + let noteChar = currNote.charAt(pos); + switch (noteChar) { + case 'c': case 'C': note = 1; break; + case 'd': case 'D': note = 3; break; + case 'e': case 'E': note = 5; break; + case 'f': case 'F': note = 6; break; + case 'g': case 'G': note = 8; break; + case 'a': case 'A': note = 10; break; + case 'b': case 'B': note = 12; break; + case 'r': case 'R': isrest = true; break; + case '#': note++; break; + case 'b': note--; break; + case ':': parsingOctave = false; beatPos = pos; break; + default: if (parsingOctave) currentOctave = parseInt(noteChar); + } + } + if (!parsingOctave) { + currentDuration = parseInt(currNote.substr(beatPos + 1, currNote.length - beatPos)); + } + let beat = (60000 / beatsPerMinute) / 4; + if (isrest) { + music.rest(currentDuration * beat) + } else { + let keyNumber = note + (12 * (currentOctave - 1)); + let frequency = keyNumber >= 0 && keyNumber < freqTable.length ? freqTable[keyNumber] : 0; + music.playTone(frequency, currentDuration * beat); + } + melody.currentDuration = currentDuration; + melody.currentOctave = currentOctave; + const repeating = melody.repeating && currentPos == melody.melodyArray.length - 1; + melody.currentPos = repeating ? 0 : currentPos + 1; + + control.raiseEvent(MICROBIT_MELODY_ID, melody.background ? MusicEvent.BackgroundMelodyNotePlayed : MusicEvent.MelodyNotePlayed); + if (repeating) + control.raiseEvent(MICROBIT_MELODY_ID, melody.background ? MusicEvent.BackgroundMelodyRepeated : MusicEvent.MelodyRepeated); + } + + class Melody { + public melodyArray: string[]; + public currentDuration: number; + public currentOctave: number; + public currentPos: number; + public repeating: boolean; + public background: boolean; + + constructor(melodyArray: string[], options: MelodyOptions) { + this.melodyArray = melodyArray; + this.repeating = ((options & MelodyOptions.Forever) != 0); + this.repeating = this.repeating ? true : ((options & MelodyOptions.ForeverInBackground) != 0) + this.background = ((options & MelodyOptions.OnceInBackground) != 0); + this.background = this.background ? true : ((options & MelodyOptions.ForeverInBackground) != 0); + this.currentDuration = 4; //Default duration (Crotchet) + this.currentOctave = 4; //Middle octave + this.currentPos = 0; + } + + hasNextNote() { + return this.repeating || this.currentPos < this.melodyArray.length; + } + + nextNote(): string { + const currentNote = this.melodyArray[this.currentPos]; + return currentNote; + } + } } diff --git a/libs/core/pxt.json b/libs/core/pxt.json index 8d77ac4c..33b394d5 100644 --- a/libs/core/pxt.json +++ b/libs/core/pxt.json @@ -27,6 +27,7 @@ "led.ts", "motors.cpp", "music.cpp", + "melodies.ts", "music.ts", "pins.cpp", "pins.ts", diff --git a/libs/core/shims.d.ts b/libs/core/shims.d.ts index 0ba88887..a32b5022 100644 --- a/libs/core/shims.d.ts +++ b/libs/core/shims.d.ts @@ -552,10 +552,9 @@ declare namespace music { * @param frequency pitch of the tone to play in Hertz (Hz) * @param ms tone duration in milliseconds (ms) */ - //% help=music/play-tone weight=90 - //% blockId=device_play_note block="play|tone %note=device_note|for %duration=device_beat" icon="\uf025" blockGap=8 - //% parts="speaker" async useEnumVal=1 shim=music::playTone - function playTone(frequency: number, ms: number): void; + //% + //% parts="speaker" async useEnumVal=1 shim=music::speakerPlayTone + function speakerPlayTone(frequency: number, ms: number): void; } declare namespace pins { diff --git a/pxtarget.json b/pxtarget.json index 2a841ffd..b1c28774 100644 --- a/pxtarget.json +++ b/pxtarget.json @@ -57,7 +57,8 @@ "listsBlocks": true, "functionBlocks": true, "onStartColor": "#54C9C9", - "onStartNamespace": "basic" + "onStartNamespace": "basic", + "onStartWeight": 54 }, "simulator": { "autoRun": true, diff --git a/sim/state/music.ts b/sim/state/music.ts index cb040e4c..1b47bb20 100644 --- a/sim/state/music.ts +++ b/sim/state/music.ts @@ -7,7 +7,7 @@ namespace pxsim { } namespace pxsim.music { - export function playTone(frequency: number, ms: number) { + export function speakerPlayTone(frequency: number, ms: number) { const b = board(); b.speakerState.frequency = frequency; b.speakerState.ms = ms; From a5f8e9a643863b9c462b42667ddf316b229810f5 Mon Sep 17 00:00:00 2001 From: Peli de Halleux Date: Thu, 14 Dec 2017 10:34:32 -0800 Subject: [PATCH 2/2] upgrading game apis (#13) --- libs/core/game.ts | 187 ++++++++++++++++++++++++++--------------- libs/core/led.cpp | 8 ++ sim/state/ledmatrix.ts | 4 + 3 files changed, 131 insertions(+), 68 deletions(-) diff --git a/libs/core/game.ts b/libs/core/game.ts index 4f5eb73c..28153310 100644 --- a/libs/core/game.ts +++ b/libs/core/game.ts @@ -32,22 +32,22 @@ namespace game { let _countdownPause: number = 0; let _level: number = 1; let _gameId: number = 0; - let img: Image; - let sprites: LedSprite[]; + let _img: Image; + let _sprites: LedSprite[]; + let _paused: boolean = false; + let _backgroundAnimation = false; // indicates if an auxiliary animation (and fiber) is already running /** * Creates a new LED sprite pointing to the right. * @param x sprite horizontal coordinate, eg: 2 * @param y sprite vertical coordinate, eg: 2 */ - //% weight=60 + //% weight=60 blockGap=8 help=game/create-sprite //% blockId=game_create_sprite block="create sprite at|x: %x|y: %y" //% parts="ledmatrix" export function createSprite(x: number, y: number): LedSprite { init(); let p = new LedSprite(x, y); - sprites.push(p); - plot(); return p; } @@ -61,7 +61,7 @@ namespace game { } /** - * Adds points to the current score + * Adds points to the current score and shows an animation * @param points amount of points to change, eg: 1 */ //% weight=10 help=game/add-score @@ -69,18 +69,22 @@ namespace game { //% parts="ledmatrix" export function addScore(points: number): void { setScore(_score + points); - control.inBackground(() => { - led.stopAnimation(); - basic.showAnimation(`0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 -0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 -0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0`, 20); - }); + if (!_paused && !_backgroundAnimation) { + _backgroundAnimation = true; + control.inBackground(() => { + led.stopAnimation(); + basic.showAnimation(`0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 + 0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 + 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0 1 0 0 0 0 0`, 20); + _backgroundAnimation = false; + }); + } } /** - * Starts a game countdown timer + * Shows an animation, then starts a game countdown timer, which causes Game Over when it reaches 0 * @param ms countdown duration in milliseconds, eg: 10000 */ //% weight=9 help=game/start-countdown @@ -96,6 +100,7 @@ namespace game { _countdownPause = Math.max(500, ms); _startTime = -1; _endTime = input.runningTime() + _countdownPause; + _paused = false; control.inBackground(() => { basic.pause(_countdownPause); gameOver(); @@ -104,7 +109,7 @@ namespace game { } /** - * Displays a game over animation. + * Displays a game over animation and the score. */ //% weight=8 help=game/game-over //% blockId=game_game_over block="game over" @@ -115,7 +120,6 @@ namespace game { unplugEvents(); led.stopAnimation(); led.setBrightness(255); - led.setDisplayMode(DisplayMode.BackAndWhite); while (true) { for (let i = 0; i < 8; i++) { basic.clearScreen(); @@ -146,8 +150,9 @@ namespace game { /** * Sets the current score value - * @param value TODO + * @param value new score value. */ + //% blockId=game_set_score block="set score %points" blockGap=8 //% weight=10 help=game/set-score export function setScore(value: number): void { _score = Math.max(0, value); @@ -202,14 +207,15 @@ namespace game { //% parts="ledmatrix" export function removeLife(life: number): void { setLife(_life - life); - control.inBackground(() => { - led.stopAnimation(); - basic.showAnimation(`1 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 + if (!_paused) + control.inBackground(() => { + led.stopAnimation(); + basic.showAnimation(`1 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0`, 40); - }); + }); } /** @@ -264,10 +270,38 @@ namespace game { * Indicates if the game is display the game over sequence. */ export function isGameOver(): boolean { - let over: boolean; return _isGameOver; } + /** + * Indicates if the game rendering is paused to allow other animations + */ + //% + export function isPaused(): boolean { + return _paused; + } + + /** + * Pauses the game rendering engine to allow other animations + */ + //% blockId=game_pause block="pause" + //% advanced=true blockGap=8 help=game/pause + export function pause(): void { + plot() + _paused = true; + } + + + /** + * Resumes the game rendering engine + */ + //% blockId=game_resume block="resume" + //% advanced=true blockGap=8 help=game/resumeP + export function resume(): void { + _paused = false; + plot(); + } + /** * returns false if game can't start */ @@ -287,29 +321,35 @@ namespace game { }); } + /** + * A game sprite rendered as a single LED + */ + //% export class LedSprite { private _x: number; private _y: number; private _dir: number; private _brightness: number; private _blink: number; + private _enabled: boolean; constructor(x: number, y: number) { this._x = Math.clamp(0, 4, x); this._y = Math.clamp(0, 4, y); this._dir = 90; this._brightness = 255; + this._enabled = true; init(); - sprites.push(this); + _sprites.push(this); plot(); } /** - * Move a certain number of LEDs + * Move a certain number of LEDs in the current direction * @param this the sprite to move * @param leds number of leds to move, eg: 1, -1 */ - //% weight=50 + //% weight=50 help=game/move //% blockId=game_move_sprite block="%sprite|move by %leds" blockGap=8 //% parts="ledmatrix" public move(leds: number): void { @@ -355,10 +395,10 @@ namespace game { } /** - * If touching the edge of the stage, then bounce away. + * If touching the edge of the stage and facing towards it, then turn away. * @param this TODO */ - //% weight=18 + //% weight=18 help=game/if-on-edge-bounce //% blockId=game_sprite_bounce block="%sprite|if on edge, bounce" //% parts="ledmatrix" public ifOnEdgeBounce(): void { @@ -412,7 +452,7 @@ namespace game { * @param direction left or right * @param degrees angle in degrees to turn, eg: 45, 90, 180, 135 */ - //% weight=49 + //% weight=49 help=game/turn //% blockId=game_turn_sprite block="%sprite|turn %direction|by (°) %degrees" public turn(direction: Direction, degrees: number) { if (direction == Direction.Right) @@ -444,7 +484,7 @@ namespace game { * @param property the name of the property to change * @param the updated value */ - //% weight=29 + //% weight=29 help=game/set //% blockId=game_sprite_set_property block="%sprite|set %property|to %value" blockGap=8 public set(property: LedSpriteProperty, value: number) { switch (property) { @@ -461,7 +501,7 @@ namespace game { * @param property the name of the property to change * @param value amount of change, eg: 1 */ - //% weight=30 + //% weight=30 help=game/change //% blockId=game_sprite_change_xy block="%sprite|change %property|by %value" blockGap=8 public change(property: LedSpriteProperty, value: number) { switch (property) { @@ -477,7 +517,7 @@ namespace game { * Gets a property of the sprite * @param property the name of the property to change */ - //% weight=28 + //% weight=28 help=game/get //% blockId=game_sprite_property block="%sprite|%property" public get(property: LedSpriteProperty) { switch (property) { @@ -567,21 +607,21 @@ namespace game { } /** - * Reports true if sprite is touching specified sprite + * Reports true if sprite has the same position as specified sprite * @param this TODO * @param other TODO */ - //% weight=20 + //% weight=20 help=game/is-touching //% blockId=game_sprite_touching_sprite block="%sprite|touching %other|?" blockGap=8 public isTouching(other: LedSprite): boolean { - return this._x == other._x && this._y == other._y; + return this._enabled && other._enabled && this._x == other._x && this._y == other._y; } /** * Reports true if sprite is touching an edge * @param this TODO */ - //% weight=19 + //% weight=19 help=game/is-touching-edge //% blockId=game_sprite_touching_edge block="%sprite|touching edge?" blockGap=8 public isTouchingEdge(): boolean { return this._x == 0 || this._x == 4 || this._y == 0 || this._y == 4; @@ -589,7 +629,7 @@ namespace game { /** * Turns on the sprite (on by default) - * @param this TODO + * @param this the sprite */ public on(): void { this.setBrightness(255); @@ -597,7 +637,7 @@ namespace game { /** * Turns off the sprite (on by default) - * @param this TODO + * @param this the sprite */ public off(): void { this.setBrightness(0); @@ -605,8 +645,8 @@ namespace game { /** * Set the ``brightness`` of a sprite - * @param this TODO - * @param brightness TODO + * @param this the sprite + * @param brightness the brightness from 0 (off) to 255 (on), eg: 255. */ //% parts="ledmatrix" public setBrightness(brightness: number): void { @@ -616,8 +656,9 @@ namespace game { /** * Reports the ``brightness` of a sprite on the LED screen - * @param this TODO + * @param this the sprite */ + //% parts="ledmatrix" public brightness(): number { let r: number; return this._brightness; @@ -625,8 +666,8 @@ namespace game { /** * Changes the ``y`` position by the given amount - * @param this TODO - * @param value TODO + * @param this the sprite + * @param value the value to change brightness */ public changeBrightnessBy(value: number): void { this.setBrightness(this._brightness + value); @@ -642,11 +683,15 @@ namespace game { } /** - * Deletes the sprite from the game engine. All further operation of the sprite will not have any effect. - * @param sprite TODO + * Deletes the sprite from the game engine. The sprite will no longer appear on the screen or interact with other sprites. + * @param this sprite to delete */ - public delete(sprite: LedSprite): void { - sprites.removeElement(sprite); + //% weight=59 help=game/delete + //% blockId="game_delete_sprite" block="delete %this" + public delete(): void { + this._enabled = false; + if (_sprites.removeElement(this)) + plot(); } /** @@ -686,30 +731,29 @@ namespace game { r = (now / ps._blink) % 2; } if (r == 0) { - img.setPixelBrightness(ps._x, ps._y, img.pixelBrightness(ps._x, ps._y) + ps._brightness); + _img.setPixelBrightness(ps._x, ps._y, _img.pixelBrightness(ps._x, ps._y) + ps._brightness); } } } } function init(): void { - if (img == null) { - img = images.createImage( - `0 0 0 0 0 + if (_img) return; + const img = images.createImage( +`0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0`); - sprites = ([]); - led.setDisplayMode(DisplayMode.Greyscale); - basic.forever(() => { - basic.pause(30); - plot(); - if (game.isGameOver()) { - basic.pause(600); - } - }); - } + _sprites = ([]); + basic.forever(() => { + basic.pause(30); + plot(); + if (game.isGameOver()) { + basic.pause(600); + } + }); + _img = img; } /** @@ -717,15 +761,23 @@ namespace game { */ //% parts="ledmatrix" function plot(): void { - if (game.isGameOver()) { + if (game.isGameOver() || game.isPaused() || !_img || _backgroundAnimation) { return; } - let now = input.runningTime(); - img.clear(); - for (let i = 0; i < sprites.length; i++) { - sprites[i]._plot(now); + // ensure greyscale mode + const dm = led.displayMode(); + if (dm != DisplayMode.Greyscale) + led.setDisplayMode(DisplayMode.Greyscale); + // render sprites + const now = input.runningTime(); + _img.clear(); + for (let i = 0; i < _sprites.length; i++) { + _sprites[i]._plot(now); } - img.plotImage(0); + _img.plotImage(0); + // restore previous display mode + if (dm != DisplayMode.Greyscale) + led.setDisplayMode(dm); } /** @@ -737,4 +789,3 @@ namespace game { } } - diff --git a/libs/core/led.cpp b/libs/core/led.cpp index 5054cd6c..a35cf8b9 100644 --- a/libs/core/led.cpp +++ b/libs/core/led.cpp @@ -92,6 +92,14 @@ namespace led { uBit.display.setDisplayMode((DisplayMode)mode); } + /** + * Gets the current display mode + */ + //% weight=1 parts="ledmatrix" advanced=true + DisplayMode_ displayMode() { + return (DisplayMode_)uBit.display.getDisplayMode(); + } + /** * Turns on or off the display */ diff --git a/sim/state/ledmatrix.ts b/sim/state/ledmatrix.ts index e105953c..f8d32ab8 100644 --- a/sim/state/ledmatrix.ts +++ b/sim/state/ledmatrix.ts @@ -286,6 +286,10 @@ namespace pxsim.led { runtime.queueDisplayUpdate() } + export function displayMode() : DisplayMode { + return board().ledMatrixState.displayMode; + } + export function screenshot(): Image { let img = createImage(5) board().ledMatrixState.image.copyTo(0, 5, img, 0);