play()

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
                }
            }
        }