1
0
mirror of https://gitlab.com/80486DX2-66/gists synced 2024-11-08 08:54:34 +05:30
gists/js-programming/bytebeat-render.js

156 lines
4.3 KiB
JavaScript

/*
* 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
switch (SELECTED_TYPE) {
case TYPE_BYTEBEAT:
lastCorrectSample = 127
break;
case TYPE_SIGNED_BYTEBEAT:
lastCorrectSample = 0
break;
case TYPE_FLOATBEAT:
lastCorrectSample = 0.0
break;
}
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)