Files
prosperon/sound.cm
2025-12-04 21:54:57 -06:00

127 lines
2.9 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 cant 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 oneshot; 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