Files
gen1recomp/tests/engine/chip_audio_preroll_test.lua
T

220 lines
6.5 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
love = require("tests.love_stub")
local sources = {}
local Source = {}
Source.__index = Source
function Source:play() self.playing = true end
function Source:stop() self.playing = false end
function Source:pause() self.playing = false end
function Source:isPlaying() return self.playing end
function Source:setLooping(v) self.looping = v end
function Source:setVolume(v) self.volume = v end
function Source:setPitch(v) self.pitch = v end
function Source:setFilter() end
function Source:getDuration() return 1 end
function Source:getFreeBufferCount() return self.free end
function Source:queue() self.free = math.max(0, self.free - 1) end
local ChipSynth = require("src.core.ChipSynth")
local BUFFERS = ChipSynth.MUSIC_BUFFER_COUNT
function Source:drain(n)
self.free = math.min(BUFFERS, self.free + n)
end
local function track(src)
sources[#sources + 1] = src
return src
end
love.audio = {
newSource = function(what, mode)
return track(setmetatable({ file = what, mode = mode, free = 0 }, Source))
end,
newQueueableSource = function()
return track(setmetatable({ queueable = true, free = BUFFERS }, Source))
end,
}
local channels = {}
local lastGen, lastEpoch = 0, 0
local Channel = {}
Channel.__index = Channel
function Channel:push(msg)
if type(msg) == "table" and msg.cmd == "play" then
lastGen, lastEpoch = msg.gen, msg.stereoEpoch or 0
end
self.queue[#self.queue + 1] = msg
end
function Channel:pop() return table.remove(self.queue, 1) end
function Channel:clear() self.queue = {} end
function Channel:getCount() return #self.queue end
local function channel(name)
channels[name] = channels[name] or setmetatable({ queue = {} }, Channel)
return channels[name]
end
love.thread = {
newThread = function()
return {
start = function() end,
getError = function() return nil end,
wait = function() end,
}
end,
getChannel = channel,
}
local ChipAudio = require("src.core.ChipAudio")
local Logger = require("src.core.Logger")
local PREROLL = ChipAudio.MUSIC_PREROLL
check(type(PREROLL) == "number" and PREROLL > 1,
"ChipAudio publishes a pre-roll floor deeper than one buffer")
local function deliver(n)
for _ = 1, (n or 1) do
channel("chipaudio_out"):push({
gen = lastGen, sd = true, stereoEpoch = lastEpoch })
end
end
local function deliverDone()
channel("chipaudio_out"):push({ gen = lastGen, done = true })
end
local ChipAsm = require("src.audio.ChipAsm")
local song = ChipAsm.song{
channels = { { hw = 1, program = {
{ notetype = { speed = 12, volume = 12, fade = 0 } },
{ octave = 4 },
{ note = "C", len = 8 },
{ loop = { count = 0, to = 1 } },
} } },
}
local data = { audio = { songs = { Music_PalletTown = song } } }
local function lastSource() return sources[#sources] end
local function newSong(allowLoops)
for i = #sources, 1, -1 do sources[i] = nil end
channel("chipaudio_out"):clear()
check(ChipAudio.playMusic(data, song, allowLoops) ~= nil,
"threaded playMusic built a queueable source")
return lastSource()
end
local src = newSong(true)
check(not src.playing, "a fresh song is silent before any buffer lands")
for queued = 1, PREROLL - 1 do
deliver(1)
ChipAudio.update()
eq(BUFFERS - src.free, queued,
("update queued buffer %d"):format(queued))
check(not src.playing,
("a looping song is still silent on %d queued buffer(s)"):format(queued))
check(ChipAudio.awaitingFirstBuffer(),
"the song still reads as awaiting its first buffer")
end
deliver(1)
ChipAudio.update()
eq(BUFFERS - src.free, PREROLL, "the pre-roll is fully queued")
check(src.playing, "the looping song starts once the pre-roll is queued")
check(not ChipAudio.awaitingFirstBuffer(), "and it is no longer awaiting")
local jingle = newSong(false)
check(ChipAudio.awaitingFirstBuffer(),
"the jingle awaits its first buffer like it always did")
deliver(1)
ChipAudio.update()
check(jingle.playing, "a playOnce jingle still starts on its first buffer")
check(not ChipAudio.awaitingFirstBuffer(),
"and awaitingFirstBuffer clears on that same buffer")
local short = newSong(true)
deliver(1)
deliverDone()
ChipAudio.update()
check(short.playing,
"a song the worker finished short of the pre-roll starts on what it has")
local recover = newSong(true)
deliver(PREROLL)
ChipAudio.update()
check(recover.playing, "the song is sounding before the stall")
recover:drain(PREROLL)
recover.playing = false
eq(BUFFERS - recover.free, 0, "the stall drained the queue dry")
ChipAudio.ensureMusicPlaying()
check(not recover.playing, "an empty queue is not restarted")
deliver(1)
ChipAudio.update()
ChipAudio.ensureMusicPlaying()
check(not recover.playing,
"recovery does not restart on one buffer straight back into an underrun")
deliver(PREROLL - 1)
ChipAudio.update()
ChipAudio.ensureMusicPlaying()
check(recover.playing, "recovery restarts once the queue is pre-rolled again")
check(type(ChipAudio._setAudioStatsForTest) == "function",
"ChipAudio exposes the audio stats counters")
ChipAudio._setAudioStatsForTest(true)
local stats = newSong(true)
local logBase = #Logger.history
deliver(PREROLL)
for _ = 1, 59 do ChipAudio.update() end
eq(#Logger.history, logBase, "the stats line does not fire before a full second")
ChipAudio.update()
local line = Logger.history[#Logger.history]
check(line ~= nil and line:find("chipaudio: depth=", 1, true) ~= nil,
"one stats line per 60 updates: " .. tostring(line))
check(line:find(("depth=%d/%d"):format(PREROLL, BUFFERS), 1, true) ~= nil,
"the line reports the live queue depth")
check(line:find("rate=" .. ChipSynth.SAMPLE_RATE, 1, true) ~= nil,
"and the sample rate the source was built at")
stats:drain(PREROLL)
stats.playing = false
for _ = 1, 60 do ChipAudio.update() end
local counters = ChipAudio._audioStatsForTest()
eq(counters.underruns, 60, "a dry queue is counted as an underrun every frame")
eq(counters.restarts, 0, "nothing restarted while the queue was dry")
deliver(PREROLL)
ChipAudio.update()
ChipAudio.ensureMusicPlaying()
eq(ChipAudio._audioStatsForTest().restarts, 1,
"the restart the reporter hears as a click is counted")
local logAfter = #Logger.history
ChipAudio._setAudioStatsForTest(false)
for _ = 1, 120 do ChipAudio.update() end
eq(ChipAudio._audioStatsForTest().frames, 120,
"the counters keep running with the per-second log gated off")
eq(#Logger.history, logAfter,
"and an ungated session logs nothing")
T.finish("chip audio pre-roll")