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;