in libs/mixer/melody.ts [349:583]
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
}
}
}