Initial CoolSoup implementation
CoolSoup is a React + TypeScript + Vite application that generates visual patterns and converts them to audio through spectral synthesis. Features multiple image generators (Tixy expressions, geometric tiles, external APIs) and an advanced audio synthesis engine that treats images as spectrograms.
This commit is contained in:
@ -54,25 +54,131 @@ export function downloadWAV(audioData: Float32Array, sampleRate: number, filenam
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Play audio in browser
|
||||
*/
|
||||
export async function playAudio(audioData: Float32Array, sampleRate: number): Promise<void> {
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||
export interface AudioPlayer {
|
||||
play(): void
|
||||
pause(): void
|
||||
stop(): void
|
||||
setVolume(volume: number): void
|
||||
isPlaying(): boolean
|
||||
onStateChange(callback: (isPlaying: boolean) => void): void
|
||||
}
|
||||
|
||||
if (audioContext.sampleRate !== sampleRate) {
|
||||
console.warn(`Audio context sample rate (${audioContext.sampleRate}) differs from data sample rate (${sampleRate})`)
|
||||
/**
|
||||
* Create an audio player with playback controls
|
||||
*/
|
||||
export function createAudioPlayer(audioData: Float32Array, sampleRate: number): AudioPlayer {
|
||||
let audioContext: AudioContext | null = null
|
||||
let source: AudioBufferSourceNode | null = null
|
||||
let gainNode: GainNode | null = null
|
||||
let isCurrentlyPlaying = false
|
||||
let isPaused = false
|
||||
let pausedAt = 0
|
||||
let startedAt = 0
|
||||
let stateCallback: ((isPlaying: boolean) => void) | null = null
|
||||
|
||||
const initAudioContext = () => {
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||
gainNode = audioContext.createGain()
|
||||
gainNode.connect(audioContext.destination)
|
||||
|
||||
if (audioContext.sampleRate !== sampleRate) {
|
||||
console.warn(`Audio context sample rate (${audioContext.sampleRate}) differs from data sample rate (${sampleRate})`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = audioContext.createBuffer(1, audioData.length, sampleRate)
|
||||
buffer.copyToChannel(audioData, 0)
|
||||
const updateState = (playing: boolean) => {
|
||||
isCurrentlyPlaying = playing
|
||||
if (stateCallback) {
|
||||
stateCallback(playing)
|
||||
}
|
||||
}
|
||||
|
||||
const source = audioContext.createBufferSource()
|
||||
source.buffer = buffer
|
||||
source.connect(audioContext.destination)
|
||||
source.start()
|
||||
return {
|
||||
play() {
|
||||
initAudioContext()
|
||||
if (!audioContext || !gainNode) return
|
||||
|
||||
if (isPaused) {
|
||||
// Resume from pause is not supported with AudioBufferSource
|
||||
// We need to restart from the beginning
|
||||
isPaused = false
|
||||
pausedAt = 0
|
||||
}
|
||||
|
||||
if (source) {
|
||||
source.stop()
|
||||
}
|
||||
|
||||
const buffer = audioContext.createBuffer(1, audioData.length, sampleRate)
|
||||
buffer.copyToChannel(audioData, 0)
|
||||
|
||||
source = audioContext.createBufferSource()
|
||||
source.buffer = buffer
|
||||
source.connect(gainNode)
|
||||
|
||||
source.onended = () => {
|
||||
updateState(false)
|
||||
isPaused = false
|
||||
pausedAt = 0
|
||||
startedAt = 0
|
||||
}
|
||||
|
||||
source.start()
|
||||
startedAt = audioContext.currentTime
|
||||
updateState(true)
|
||||
},
|
||||
|
||||
pause() {
|
||||
if (source && isCurrentlyPlaying) {
|
||||
source.stop()
|
||||
source = null
|
||||
isPaused = true
|
||||
pausedAt = audioContext ? audioContext.currentTime - startedAt : 0
|
||||
updateState(false)
|
||||
}
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (source) {
|
||||
source.stop()
|
||||
source = null
|
||||
}
|
||||
isPaused = false
|
||||
pausedAt = 0
|
||||
startedAt = 0
|
||||
updateState(false)
|
||||
},
|
||||
|
||||
setVolume(volume: number) {
|
||||
if (gainNode) {
|
||||
gainNode.gain.value = Math.max(0, Math.min(1, volume))
|
||||
}
|
||||
},
|
||||
|
||||
isPlaying() {
|
||||
return isCurrentlyPlaying
|
||||
},
|
||||
|
||||
onStateChange(callback: (isPlaying: boolean) => void) {
|
||||
stateCallback = callback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play audio in browser (legacy function for backward compatibility)
|
||||
*/
|
||||
export async function playAudio(audioData: Float32Array, sampleRate: number): Promise<void> {
|
||||
const player = createAudioPlayer(audioData, sampleRate)
|
||||
|
||||
return new Promise(resolve => {
|
||||
source.onended = () => resolve()
|
||||
player.onStateChange((isPlaying) => {
|
||||
if (!isPlaying) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
player.play()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user