// 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); } };