/* * bytebeat-render.js [Node] * * Node.JS: Bytebeat generator * * Note: Calls `ffmpeg` to convert output raw audio to a WAVE file * * Warning: The current result is quick and dirty. Not for educational or * production purposes. * * TODO: Fix signed bytebeat and floatbeat * TODO: Add support for arbitrary (1 <= integer <= 2^32 - 1, because of * WAVE/RIFF format limitations) audio bit depth * TODO: Do not use FFmpeg, use `pcm.js` or simply write the WAVE/RIFF file * headers * * Author: Intel A80486DX2-66 * License: Creative Commons Zero 1.0 Universal */ const { appendFileSync, unlinkSync, writeFileSync } = require("fs") const { tmpdir } = require("os") const { execSync } = require("child_process") const { basename } = require("path") let BUFFER_SIZE = 65536 // feel free to change this const LIGHTNING_MODE = false // disables sequential file write optimization, // feel free to enable this const SAMPLE_RATE = 8000 // feel free to change this const SECONDS = 30 // feel free to change this const CHANNELS = 1 // feel free to change this const FINAL_SAMPLE_RATE = 44100 // feel free to change this const FINAL_SAMPLE_RATE_CONVERSION = SAMPLE_RATE / FINAL_SAMPLE_RATE / CHANNELS const SAMPLES = SECONDS * FINAL_SAMPLE_RATE // feel free to change this const PRODUCT = SAMPLES * CHANNELS BUFFER_SIZE = LIGHTNING_MODE ? PRODUCT : BUFFER_SIZE const TYPE_BYTEBEAT = 0 const TYPE_SIGNED_BYTEBEAT = 1 const TYPE_FLOATBEAT = 2 const SELECTED_TYPE = TYPE_BYTEBEAT // feel free to change this // for bytebeat var int = x => Math.floor(x) var abs = Math.abs var acos = Math.acos var acosh = Math.acosh var asin = Math.asin var asinh = Math.asinh var atan = Math.atan var atanh = Math.atanh var cbrt = Math.cbrt var cos = Math.cos var cosh = Math.cosh var exp = Math.exp var floor = Math.floor var log = Math.log var log2 = Math.log2 var max = Math.max var min = Math.min var PI = Math.PI var pow = Math.pow var random = Math.random var sin = Math.sin var sinh = Math.sinh var sqrt = Math.sqrt var tan = Math.tan var tanh = Math.tanh const generateAudio = t => { return t&t>>8 } const clamp = (a, b, c) => max(min(a, c), b) let lastCorrectSample = 127 // FIXME: guessed value const constrainValue = sample => { if (isNaN(sample) || sample < 0) sample = lastCorrectSample else lastCorrectSample = sample switch (SELECTED_TYPE) { case TYPE_BYTEBEAT: return sample & 255 case TYPE_SIGNED_BYTEBEAT: return ((sample + 127) & 255) - 128 case TYPE_FLOATBEAT: // NOTE: temporary fix copied from a code by lehandsomeguy, see https:// // www.reddit.com/r/bytebeat/comments/48r00a/floatbeat_test/ return floor((clamp(sample, -0.9999, 0.9999) * 128) + 128) } } const random_choice = choices => choices[Math.floor(Math.random() * choices.length)] const randomFileNameAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-" const generateRandomFilePath = () => { let res = tmpdir() + "/" + basename(__filename) + "_" for (let i = 0; i < 64; i++) res += random_choice(randomFileNameAlphabet) return res } let t = 0 let filePath = generateRandomFilePath() writeFileSync(filePath, Buffer.alloc(0)) // the loop of sequential file writing, created to ease load on RAM const buffer_max = Math.floor((PRODUCT + (BUFFER_SIZE - 1)) / BUFFER_SIZE), needTwoBuffers = buffer_max > 1, needSingleBuffer = !needTwoBuffers let audioData = new Uint8Array(needSingleBuffer ? PRODUCT : BUFFER_SIZE) for (let seq = 0; seq < buffer_max; seq++) { if (needTwoBuffers && (t + BUFFER_SIZE) >= PRODUCT) { let calculatedSize = PRODUCT - t audioData = new Uint8Array(calculatedSize) } for (let idx = 0; t < PRODUCT && idx < BUFFER_SIZE; idx++, t++) { let sample = generateAudio(t * FINAL_SAMPLE_RATE_CONVERSION) if (sample.constructor === Array) sample.forEach((sample, index) => { audioData[CHANNELS * idx + index] = constrainValue(sample) }) else audioData[idx] = constrainValue(sample) } appendFileSync(filePath, Buffer.from(audioData.buffer)) } execSync( `ffmpeg -f u8 -ar ${FINAL_SAMPLE_RATE} -ac ${CHANNELS} ` + `-i ${filePath} output_${+new Date()}.wav`) unlinkSync(filePath)