modularity
This commit is contained in:
@ -1,25 +1,20 @@
|
||||
import type { BytebeatOptions, BitDepth } from './types'
|
||||
import { encodeWAV } from './wavEncoder'
|
||||
import { EffectsChain } from './EffectsChain'
|
||||
import type { EffectValues } from '../../types/effects'
|
||||
import { compileFormula } from '../../domain/audio/BytebeatCompiler'
|
||||
import { generateSamples } from '../../domain/audio/SampleGenerator'
|
||||
import { exportToWav } from '../../domain/audio/WavExporter'
|
||||
import { AudioPlayer } from '../../domain/audio/AudioPlayer'
|
||||
|
||||
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
|
||||
private audioPlayer: AudioPlayer
|
||||
|
||||
constructor(options: BytebeatOptions = {}) {
|
||||
this.sampleRate = options.sampleRate ?? 8000
|
||||
this.duration = options.duration ?? 10
|
||||
this.audioPlayer = new AudioPlayer({ sampleRate: this.sampleRate, duration: this.duration })
|
||||
}
|
||||
|
||||
updateOptions(options: Partial<BytebeatOptions>): void {
|
||||
@ -31,133 +26,71 @@ export class BytebeatGenerator {
|
||||
this.duration = options.duration
|
||||
this.audioBuffer = null
|
||||
}
|
||||
this.audioPlayer.updateOptions({ sampleRate: this.sampleRate, duration: this.duration })
|
||||
}
|
||||
|
||||
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'}`)
|
||||
const result = compileFormula(formula)
|
||||
|
||||
if (!result.success || !result.compiledFormula) {
|
||||
throw new Error(`Invalid formula: ${result.error}`)
|
||||
}
|
||||
|
||||
this.audioBuffer = generateSamples(result.compiledFormula, {
|
||||
sampleRate: this.sampleRate,
|
||||
duration: this.duration
|
||||
})
|
||||
}
|
||||
|
||||
generate(): Float32Array {
|
||||
if (!this.compiledFormula) {
|
||||
if (!this.audioBuffer) {
|
||||
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
|
||||
return this.audioBuffer
|
||||
}
|
||||
|
||||
setEffects(values: EffectValues): void {
|
||||
this.effectValues = values
|
||||
if (this.effectsChain) {
|
||||
this.effectsChain.updateEffects(values)
|
||||
}
|
||||
this.audioPlayer.setEffects(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
|
||||
return this.audioPlayer.getPlaybackPosition()
|
||||
}
|
||||
|
||||
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
|
||||
throw new Error('No audio buffer. Call setFormula() first.')
|
||||
}
|
||||
this.audioPlayer.play(this.audioBuffer)
|
||||
}
|
||||
|
||||
onLoopEnd(callback: () => void): void {
|
||||
if (this.sourceNode && !this.sourceNode.loop) {
|
||||
this.sourceNode.onended = callback
|
||||
}
|
||||
if (!this.audioBuffer) return
|
||||
this.audioPlayer.setLooping(false)
|
||||
this.audioPlayer.play(this.audioBuffer, callback)
|
||||
}
|
||||
|
||||
setLooping(loop: boolean): void {
|
||||
this.isLooping = loop
|
||||
this.audioPlayer.setLooping(loop)
|
||||
}
|
||||
|
||||
scheduleNextTrack(callback: () => void): void {
|
||||
if (this.audioContext && this.sourceNode) {
|
||||
this.sourceNode.loop = false
|
||||
this.sourceNode.onended = () => {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
this.audioPlayer.scheduleNextTrack(callback)
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
if (this.sourceNode && this.audioContext) {
|
||||
this.pauseTime = this.audioContext.currentTime - this.startTime
|
||||
this.sourceNode.stop()
|
||||
this.sourceNode = null
|
||||
}
|
||||
this.audioPlayer.pause()
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (this.sourceNode) {
|
||||
this.sourceNode.stop()
|
||||
this.sourceNode = null
|
||||
}
|
||||
this.startTime = 0
|
||||
this.pauseTime = 0
|
||||
this.audioPlayer.stop()
|
||||
}
|
||||
|
||||
exportWAV(bitDepth: BitDepth = 8): Blob {
|
||||
if (!this.audioBuffer) {
|
||||
this.generate()
|
||||
throw new Error('No audio buffer. Call setFormula() first.')
|
||||
}
|
||||
return encodeWAV(this.audioBuffer!, this.sampleRate, bitDepth)
|
||||
return exportToWav(this.audioBuffer, { sampleRate: this.sampleRate, bitDepth })
|
||||
}
|
||||
|
||||
downloadWAV(filename: string = 'bytebeat.wav', bitDepth: BitDepth = 8): void {
|
||||
@ -171,14 +104,6 @@ export class BytebeatGenerator {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop()
|
||||
if (this.effectsChain) {
|
||||
this.effectsChain.dispose()
|
||||
this.effectsChain = null
|
||||
}
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close()
|
||||
this.audioContext = null
|
||||
}
|
||||
this.audioPlayer.dispose()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user