mirror of
https://gitlab.com/80486DX2-66/gists
synced 2024-11-15 05:45:55 +05:30
121 lines
3.4 KiB
JavaScript
121 lines
3.4 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
|
|
*
|
|
* 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
|
|
const int = x => Math.floor(x)
|
|
const abs = Math.abs
|
|
const cbrt = Math.cbrt
|
|
const cos = Math.cos
|
|
const cosh = Math.cosh
|
|
const floor = Math.floor
|
|
const log = Math.log
|
|
const log2 = Math.log2
|
|
const PI = Math.PI
|
|
const pow = Math.pow
|
|
const random = Math.random
|
|
const sin = Math.sin
|
|
const sinh = Math.sinh
|
|
const sqrt = Math.sqrt
|
|
const tan = Math.tan
|
|
const tanh = Math.tanh
|
|
|
|
const generateAudio = t => {
|
|
return t&t>>8
|
|
}
|
|
|
|
const constrainValue = sample => {
|
|
switch (SELECTED_TYPE) {
|
|
case TYPE_BYTEBEAT:
|
|
return sample & 255
|
|
case TYPE_SIGNED_BYTEBEAT:
|
|
return ((sample + 127) & 255) - 128
|
|
case TYPE_FLOATBEAT:
|
|
return floor(sample * 127) + 127
|
|
}
|
|
}
|
|
|
|
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 max = Math.floor((PRODUCT + (BUFFER_SIZE - 1)) / BUFFER_SIZE),
|
|
needTwoBuffers = max > 1, needSingleBuffer = !needTwoBuffers
|
|
|
|
let audioData = new Uint8Array(needSingleBuffer ? PRODUCT : BUFFER_SIZE)
|
|
|
|
for (let seq = 0; seq < 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)
|