Skip to content

Instantly share code, notes, and snippets.

@sebm
Last active September 23, 2020 07:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sebm/cbd64e30e21edd7049a9e5a7cd62a2fe to your computer and use it in GitHub Desktop.
Save sebm/cbd64e30e21edd7049a9e5a7cd62a2fe to your computer and use it in GitHub Desktop.
Render a metronome mp3 in response to an HTTP request
// 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");
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);
}
};
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);
};
@sebm
Copy link
Author

sebm commented Sep 23, 2020

Screen Shot 2020-09-22 at 23 55 56

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment