This commit is contained in:
bryanthaboi
2026-08-18 14:19:42 -04:00
parent abf95ce1c4
commit 7583ba8729
9 changed files with 193 additions and 268 deletions
+47 -3
View File
@@ -183,6 +183,9 @@ end
-- ---------------------------------------------------------------------------
local musicGen = 0
-- bumps when SOUND flips so already-queued PCM (old pan) is dropped rather
-- than playing out the ~6s stall-tolerance queue (#1471)
local stereoEpoch = 0
function ChipAudio.playMusic(data, header, allowLoops)
if not ensureWorker() then
@@ -204,9 +207,11 @@ function ChipAudio.playMusic(data, header, allowLoops)
allowLoops = allowLoops, audio = slimAudio(data),
channelVolumes = ChipSynth.getChannelVolumes(),
channelPitches = ChipSynth.getChannelPitches(),
stereo = ChipSynth.getStereo() })
stereo = ChipSynth.getStereo(),
stereoEpoch = stereoEpoch })
currentMusic = { source = source, gen = gen, threaded = true,
started = false, finished = false }
started = false, finished = false,
stereoEpoch = stereoEpoch }
-- playback starts in update() once the first buffer arrives (~1 frame)
return source
end
@@ -236,6 +241,9 @@ local function updateThreaded()
if not buf then break end
if buf.gen ~= m.gen then
-- stale buffer from a superseded song: drop it
elseif buf.stereoEpoch ~= nil and m.stereoEpoch ~= nil
and buf.stereoEpoch ~= m.stereoEpoch then
-- stale pan mix from before a live SOUND toggle (#1471)
elseif buf.done then
m.finished = true
elseif buf.error then
@@ -354,9 +362,45 @@ function ChipAudio.shutdown()
workerReady = false
end
function ChipAudio.currentSource()
return currentMusic and currentMusic.source
end
function ChipAudio.setStereo(enabled)
enabled = not not enabled
if ChipSynth.getStereo() == enabled then return end
ChipSynth.setStereo(enabled)
pushChannelMix()
stereoEpoch = stereoEpoch + 1
local m = currentMusic
if m and m.engine then
ChipSynth.applyStereo(m.engine)
end
if workerReady and cmdCh then
cmdCh:push({ cmd = "channelMix",
volumes = ChipSynth.getChannelVolumes(),
pitches = ChipSynth.getChannelPitches(),
stereo = enabled,
stereoEpoch = stereoEpoch })
end
if not m then return end
pendingBuf = nil
if outCh then outCh:clear() end
m.stereoEpoch = stereoEpoch
-- QueueableSource cannot unqueue; swap so the ~6s stall-tolerance buffers
-- (mixed under the previous pan) do not have to play out first (#1471)
if not love.audio then return end
local ok, source = pcall(
love.audio.newQueueableSource, SAMPLE_RATE, 16, 2, MUSIC_BUFFER_COUNT)
if not ok or not source then return end
local old = m.source
m.source = source
m.started = false
if old then pcall(old.stop, old) end
if not m.threaded then
fillSync(MUSIC_FILL_INITIAL)
if not musicHeld then pcall(source.play, source) end
m.started = true
end
end
function ChipAudio.getStereo()
+49 -4
View File
@@ -294,6 +294,10 @@ function Channel.new(engine, spec, options)
noiseSampling = false, -- Gen 2 toggle_noise
condition = 0, -- Gen 2 set_condition / sound_jump_if
tracks = tracks, -- Gen 2 CHANNEL_TRACKS (NR51 bits for this channel)
-- last Music_StereoPanning byte; remembered even while MONO so a live
-- SOUND toggle can re-apply it without restarting the song (#1471)
stereoPanning = nil,
forcePanning = false, -- ForceStereoPanning ($e4) ignores the SOUND option
waveInstrument = 0,
waveLevel = 1,
perfectPitch = false,
@@ -406,6 +410,20 @@ function Channel:pan()
bit.band(self.engine.pan, mask) ~= 0
end
-- Recompute CHANNEL_TRACKS from the remembered panning byte and the live
-- STEREO flag. ForceStereoPanning stays put either way.
function Channel:applyStereoMix()
local mask = bit.lshift(1, self.hardware - 1)
local default = bit.bor(bit.lshift(mask, 4), mask)
if self.forcePanning then
return
elseif stereoEnabled and self.stereoPanning then
self.tracks = bit.band(self.stereoPanning, default)
else
self.tracks = default
end
end
function Channel:tone(ticks, register, volume, fade)
if register >= 0x800 then
return self:timedEvent({ silence = true }, ticks)
@@ -756,6 +774,7 @@ function Channel:nextEventGen2()
local mask = bit.lshift(1, self.hardware - 1)
local default = bit.bor(bit.lshift(mask, 4), mask)
self.tracks = bit.band(packed, default)
self.forcePanning = true
elseif command == 0xE5 then -- volume (global master; ignored for mix)
self:byte()
elseif command == 0xE6 then -- pitch_offset (big-endian)
@@ -778,8 +797,12 @@ function Channel:nextEventGen2()
elseif command == 0xEE then -- unknownmusic0xee
self:word()
elseif command == 0xEF then
-- audio/engine.asm:1987 Music_StereoPanning: apply only when STEREO is on
-- audio/engine.asm:1987 Music_StereoPanning: apply only when STEREO is on.
-- The packed byte is kept either way so ChipSynth.applyStereo can honour
-- a live SOUND toggle mid-song (#1471).
local packed = self:byte()
self.stereoPanning = packed
self.forcePanning = false
if stereoEnabled then
local mask = bit.lshift(1, self.hardware - 1)
local default = bit.bor(bit.lshift(mask, 4), mask)
@@ -1298,14 +1321,30 @@ function Engine:sampleStereo()
local left, right = 0, 0
for _, channel in ipairs(self.channels) do
local value = channel:sample()
local event = channel.event
if not event or event.panLeft ~= false then left = left + value end
if not event or event.panRight ~= false then right = right + value end
local panLeft, panRight
if self.generation == 2 then
-- live CHANNEL_TRACKS, not the pan baked into the current note, so a
-- SOUND toggle reaches the next synthesized sample (#1471)
panLeft, panRight = channel:pan()
else
local event = channel.event
panLeft = not event or event.panLeft ~= false
panRight = not event or event.panRight ~= false
end
if panLeft then left = left + value end
if panRight then right = right + value end
end
return analogOut(self, left, "hpfCapLeft", "lpfLeft"),
analogOut(self, right, "hpfCapRight", "lpfRight")
end
function Engine:applyStereo()
if self.generation ~= 2 then return end
for _, channel in ipairs(self.channels) do
channel:applyStereoMix()
end
end
function Engine:sampleChannel(number)
local selected = 0
for _, channel in ipairs(self.channels) do
@@ -1360,4 +1399,10 @@ ChipSynth.newEngine = Engine.new
ChipSynth.soundData = soundData
ChipSynth.renderEffectData = renderEffectData
function ChipSynth.applyStereo(engine)
if type(engine) == "table" and engine.applyStereo then
engine:applyStereo()
end
end
return ChipSynth
+12 -1
View File
@@ -507,7 +507,18 @@ function Music.applyOptions(opts)
Music.setVolumeLevel(opts and opts.musicVol or 7)
Music.setFilterLevel(opts and opts.musicFilter or 0)
-- engine/menus/options_menu.asm SOUND row (wOptions STEREO bit)
require("src.core.ChipAudio").setStereo(opts and opts.sound == "STEREO")
local ChipAudio = require("src.core.ChipAudio")
ChipAudio.setStereo(opts and opts.sound == "STEREO")
-- setStereo may swap the queueable source so the new pan is not sitting
-- behind already-mixed buffers; re-bind so volume/filter follow (#1471)
if state.chip then
local src = ChipAudio.currentSource()
if src then
state.source = src
applyVolume(src)
applyFilter(src)
end
end
end
local function sourceStopped(src)
+14 -5
View File
@@ -6,9 +6,10 @@
-- Protocol -- main thread pushes command tables onto the "chipaudio_cmd"
-- channel and drains produced buffers off "chipaudio_out":
-- cmd = "play" { gen, header, allowLoops, audio,
-- channelVolumes?, channelPitches? }
-- channelVolumes?, channelPitches?, stereo?, stereoEpoch? }
-- cmd = "stop" halt production
-- cmd = "channelMix" { volumes, pitches } per-hw volume/pitch
-- cmd = "channelMix" { volumes, pitches, stereo?, stereoEpoch? }
-- stereoEpoch present: live SOUND toggle; drop lookahead
-- cmd = "invalidate" drop the bank cache
-- cmd = "quit" end the thread
-- out buffers are tagged with the play's `gen` so the main thread can
@@ -39,6 +40,7 @@ local gen = nil -- active song generation, or nil when stopped
local engine = nil -- the ChipSynth engine producing the current song
local finished = false -- the current song ran out (non-looping)
local data = nil -- { audio = <slim audio tables> } for ROM bank/wave reads
local stereoEpoch = 0 -- matches ChipAudio; stale pan buffers are dropped
local function handle(cmd)
if cmd.cmd == "play" then
@@ -56,6 +58,7 @@ local function handle(cmd)
if cmd.stereo ~= nil then
ChipSynth.setStereo(cmd.stereo)
end
if cmd.stereoEpoch ~= nil then stereoEpoch = cmd.stereoEpoch end
local ok, eng = pcall(ChipSynth.newEngine, data, cmd.header,
{ allowLoops = cmd.allowLoops })
if ok then
@@ -73,6 +76,11 @@ local function handle(cmd)
if cmd.volumes ~= nil then ChipSynth.setChannelVolumes(cmd.volumes) end
if cmd.pitches ~= nil then ChipSynth.setChannelPitches(cmd.pitches) end
if cmd.stereo ~= nil then ChipSynth.setStereo(cmd.stereo) end
if engine and cmd.stereo ~= nil then ChipSynth.applyStereo(engine) end
if cmd.stereoEpoch ~= nil then
stereoEpoch = cmd.stereoEpoch
outCh:clear()
end
elseif cmd.cmd == "invalidate" then
ChipSynth.invalidateBanks()
elseif cmd.cmd == "quit" then
@@ -95,12 +103,13 @@ while true do
local activeGen = gen
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
if not ok then
outCh:push({ gen = activeGen, error = tostring(sd) })
outCh:push({ gen = activeGen, error = tostring(sd),
stereoEpoch = stereoEpoch })
finished = true
else
outCh:push({ gen = activeGen, sd = sd })
outCh:push({ gen = activeGen, sd = sd, stereoEpoch = stereoEpoch })
if engine:finished() then
outCh:push({ gen = activeGen, done = true })
outCh:push({ gen = activeGen, done = true, stereoEpoch = stereoEpoch })
finished = true
end
end
+5
View File
@@ -355,6 +355,11 @@ function OptionsMenu:cycle(row, delta)
if next_ < 1 then next_ = #row.values end
if next_ > #row.values then next_ = 1 end
self.options[row.key] = row.values[next_]
-- MUSIC VOL applies itself as it steps; SOUND has to as well, or the
-- pan sits on the current song until the next map change (#1471)
if row.key == "sound" then
require("src.core.Music").applyOptions(self.options)
end
end
function OptionsMenu:leave_()