mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
CLOSES #1471
This commit is contained in:
+47
-3
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user