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:
2025-09-29 14:44:48 +02:00
parent b564e41820
commit 623082ce3b
79 changed files with 6247 additions and 951 deletions

View File

@ -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()
})
}