127 lines
2.9 KiB
Plaintext
127 lines
2.9 KiB
Plaintext
var tween = use('tween')
|
||
var io = use('cellfs')
|
||
var res = use('resources')
|
||
var wav = use('audio/wav')
|
||
var mp3 = use('audio/mp3')
|
||
var flac = use('audio/flac')
|
||
var dsp = use('audio/dsp')
|
||
|
||
var audio = {}
|
||
var pcms = {}
|
||
|
||
// keep every live voice here so GC can’t collect it prematurely
|
||
var voices = []
|
||
|
||
// load-and-cache WAVs/MP3s/FLACs
|
||
audio.pcm = function pcm(file) {
|
||
file = res.find_sound(file); if (!file) return
|
||
if (pcms[file]) return pcms[file]
|
||
|
||
var buf = io.slurp(file)
|
||
if (!buf) return null
|
||
|
||
var decoded = null
|
||
if (file.endsWith('.wav')) {
|
||
decoded = wav.decode(buf)
|
||
} else if (file.endsWith('.mp3')) {
|
||
decoded = mp3.decode(buf)
|
||
} else if (file.endsWith('.flac')) {
|
||
decoded = flac.decode(buf)
|
||
}
|
||
|
||
if (decoded && decoded.pcm) {
|
||
// Store extra info needed for playback
|
||
decoded.file = file
|
||
return pcms[file] = decoded
|
||
}
|
||
return null
|
||
}
|
||
|
||
function cleanup() {
|
||
// Remove voices that have finished playing
|
||
var active_voices = []
|
||
for (var i = 0; i < voices.length; i++) {
|
||
var v = voices[i]
|
||
// Check if voice has finished (pos >= total_frames)
|
||
// We can calculate total frames from pcm length and format
|
||
var total_samples = v.pcm.length / 4 // f32 is 4 bytes
|
||
var total_frames = total_samples / v.channels
|
||
|
||
if (v.pos < total_frames && !v.stopped) {
|
||
active_voices.push(v)
|
||
} else {
|
||
v.finish_hook?.()
|
||
}
|
||
}
|
||
voices = active_voices
|
||
}
|
||
|
||
// Voice class
|
||
function Voice(pcm_data) {
|
||
this.pcm = pcm_data.pcm
|
||
this.channels = pcm_data.channels || 1
|
||
this.sample_rate = pcm_data.sample_rate || 44100
|
||
this.pos = 0
|
||
this.vol = 1.0
|
||
this.stopped = false
|
||
}
|
||
|
||
Voice.prototype.stop = function() {
|
||
this.stopped = true
|
||
}
|
||
|
||
Voice.prototype.set_volume = function(v) {
|
||
this.vol = v
|
||
}
|
||
|
||
// play a one‑shot; returns the voice for volume/stop control
|
||
audio.play = function play(file) {
|
||
var pcm_data = audio.pcm(file); if (!pcm_data) return
|
||
var voice = new Voice(pcm_data)
|
||
voices.push(voice)
|
||
return voice
|
||
}
|
||
|
||
// cry is just a play+stop closure
|
||
audio.cry = function cry(file) {
|
||
var v = audio.play(file); if (!v) return
|
||
return function() { v.stop(); v = null }
|
||
}
|
||
|
||
return audio
|
||
|
||
//
|
||
// pump + periodic cleanup
|
||
//
|
||
var ss = use('sdl/audio')
|
||
var feeder = ss.open_stream("playback")
|
||
// We output stereo float32 at 44100Hz
|
||
feeder.set_format({format:"f32", channels:2, samplerate:44100})
|
||
feeder.resume()
|
||
|
||
var FRAMES = 1024
|
||
var CHANNELS = 2
|
||
var BYTES_PER_F = 4
|
||
var CHUNK_BYTES = FRAMES * CHANNELS * BYTES_PER_F
|
||
|
||
function pump() {
|
||
// Keep buffer full
|
||
if (feeder.queued() < CHUNK_BYTES*3) {
|
||
// Mix voices
|
||
var mixed_blob = dsp.mix(voices, FRAMES)
|
||
if (mixed_blob) {
|
||
feeder.put(mixed_blob)
|
||
} else {
|
||
// If mix failed or no voices, output silence?
|
||
// dsp.mix should return silence if no voices, but let's be safe
|
||
// Actually dsp.mix returns a zeroed buffer if no voices.
|
||
}
|
||
cleanup()
|
||
}
|
||
$_.delay(pump, 1/240)
|
||
}
|
||
|
||
pump()
|
||
|
||
return audio
|