temporary
This commit is contained in:
154
src/lib/bytebeat/BytebeatGenerator.ts
Normal file
154
src/lib/bytebeat/BytebeatGenerator.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import type { BytebeatOptions, BitDepth } from './types'
|
||||
import { encodeWAV } from './wavEncoder'
|
||||
import { EffectsChain } from './EffectsChain'
|
||||
import type { EffectValues } from '../../types/effects'
|
||||
|
||||
export class BytebeatGenerator {
|
||||
private sampleRate: number
|
||||
private duration: number
|
||||
private formula: string | null = null
|
||||
private compiledFormula: ((t: number) => number) | null = null
|
||||
private audioBuffer: Float32Array | null = null
|
||||
private audioContext: AudioContext | null = null
|
||||
private sourceNode: AudioBufferSourceNode | null = null
|
||||
private effectsChain: EffectsChain | null = null
|
||||
private effectValues: EffectValues = {}
|
||||
private startTime: number = 0
|
||||
private pauseTime: number = 0
|
||||
private isLooping: boolean = true
|
||||
|
||||
constructor(options: BytebeatOptions = {}) {
|
||||
this.sampleRate = options.sampleRate ?? 8000
|
||||
this.duration = options.duration ?? 10
|
||||
}
|
||||
|
||||
setFormula(formula: string): void {
|
||||
this.formula = formula
|
||||
try {
|
||||
this.compiledFormula = new Function('t', `return ${formula}`) as (t: number) => number
|
||||
this.audioBuffer = null
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid formula: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
generate(): Float32Array {
|
||||
if (!this.compiledFormula) {
|
||||
throw new Error('No formula set. Call setFormula() first.')
|
||||
}
|
||||
|
||||
const numSamples = Math.floor(this.sampleRate * this.duration)
|
||||
const buffer = new Float32Array(numSamples)
|
||||
|
||||
for (let t = 0; t < numSamples; t++) {
|
||||
try {
|
||||
const value = this.compiledFormula(t)
|
||||
const byteValue = value & 0xFF
|
||||
buffer[t] = (byteValue - 128) / 128
|
||||
} catch (error) {
|
||||
buffer[t] = 0
|
||||
}
|
||||
}
|
||||
|
||||
this.audioBuffer = buffer
|
||||
return buffer
|
||||
}
|
||||
|
||||
setEffects(values: EffectValues): void {
|
||||
this.effectValues = values
|
||||
if (this.effectsChain) {
|
||||
this.effectsChain.updateEffects(values)
|
||||
}
|
||||
}
|
||||
|
||||
getPlaybackPosition(): number {
|
||||
if (!this.audioContext || !this.sourceNode || this.startTime === 0) {
|
||||
return 0
|
||||
}
|
||||
const elapsed = this.audioContext.currentTime - this.startTime
|
||||
return (elapsed % this.duration) / this.duration
|
||||
}
|
||||
|
||||
play(): void {
|
||||
if (!this.audioBuffer) {
|
||||
this.generate()
|
||||
}
|
||||
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext({ sampleRate: this.sampleRate })
|
||||
}
|
||||
|
||||
if (!this.effectsChain) {
|
||||
this.effectsChain = new EffectsChain(this.audioContext)
|
||||
this.effectsChain.updateEffects(this.effectValues)
|
||||
}
|
||||
|
||||
if (this.sourceNode) {
|
||||
this.sourceNode.stop()
|
||||
}
|
||||
|
||||
const audioBuffer = this.audioContext.createBuffer(1, this.audioBuffer!.length, this.sampleRate)
|
||||
audioBuffer.getChannelData(0).set(this.audioBuffer!)
|
||||
|
||||
this.sourceNode = this.audioContext.createBufferSource()
|
||||
this.sourceNode.buffer = audioBuffer
|
||||
this.sourceNode.loop = this.isLooping
|
||||
this.sourceNode.connect(this.effectsChain.getInputNode())
|
||||
this.effectsChain.getOutputNode().connect(this.audioContext.destination)
|
||||
|
||||
if (this.pauseTime > 0) {
|
||||
this.sourceNode.start(0, this.pauseTime)
|
||||
this.startTime = this.audioContext.currentTime - this.pauseTime
|
||||
this.pauseTime = 0
|
||||
} else {
|
||||
this.sourceNode.start(0)
|
||||
this.startTime = this.audioContext.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
if (this.sourceNode && this.audioContext) {
|
||||
this.pauseTime = this.audioContext.currentTime - this.startTime
|
||||
this.sourceNode.stop()
|
||||
this.sourceNode = null
|
||||
}
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.sourceNode) {
|
||||
this.sourceNode.stop()
|
||||
this.sourceNode = null
|
||||
}
|
||||
this.startTime = 0
|
||||
this.pauseTime = 0
|
||||
}
|
||||
|
||||
exportWAV(bitDepth: BitDepth = 8): Blob {
|
||||
if (!this.audioBuffer) {
|
||||
this.generate()
|
||||
}
|
||||
return encodeWAV(this.audioBuffer!, this.sampleRate, bitDepth)
|
||||
}
|
||||
|
||||
downloadWAV(filename: string = 'bytebeat.wav', bitDepth: BitDepth = 8): void {
|
||||
const blob = this.exportWAV(bitDepth)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop()
|
||||
if (this.effectsChain) {
|
||||
this.effectsChain.dispose()
|
||||
this.effectsChain = null
|
||||
}
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close()
|
||||
this.audioContext = null
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/lib/bytebeat/EffectsChain.ts
Normal file
111
src/lib/bytebeat/EffectsChain.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import type { EffectValues } from '../../types/effects'
|
||||
|
||||
export class EffectsChain {
|
||||
private audioContext: AudioContext
|
||||
private inputNode: GainNode
|
||||
private outputNode: GainNode
|
||||
|
||||
private delayNode: DelayNode
|
||||
private delayFeedbackNode: GainNode
|
||||
private delayWetNode: GainNode
|
||||
private delayDryNode: GainNode
|
||||
|
||||
private convolverNode: ConvolverNode
|
||||
private reverbWetNode: GainNode
|
||||
private reverbDryNode: GainNode
|
||||
|
||||
private tbdNode: GainNode
|
||||
|
||||
constructor(audioContext: AudioContext) {
|
||||
this.audioContext = audioContext
|
||||
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
|
||||
this.delayNode = audioContext.createDelay(2.0)
|
||||
this.delayFeedbackNode = audioContext.createGain()
|
||||
this.delayWetNode = audioContext.createGain()
|
||||
this.delayDryNode = audioContext.createGain()
|
||||
|
||||
this.convolverNode = audioContext.createConvolver()
|
||||
this.reverbWetNode = audioContext.createGain()
|
||||
this.reverbDryNode = audioContext.createGain()
|
||||
|
||||
this.tbdNode = audioContext.createGain()
|
||||
|
||||
this.setupChain()
|
||||
this.generateImpulseResponse()
|
||||
}
|
||||
|
||||
private setupChain(): void {
|
||||
this.delayDryNode.gain.value = 1
|
||||
this.delayWetNode.gain.value = 0
|
||||
|
||||
this.inputNode.connect(this.delayDryNode)
|
||||
this.inputNode.connect(this.delayNode)
|
||||
this.delayNode.connect(this.delayFeedbackNode)
|
||||
this.delayFeedbackNode.connect(this.delayNode)
|
||||
this.delayNode.connect(this.delayWetNode)
|
||||
|
||||
this.delayDryNode.connect(this.reverbDryNode)
|
||||
this.delayWetNode.connect(this.reverbDryNode)
|
||||
|
||||
this.delayDryNode.connect(this.convolverNode)
|
||||
this.delayWetNode.connect(this.convolverNode)
|
||||
this.convolverNode.connect(this.reverbWetNode)
|
||||
|
||||
this.reverbDryNode.connect(this.tbdNode)
|
||||
this.reverbWetNode.connect(this.tbdNode)
|
||||
|
||||
this.tbdNode.connect(this.outputNode)
|
||||
}
|
||||
|
||||
private generateImpulseResponse(): void {
|
||||
const length = this.audioContext.sampleRate * 2
|
||||
const impulse = this.audioContext.createBuffer(2, length, this.audioContext.sampleRate)
|
||||
const left = impulse.getChannelData(0)
|
||||
const right = impulse.getChannelData(1)
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const n = length - i
|
||||
left[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2)
|
||||
right[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2)
|
||||
}
|
||||
|
||||
this.convolverNode.buffer = impulse
|
||||
}
|
||||
|
||||
updateEffects(values: EffectValues): void {
|
||||
const reverbWet = values.reverbWetDry / 100
|
||||
this.reverbWetNode.gain.value = reverbWet
|
||||
this.reverbDryNode.gain.value = 1 - reverbWet
|
||||
|
||||
this.delayNode.delayTime.value = values.delayTime / 1000
|
||||
this.delayFeedbackNode.gain.value = values.delayFeedback / 100
|
||||
|
||||
const delayAmount = Math.min(values.delayTime / 1000, 0.5)
|
||||
this.delayWetNode.gain.value = delayAmount
|
||||
this.delayDryNode.gain.value = 1 - delayAmount
|
||||
}
|
||||
|
||||
getInputNode(): AudioNode {
|
||||
return this.inputNode
|
||||
}
|
||||
|
||||
getOutputNode(): AudioNode {
|
||||
return this.outputNode
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.inputNode.disconnect()
|
||||
this.outputNode.disconnect()
|
||||
this.delayNode.disconnect()
|
||||
this.delayFeedbackNode.disconnect()
|
||||
this.delayWetNode.disconnect()
|
||||
this.delayDryNode.disconnect()
|
||||
this.convolverNode.disconnect()
|
||||
this.reverbWetNode.disconnect()
|
||||
this.reverbDryNode.disconnect()
|
||||
this.tbdNode.disconnect()
|
||||
}
|
||||
}
|
||||
15
src/lib/bytebeat/index.ts
Normal file
15
src/lib/bytebeat/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export { BytebeatGenerator } from './BytebeatGenerator'
|
||||
export type { BytebeatOptions, BytebeatState, BitDepth } from './types'
|
||||
|
||||
export const EXAMPLE_FORMULAS = {
|
||||
classic: 't * ((t>>12)|(t>>8))&(63&(t>>4))',
|
||||
melody: 't>>6^t&0x25|t+(t^t>>11)',
|
||||
simple: 't & (t>>4)|(t>>8)',
|
||||
harmony: '(t>>10&42)*t',
|
||||
glitch: 't*(t>>8*((t>>15)|(t>>8))&(20|(t>>19)*5>>t|(t>>3)))',
|
||||
drums: '((t>>10)&42)*(t>>8)',
|
||||
ambient: '(t*5&t>>7)|(t*3&t>>10)',
|
||||
noise: 't>>6&1?t>>5:-t>>4',
|
||||
arpeggio: 't*(((t>>9)|(t>>13))&25&t>>6)',
|
||||
chaos: 't*(t^t+(t>>15|1))',
|
||||
} as const
|
||||
12
src/lib/bytebeat/types.ts
Normal file
12
src/lib/bytebeat/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface BytebeatOptions {
|
||||
sampleRate?: number
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export interface BytebeatState {
|
||||
isPlaying: boolean
|
||||
isPaused: boolean
|
||||
currentTime: number
|
||||
}
|
||||
|
||||
export type BitDepth = 8 | 16
|
||||
49
src/lib/bytebeat/wavEncoder.ts
Normal file
49
src/lib/bytebeat/wavEncoder.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import type { BitDepth } from './types'
|
||||
|
||||
function writeString(view: DataView, offset: number, str: string): void {
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
view.setUint8(offset + i, str.charCodeAt(i))
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeWAV(samples: Float32Array, sampleRate: number, bitDepth: BitDepth): Blob {
|
||||
const numChannels = 1
|
||||
const bytesPerSample = bitDepth / 8
|
||||
const blockAlign = numChannels * bytesPerSample
|
||||
const dataSize = samples.length * bytesPerSample
|
||||
const buffer = new ArrayBuffer(44 + dataSize)
|
||||
const view = new DataView(buffer)
|
||||
|
||||
writeString(view, 0, 'RIFF')
|
||||
view.setUint32(4, 36 + dataSize, true)
|
||||
writeString(view, 8, 'WAVE')
|
||||
|
||||
writeString(view, 12, 'fmt ')
|
||||
view.setUint32(16, 16, true)
|
||||
view.setUint16(20, 1, true)
|
||||
view.setUint16(22, numChannels, true)
|
||||
view.setUint32(24, sampleRate, true)
|
||||
view.setUint32(28, sampleRate * blockAlign, true)
|
||||
view.setUint16(32, blockAlign, true)
|
||||
view.setUint16(34, bitDepth, true)
|
||||
|
||||
writeString(view, 36, 'data')
|
||||
view.setUint32(40, dataSize, true)
|
||||
|
||||
let offset = 44
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const sample = Math.max(-1, Math.min(1, samples[i]))
|
||||
|
||||
if (bitDepth === 8) {
|
||||
const value = Math.floor((sample + 1) * 127.5)
|
||||
view.setUint8(offset, value)
|
||||
offset += 1
|
||||
} else {
|
||||
const value = Math.floor(sample * 32767)
|
||||
view.setInt16(offset, value, true)
|
||||
offset += 2
|
||||
}
|
||||
}
|
||||
|
||||
return new Blob([buffer], { type: 'audio/wav' })
|
||||
}
|
||||
Reference in New Issue
Block a user