Better code quality
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
68
src/domain/audio/effects/BytebeatSourceEffect.ts
Normal file
68
src/domain/audio/effects/BytebeatSourceEffect.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user