Better code quality

This commit is contained in:
2025-10-04 14:52:20 +02:00
parent c6cc1a47c0
commit ba37b94908
25 changed files with 904 additions and 588 deletions

View File

@ -1,4 +1,5 @@
import { EffectsChain } from './effects/EffectsChain'
import { BytebeatSourceEffect } from './effects/BytebeatSourceEffect'
import type { EffectValues } from '../../types/effects'
export interface AudioPlayerOptions {
@ -8,15 +9,12 @@ export interface AudioPlayerOptions {
export class AudioPlayer {
private audioContext: AudioContext | null = null
private sourceNode: AudioBufferSourceNode | null = null
private bytebeatSource: BytebeatSourceEffect | null = null
private effectsChain: EffectsChain | null = null
private effectValues: EffectValues = {}
private startTime: number = 0
private pauseTime: number = 0
private isLooping: boolean = true
private sampleRate: number
private duration: number
private pitch: number = 1
private workletRegistered: boolean = false
constructor(options: AudioPlayerOptions) {
@ -40,7 +38,7 @@ export class AudioPlayer {
}
private async recreateAudioContext(): Promise<void> {
const wasPlaying = this.sourceNode !== null
const wasPlaying = this.bytebeatSource !== null
this.dispose()
@ -61,7 +59,10 @@ export class AudioPlayer {
if (this.workletRegistered) return
try {
await context.audioWorklet.addModule('/worklets/fold-crush-processor.js')
await Promise.all([
context.audioWorklet.addModule('/worklets/fold-crush-processor.js'),
context.audioWorklet.addModule('/worklets/bytebeat-processor.js')
])
this.workletRegistered = true
} catch (error) {
console.error('Failed to register AudioWorklet:', error)
@ -75,18 +76,7 @@ export class AudioPlayer {
}
}
setPitch(pitch: number): void {
this.pitch = pitch
if (this.sourceNode && this.audioContext) {
this.sourceNode.playbackRate.setTargetAtTime(
pitch,
this.audioContext.currentTime,
0.015
)
}
}
async play(buffer: Float32Array, onEnded?: () => void): Promise<void> {
private async ensureAudioContext(): Promise<void> {
if (!this.audioContext) {
this.audioContext = new AudioContext({ sampleRate: this.sampleRate })
await this.registerWorklet(this.audioContext)
@ -97,78 +87,54 @@ export class AudioPlayer {
await this.effectsChain.initialize(this.audioContext)
this.effectsChain.updateEffects(this.effectValues)
}
if (this.sourceNode) {
this.sourceNode.stop()
}
const audioBuffer = this.audioContext.createBuffer(1, buffer.length, this.sampleRate)
audioBuffer.getChannelData(0).set(buffer)
this.sourceNode = this.audioContext.createBufferSource()
this.sourceNode.buffer = audioBuffer
this.sourceNode.loop = this.isLooping
this.sourceNode.playbackRate.value = this.pitch
if (onEnded) {
this.sourceNode.onended = onEnded
}
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
}
}
setLooping(loop: boolean): void {
this.isLooping = loop
if (this.sourceNode) {
this.sourceNode.loop = loop
async playRealtime(formula: string, a: number, b: number, c: number, d: number): Promise<void> {
await this.ensureAudioContext()
if (!this.bytebeatSource) {
this.bytebeatSource = new BytebeatSourceEffect(this.audioContext!)
await this.bytebeatSource.initialize(this.audioContext!)
}
this.bytebeatSource.setLoopLength(this.sampleRate, this.duration)
this.bytebeatSource.setFormula(formula)
this.bytebeatSource.setVariables(a, b, c, d)
this.bytebeatSource.reset()
this.bytebeatSource.getOutputNode().connect(this.effectsChain!.getInputNode())
this.effectsChain!.getOutputNode().connect(this.audioContext!.destination)
this.startTime = this.audioContext!.currentTime
}
scheduleNextTrack(callback: () => void): void {
if (this.sourceNode) {
this.sourceNode.loop = false
this.sourceNode.onended = callback
updateRealtimeVariables(a: number, b: number, c: number, d: number): void {
if (this.bytebeatSource) {
this.bytebeatSource.setVariables(a, b, c, d)
}
}
getPlaybackPosition(): number {
if (!this.audioContext || !this.sourceNode || this.startTime === 0) {
if (!this.audioContext || this.startTime === 0) {
return 0
}
const elapsed = this.audioContext.currentTime - this.startTime
const actualDuration = this.duration / this.pitch
return (elapsed % actualDuration) / actualDuration
}
pause(): void {
if (this.sourceNode && this.audioContext) {
this.pauseTime = this.audioContext.currentTime - this.startTime
this.sourceNode.stop()
this.sourceNode = null
}
return (elapsed % this.duration) / this.duration
}
stop(): void {
if (this.sourceNode) {
this.sourceNode.stop()
this.sourceNode = null
if (this.bytebeatSource) {
this.bytebeatSource.getOutputNode().disconnect()
}
this.startTime = 0
this.pauseTime = 0
}
dispose(): void {
this.stop()
if (this.bytebeatSource) {
this.bytebeatSource.dispose()
this.bytebeatSource = null
}
if (this.effectsChain) {
this.effectsChain.dispose()
this.effectsChain = null
@ -178,5 +144,6 @@ export class AudioPlayer {
this.audioContext = null
}
this.workletRegistered = false
this.startTime = 0
}
}

View File

@ -1,4 +1,4 @@
export type CompiledFormula = (t: number) => number
export type CompiledFormula = (t: number, a: number, b: number, c: number, d: number) => number
export interface CompilationResult {
success: boolean
@ -8,7 +8,7 @@ export interface CompilationResult {
export function compileFormula(formula: string): CompilationResult {
try {
const compiledFormula = new Function('t', `return ${formula}`) as CompiledFormula
const compiledFormula = new Function('t', 'a', 'b', 'c', 'd', `return ${formula}`) as CompiledFormula
return {
success: true,
compiledFormula
@ -28,7 +28,7 @@ export function testFormula(formula: string): boolean {
}
try {
result.compiledFormula(0)
result.compiledFormula(0, 8, 16, 32, 64)
return true
} catch {
return false

View File

@ -3,19 +3,23 @@ import type { CompiledFormula } from './BytebeatCompiler'
export interface GeneratorOptions {
sampleRate: number
duration: number
a?: number
b?: number
c?: number
d?: number
}
export function generateSamples(
compiledFormula: CompiledFormula,
options: GeneratorOptions
): Float32Array {
const { sampleRate, duration } = options
const { sampleRate, duration, a = 8, b = 16, c = 32, d = 64 } = options
const numSamples = Math.floor(sampleRate * duration)
const buffer = new Float32Array(numSamples)
for (let t = 0; t < numSamples; t++) {
try {
const value = compiledFormula(t)
const value = compiledFormula(t, a, b, c, d)
const byteValue = value & 0xFF
buffer[t] = (byteValue - 128) / 128
} catch (error) {
@ -31,7 +35,7 @@ export function generateSamplesWithBitDepth(
options: GeneratorOptions,
bitDepth: 8 | 16 | 24
): Float32Array {
const { sampleRate, duration } = options
const { sampleRate, duration, a = 8, b = 16, c = 32, d = 64 } = options
const numSamples = Math.floor(sampleRate * duration)
const buffer = new Float32Array(numSamples)
@ -40,7 +44,7 @@ export function generateSamplesWithBitDepth(
for (let t = 0; t < numSamples; t++) {
try {
const value = compiledFormula(t)
const value = compiledFormula(t, a, b, c, d)
const clampedValue = value & maxValue
buffer[t] = (clampedValue - midPoint) / midPoint
} catch (error) {

View File

@ -1,13 +1,70 @@
import { encodeWAV } from '../../lib/bytebeat/wavEncoder'
import type { BitDepth } from '../../lib/bytebeat/types'
export type { BitDepth }
export type BitDepth = 8 | 16 | 24
export interface ExportOptions {
sampleRate: number
bitDepth?: BitDepth
}
function encodeWAV(samples: Float32Array, sampleRate: number, bitDepth: BitDepth): Blob {
const numChannels = 1
const bytesPerSample = bitDepth / 8
const blockAlign = numChannels * bytesPerSample
const byteRate = sampleRate * blockAlign
const dataSize = samples.length * bytesPerSample
const bufferSize = 44 + dataSize
const buffer = new ArrayBuffer(bufferSize)
const view = new DataView(buffer)
const writeString = (offset: number, string: string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
writeString(0, 'RIFF')
view.setUint32(4, 36 + dataSize, true)
writeString(8, 'WAVE')
writeString(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, byteRate, true)
view.setUint16(32, blockAlign, true)
view.setUint16(34, bitDepth, true)
writeString(36, 'data')
view.setUint32(40, dataSize, true)
const maxValue = Math.pow(2, bitDepth - 1) - 1
let offset = 44
for (let i = 0; i < samples.length; i++) {
const sample = Math.max(-1, Math.min(1, samples[i]))
const intSample = Math.round(sample * maxValue)
if (bitDepth === 8) {
view.setUint8(offset, intSample + 128)
offset += 1
} else if (bitDepth === 16) {
view.setInt16(offset, intSample, true)
offset += 2
} else if (bitDepth === 24) {
const bytes = [
intSample & 0xff,
(intSample >> 8) & 0xff,
(intSample >> 16) & 0xff
]
view.setUint8(offset, bytes[0])
view.setUint8(offset + 1, bytes[1])
view.setUint8(offset + 2, bytes[2])
offset += 3
}
}
return new Blob([buffer], { type: 'audio/wav' })
}
export function exportToWav(
samples: Float32Array,
options: ExportOptions

View File

@ -0,0 +1,68 @@
import type { Effect } from './Effect.interface'
export class BytebeatSourceEffect implements Effect {
readonly id = 'bytebeat-source'
private inputNode: GainNode
private outputNode: GainNode
private processorNode: AudioWorkletNode | null = null
constructor(audioContext: AudioContext) {
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
}
async initialize(audioContext: AudioContext): Promise<void> {
try {
this.processorNode = new AudioWorkletNode(audioContext, 'bytebeat-processor')
this.processorNode.connect(this.outputNode)
} catch (error) {
console.error('Failed to initialize BytebeatSourceEffect worklet:', error)
}
}
getInputNode(): AudioNode {
return this.inputNode
}
getOutputNode(): AudioNode {
return this.outputNode
}
setBypass(_bypass: boolean): void {
// Source node doesn't support bypass
}
updateParams(_values: Record<string, number | string>): void {
// Parameters handled via specific methods
}
setFormula(formula: string): void {
if (!this.processorNode) return
this.processorNode.port.postMessage({ type: 'formula', value: formula })
}
setVariables(a: number, b: number, c: number, d: number): void {
if (!this.processorNode) return
this.processorNode.port.postMessage({ type: 'variables', value: { a, b, c, d } })
}
setLoopLength(sampleRate: number, duration: number): void {
if (!this.processorNode) return
const loopLength = sampleRate * duration
this.processorNode.port.postMessage({ type: 'loopLength', value: loopLength })
}
reset(): void {
if (!this.processorNode) return
this.processorNode.port.postMessage({ type: 'reset' })
}
dispose(): void {
if (this.processorNode) {
this.processorNode.disconnect()
}
this.inputNode.disconnect()
this.outputNode.disconnect()
}
}