pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
676 lines (589 loc) • 23.9 kB
text/typescript
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')
}