Skip to content

Instantly share code, notes, and snippets.

@sebm
Last active September 23, 2020 07:02
Show Gist options
  • Select an option

  • Save sebm/cbd64e30e21edd7049a9e5a7cd62a2fe to your computer and use it in GitHub Desktop.

Select an option

Save sebm/cbd64e30e21edd7049a9e5a7cd62a2fe to your computer and use it in GitHub Desktop.

Revisions

  1. sebm revised this gist Sep 23, 2020. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions twilio-function.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    exports.handler = function(context, event, callback) {
    const twiml = new Twilio.twiml.VoiceResponse();
    const { bpm } = event;

    twiml.play(`💃.cloudfunctions.net/metronome?bpm=${bpm}`)

    return callback(null, twiml);
    };
  2. sebm revised this gist Sep 23, 2020. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions metronome.js
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    // Google Cloud Source repos don't allow world-readable so this is the next best thing
    // this is the code as of SHA b34717ce19fa9da940333402e7534c0e99f687a3
    const synth = require("synth-js");
    const MidiWriter = require("midi-writer-js");
    const ffmpeg = require("fluent-ffmpeg");
  3. sebm created this gist Sep 23, 2020.
    92 changes: 92 additions & 0 deletions metronome.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,92 @@
    const synth = require("synth-js");
    const MidiWriter = require("midi-writer-js");
    const ffmpeg = require("fluent-ffmpeg");
    const { Readable } = require("stream");
    const concat = require("concat-stream");

    const MEASURES = 100;

    const THE_ONE = { pitch: ["A6"], duration: "16" };
    const TWO_THREE_FOUR = {
    pitch: ["A4"],
    duration: "16",
    wait: ["16", "16", "16"],
    repeat: "3",
    };

    const MAX_BPM = 400;

    function validateRequest(req) {
    const { bpm } = req.query;
    if (!bpm) {
    throw new Error("bpm param must be set");
    }

    if (Number.isNaN(Number.parseInt(bpm))) {
    throw new Error("bpm must be a number");
    }

    if (bpm > MAX_BPM) {
    throw new Error(`bpm must be less than ${MAX_BPM}`);
    }

    if (bpm < 40) {
    throw new Error("bpm must be less than 40");
    }
    }

    exports.metronome = (req, res) => {
    try {
    validateRequest(req);
    // Start with a new track
    const track = new MidiWriter.Track();
    const { bpm } = req.query;

    // Define an instrument (optional):
    track.addEvent(new MidiWriter.ProgramChangeEvent({ instrument: 1 }));
    track.setTempo(bpm);

    track.addEvent(
    [
    new MidiWriter.NoteEvent(THE_ONE),
    new MidiWriter.NoteEvent(TWO_THREE_FOUR),
    ],
    () => ({ sequential: true })
    );

    for (let x = 0; x < MEASURES; x++) {
    track.addEvent(
    [
    new MidiWriter.NoteEvent({ ...THE_ONE, wait: ["16", "16", "16"] }),
    new MidiWriter.NoteEvent(TWO_THREE_FOUR),
    ],
    () => ({ sequential: true })
    );
    }

    const midiWriter = new MidiWriter.Writer(track);

    const midiBuffer = new Buffer.from(midiWriter.buildFile());
    const wavBuffer = synth.midiToWav(midiBuffer).toBuffer();
    const metronomeWavStream = Readable.from(wavBuffer);
    const filename = `${bpm}.mp3`;

    res.setHeader("Content-disposition", "attachment; filename=" + filename);
    res.contentType("mp3");

    const concatStream = concat((buffer) => {
    res.send(buffer);
    res.end();
    });

    ffmpeg()
    .format("mp3")
    .audioCodec("libmp3lame")
    .audioBitrate(8)
    .audioFrequency(12000)
    .input(metronomeWavStream)
    .pipe(concatStream);
    } catch (err) {
    res.status(500).send(err.message);
    }
    };