mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 04:31:09 +02:00
CLOSES #1471
This commit is contained in:
@@ -1,118 +0,0 @@
|
|||||||
name: Feature request
|
|
||||||
description: Ask for something new in the engine, launcher, or platform — not a content/gameplay mod.
|
|
||||||
labels: ["enhancement"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Use this for **engine / launcher / platform** work (ports, video options, save
|
|
||||||
tooling, networking, mod API seams, docs).
|
|
||||||
|
|
||||||
If what you want is a gameplay, cosmetic, audio, or QoL change that a Lua mod
|
|
||||||
could ship — running shoes, alternate sprites, day/night, shiny indicators,
|
|
||||||
Gen 2-like battle toggles, soundtrack packs — open a
|
|
||||||
**[Mod request](https://github.com/bryanthaboi/gen1recomp/issues/new?template=mod_request.yml)**
|
|
||||||
instead.
|
|
||||||
|
|
||||||
"Can we add X" on its own is hard to act on. Say what you want, why you want it,
|
|
||||||
and how you picture it working.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: One line summary
|
|
||||||
description: What you want, in a sentence.
|
|
||||||
placeholder: Add Linux AppImage releases next to the macOS and Windows builds
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: game
|
|
||||||
attributes:
|
|
||||||
label: Which game is this about
|
|
||||||
description: Pick every version it applies to. Use N/A if it isn't game-specific.
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Red
|
|
||||||
- Blue
|
|
||||||
- Yellow
|
|
||||||
- Gold
|
|
||||||
- N/A
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: discord
|
|
||||||
attributes:
|
|
||||||
label: Discord username (optional)
|
|
||||||
description: >
|
|
||||||
So maintainers can ping you on Discord if they need a quick follow-up.
|
|
||||||
Leave blank if you'd rather keep everything on GitHub.
|
|
||||||
placeholder: yourname
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: what
|
|
||||||
attributes:
|
|
||||||
label: What do you want
|
|
||||||
description: >
|
|
||||||
Describe it properly. What is it, where does it live (launcher, options,
|
|
||||||
engine), what does the player see or do. If it changes something that already
|
|
||||||
exists, say what it does today and what it should do instead.
|
|
||||||
placeholder: |
|
|
||||||
Ship a Linux AppImage on each release, same version as the macOS/Windows builds,
|
|
||||||
with the same save folder layout and mod discovery path.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: why
|
|
||||||
attributes:
|
|
||||||
label: Why is this worth doing
|
|
||||||
description: >
|
|
||||||
What's annoying or missing right now. What does this fix. If it's just because you
|
|
||||||
think it would be fun, say that, it's a real answer.
|
|
||||||
placeholder: |
|
|
||||||
LÖVE already runs on Linux; without a packaged build, players have to assemble
|
|
||||||
it themselves and miss release notes / update checks.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: how
|
|
||||||
attributes:
|
|
||||||
label: How should it work
|
|
||||||
description: >
|
|
||||||
The specifics. Which menu, what happens in the edge cases. If you don't
|
|
||||||
know, say what you'd expect as a player and leave the rest open.
|
|
||||||
placeholder: |
|
|
||||||
- GitHub Releases asset next to the .dmg / .exe
|
|
||||||
- Same options.lua / mods/ layout as desktop
|
|
||||||
- Documented in the README install section
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: scope
|
|
||||||
attributes:
|
|
||||||
label: Does this change how the original game plays
|
|
||||||
description: >
|
|
||||||
Some requests are quality of life, some change the actual game. Both are fine,
|
|
||||||
it just helps to know which one you're asking for.
|
|
||||||
options:
|
|
||||||
- Quality of life, original game is untouched
|
|
||||||
- Changes how the game plays
|
|
||||||
- Not sure
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: extra
|
|
||||||
attributes:
|
|
||||||
label: Anything else
|
|
||||||
description: >
|
|
||||||
Reference screenshots, how another game does it, related issues. Leave blank
|
|
||||||
if nothing comes to mind.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
name: Mod request
|
|
||||||
description: Ask for a gameplay, cosmetic, audio, or QoL change that belongs as a Lua mod.
|
|
||||||
labels: ["mod request"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
This tracker is for ideas that should ship as **mods**, not as core engine
|
|
||||||
features — alternate sprites, running shoes, day/night, shiny indicators,
|
|
||||||
soundtrack packs, Gen 2-like battle toggles, map cosmetics, bag QoL, etc.
|
|
||||||
|
|
||||||
The engine already exposes a lot of this through registries and hooks
|
|
||||||
([modding wiki](https://github.com/bryanthaboi/gen1recomp/wiki)).
|
|
||||||
Use a **Feature request** instead for launcher / ports / video options /
|
|
||||||
networking / save tooling / new API seams.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: summary
|
|
||||||
attributes:
|
|
||||||
label: One line summary
|
|
||||||
description: What the mod should do, in a sentence.
|
|
||||||
placeholder: Hold B to run at bike speed on the overworld
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: game
|
|
||||||
attributes:
|
|
||||||
label: Which game is this for
|
|
||||||
description: Pick every version the mod should cover. Use N/A if it isn't game-specific.
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Red
|
|
||||||
- Blue
|
|
||||||
- Yellow
|
|
||||||
- Gold
|
|
||||||
- N/A
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: discord
|
|
||||||
attributes:
|
|
||||||
label: Discord username (optional)
|
|
||||||
description: >
|
|
||||||
So maintainers or mod authors can ping you on Discord if they pick this up.
|
|
||||||
Leave blank if you'd rather keep everything on GitHub.
|
|
||||||
placeholder: yourname
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: what
|
|
||||||
attributes:
|
|
||||||
label: What should the mod do
|
|
||||||
description: >
|
|
||||||
Describe the player-facing behavior. What changes, where, what does the
|
|
||||||
player see or press. If it toggles from Options or a START-menu entry, say so.
|
|
||||||
placeholder: |
|
|
||||||
Hold B while walking outdoors to move at bike speed. Release to walk again.
|
|
||||||
Same places the bike is allowed; no effect in battles or menus.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: why
|
|
||||||
attributes:
|
|
||||||
label: Why is this worth doing as a mod
|
|
||||||
description: >
|
|
||||||
Why optional/modded rather than a core option. Who wants it on, who wants
|
|
||||||
vanilla left alone.
|
|
||||||
placeholder: |
|
|
||||||
Great for replaying and backtracking, but some people want a strict Gen 1
|
|
||||||
pace. A mod (or an opt-in mod option) keeps both camps happy.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: how
|
|
||||||
attributes:
|
|
||||||
label: How should it work
|
|
||||||
description: >
|
|
||||||
Buttons, menus, edge cases, whether it needs new art/audio. If you know a
|
|
||||||
hook or registry that fits (movement.speed, pokemon.sprite, rulesets, …),
|
|
||||||
mention it — otherwise leave it open.
|
|
||||||
placeholder: |
|
|
||||||
- Hold B on the overworld
|
|
||||||
- Same step timing as the bike
|
|
||||||
- Disabled where the bike is disabled
|
|
||||||
- Prefer hooks:wrap("movement.speed") if that still fits
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: vanilla
|
|
||||||
attributes:
|
|
||||||
label: With the mod off, is vanilla unchanged
|
|
||||||
options:
|
|
||||||
- Yes — parity when disabled
|
|
||||||
- No — it would replace something always-on
|
|
||||||
- Not sure
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: category
|
|
||||||
attributes:
|
|
||||||
label: Best-fit mod category
|
|
||||||
description: Same taxonomy as example mods (BALANCE, GRAPHICS, AUDIO, …).
|
|
||||||
options:
|
|
||||||
- GAMEPLAY / QoL
|
|
||||||
- GRAPHICS
|
|
||||||
- AUDIO
|
|
||||||
- BALANCE / ruleset
|
|
||||||
- CONTENT (maps, encounters, trainers)
|
|
||||||
- UI / TOOL
|
|
||||||
- TOTAL_CONVERSION-ish
|
|
||||||
- Not sure
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: extra
|
|
||||||
attributes:
|
|
||||||
label: Anything else
|
|
||||||
description: >
|
|
||||||
Reference screenshots, other games/hacks that do it, related issues, or
|
|
||||||
"I'd like to try writing this myself." Leave blank if nothing comes to mind.
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
@@ -332,7 +332,7 @@ Maps can be edited in our own build of [Tiled](https://www.mapeditor.org),
|
|||||||
and exported back out as a mod; see
|
and exported back out as a mod; see
|
||||||
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
|
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
|
||||||
|
|
||||||
## Bugs and Ideas
|
## Bugs
|
||||||
|
|
||||||
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
|
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
|
||||||
that looks wrong, text in the wrong box, anything that does not match the
|
that looks wrong, text in the wrong box, anything that does not match the
|
||||||
@@ -341,12 +341,6 @@ original game.
|
|||||||
Attach a screenshot if you can. It saves a lot of back and forth, and if you
|
Attach a screenshot if you can. It saves a lot of back and forth, and if you
|
||||||
can't get one, the form asks you to describe what you saw instead.
|
can't get one, the form asks you to describe what you saw instead.
|
||||||
|
|
||||||
Thought of a feature that could be good, or a way to improve one that already
|
|
||||||
exists?
|
|
||||||
[Open a feature request](https://github.com/bryanthaboi/gen1recomp/issues/new?template=feature_request.yml).
|
|
||||||
Say what you want, why it is worth doing, and how you picture it working. A
|
|
||||||
request with real detail is one that can actually get built.
|
|
||||||
|
|
||||||
## More
|
## More
|
||||||
|
|
||||||
- [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play)
|
- [Link play](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Link-Play)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Tiled map editing (mod authoring)
|
||||||
|
|
||||||
|
`tools/tiled_export.py` turns the imported ROM cache into a
|
||||||
|
[Tiled](https://www.mapeditor.org) workspace, so maps can be edited in a
|
||||||
|
real map editor and exported back out as a mod. The original had no map
|
||||||
|
editor at all; the port's own map data is plain Lua, which is what makes
|
||||||
|
this a data path rather than an asset path.
|
||||||
|
|
||||||
|
Editing is done in our own Tiled build,
|
||||||
|
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
|
||||||
|
which ships the `gen1-mod-export` extension the workspace relies on. Grab it
|
||||||
|
from that repo's releases; upstream Tiled opens the workspace but cannot
|
||||||
|
export a mod out of it.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 tools/tiled_export.py # -> build/tiled/ (gitignored)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `build/tiled/gen1.tiled-project` in that build of Tiled.
|
||||||
|
|
||||||
|
- **The overworld is one surface.** All 222 maps become `maps/*.tmj`, and
|
||||||
|
`kanto.world` places the 36 connected overworld maps at their real
|
||||||
|
connection offsets. That world is pre-loaded (seeded into the workspace's
|
||||||
|
Tiled session), so opening any one overworld map draws its neighbors around
|
||||||
|
it and you scroll and edit straight across the seams. Everything else is a
|
||||||
|
double-click away in Tiled's project panel.
|
||||||
|
- **Extending Kanto wires both ends.** A connection lives on both maps, so
|
||||||
|
hooking a new map onto a base map also emits the return connection as a
|
||||||
|
patch on that base map, keeping its other directions intact. The return
|
||||||
|
offset is derived, not guessed: all 78 vanilla reciprocal pairs satisfy
|
||||||
|
`back.offset == -offset`.
|
||||||
|
- **A Tiled tile is a gen1 block.** Each of the 24 tilesets becomes a Tiled
|
||||||
|
tileset whose tiles are its 32x32 blocks, composited from the 8x8 sheet,
|
||||||
|
so a tile layer *is* the map's `blocks` array. Warps, signs and objects
|
||||||
|
sit on the 16px cell grid in object layers, which is the grid the engine
|
||||||
|
addresses them on.
|
||||||
|
- **Collision is visible.** View > Show Tile Collision Shapes draws the real
|
||||||
|
walkability: a rectangle covers each cell whose feet tile is not in the
|
||||||
|
tileset's `walkable` list, which is the rule `src/world/Map.lua` applies.
|
||||||
|
- **Maps are shown in their real colors.** Each map is atlased in the SGB
|
||||||
|
palette it renders with, so Cerulean is blue and Lavender is purple in the
|
||||||
|
editor exactly as in game. Vanilla resolves that through a cascade with
|
||||||
|
interiors inheriting the last outdoor map, so the workspace mirrors the
|
||||||
|
cascade and walks the warp graph to colour interiors. Changing a map's
|
||||||
|
`palette` exports `palette = "..."` on the record, which beats the cascade,
|
||||||
|
and the editor offers the real palette names as a dropdown.
|
||||||
|
- **New blocks and new tilesets.** `blocksets/*.tmj` show a tileset's blocks
|
||||||
|
as raw 8x8 tiles, four by four, so new blocks can be composed there;
|
||||||
|
per-tile flags on `tilesets/tiles_*.tsj` become `walkable`, `waterTiles`,
|
||||||
|
`doorTiles` and the rest.
|
||||||
|
- **Export is a diff, not a fork of the data.** The `gen1-mod-export`
|
||||||
|
extension (shipped in `tiled_gen1recomp`) writes either one map file or a whole
|
||||||
|
loadable mod folder. An edited vanilla map diffs against the imported data
|
||||||
|
and emits `mod.content.maps:patch` carrying *only* the fields that moved, so
|
||||||
|
a mod covers the parts it changes and leaves the rest to the base game; a
|
||||||
|
new map gets `:register` at an index of 1000 or above. An unchanged map
|
||||||
|
exports nothing at all. Exports pass `tools/modkit.py validate` and `lint`.
|
||||||
|
- **Or the whole record, on request.** Ticking `exactExport` on a map switches
|
||||||
|
it to `mod.content.maps:override`, pinning the map to exactly what the
|
||||||
|
editor shows. It is off by default because an override wins outright over
|
||||||
|
any other mod patching that map, where a patch composes.
|
||||||
|
|
||||||
|
No ROM-derived art travels into an exported mod: a tileset still drawing on
|
||||||
|
the player's own imported sheet references that path rather than shipping the
|
||||||
|
pixels, and only a sheet the author supplied is copied in.
|
||||||
+47
-3
@@ -183,6 +183,9 @@ end
|
|||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
local musicGen = 0
|
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)
|
function ChipAudio.playMusic(data, header, allowLoops)
|
||||||
if not ensureWorker() then
|
if not ensureWorker() then
|
||||||
@@ -204,9 +207,11 @@ function ChipAudio.playMusic(data, header, allowLoops)
|
|||||||
allowLoops = allowLoops, audio = slimAudio(data),
|
allowLoops = allowLoops, audio = slimAudio(data),
|
||||||
channelVolumes = ChipSynth.getChannelVolumes(),
|
channelVolumes = ChipSynth.getChannelVolumes(),
|
||||||
channelPitches = ChipSynth.getChannelPitches(),
|
channelPitches = ChipSynth.getChannelPitches(),
|
||||||
stereo = ChipSynth.getStereo() })
|
stereo = ChipSynth.getStereo(),
|
||||||
|
stereoEpoch = stereoEpoch })
|
||||||
currentMusic = { source = source, gen = gen, threaded = true,
|
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)
|
-- playback starts in update() once the first buffer arrives (~1 frame)
|
||||||
return source
|
return source
|
||||||
end
|
end
|
||||||
@@ -236,6 +241,9 @@ local function updateThreaded()
|
|||||||
if not buf then break end
|
if not buf then break end
|
||||||
if buf.gen ~= m.gen then
|
if buf.gen ~= m.gen then
|
||||||
-- stale buffer from a superseded song: drop it
|
-- 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
|
elseif buf.done then
|
||||||
m.finished = true
|
m.finished = true
|
||||||
elseif buf.error then
|
elseif buf.error then
|
||||||
@@ -354,9 +362,45 @@ function ChipAudio.shutdown()
|
|||||||
workerReady = false
|
workerReady = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ChipAudio.currentSource()
|
||||||
|
return currentMusic and currentMusic.source
|
||||||
|
end
|
||||||
|
|
||||||
function ChipAudio.setStereo(enabled)
|
function ChipAudio.setStereo(enabled)
|
||||||
|
enabled = not not enabled
|
||||||
|
if ChipSynth.getStereo() == enabled then return end
|
||||||
ChipSynth.setStereo(enabled)
|
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
|
end
|
||||||
|
|
||||||
function ChipAudio.getStereo()
|
function ChipAudio.getStereo()
|
||||||
|
|||||||
+49
-4
@@ -294,6 +294,10 @@ function Channel.new(engine, spec, options)
|
|||||||
noiseSampling = false, -- Gen 2 toggle_noise
|
noiseSampling = false, -- Gen 2 toggle_noise
|
||||||
condition = 0, -- Gen 2 set_condition / sound_jump_if
|
condition = 0, -- Gen 2 set_condition / sound_jump_if
|
||||||
tracks = tracks, -- Gen 2 CHANNEL_TRACKS (NR51 bits for this channel)
|
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,
|
waveInstrument = 0,
|
||||||
waveLevel = 1,
|
waveLevel = 1,
|
||||||
perfectPitch = false,
|
perfectPitch = false,
|
||||||
@@ -406,6 +410,20 @@ function Channel:pan()
|
|||||||
bit.band(self.engine.pan, mask) ~= 0
|
bit.band(self.engine.pan, mask) ~= 0
|
||||||
end
|
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)
|
function Channel:tone(ticks, register, volume, fade)
|
||||||
if register >= 0x800 then
|
if register >= 0x800 then
|
||||||
return self:timedEvent({ silence = true }, ticks)
|
return self:timedEvent({ silence = true }, ticks)
|
||||||
@@ -756,6 +774,7 @@ function Channel:nextEventGen2()
|
|||||||
local mask = bit.lshift(1, self.hardware - 1)
|
local mask = bit.lshift(1, self.hardware - 1)
|
||||||
local default = bit.bor(bit.lshift(mask, 4), mask)
|
local default = bit.bor(bit.lshift(mask, 4), mask)
|
||||||
self.tracks = bit.band(packed, default)
|
self.tracks = bit.band(packed, default)
|
||||||
|
self.forcePanning = true
|
||||||
elseif command == 0xE5 then -- volume (global master; ignored for mix)
|
elseif command == 0xE5 then -- volume (global master; ignored for mix)
|
||||||
self:byte()
|
self:byte()
|
||||||
elseif command == 0xE6 then -- pitch_offset (big-endian)
|
elseif command == 0xE6 then -- pitch_offset (big-endian)
|
||||||
@@ -778,8 +797,12 @@ function Channel:nextEventGen2()
|
|||||||
elseif command == 0xEE then -- unknownmusic0xee
|
elseif command == 0xEE then -- unknownmusic0xee
|
||||||
self:word()
|
self:word()
|
||||||
elseif command == 0xEF then
|
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()
|
local packed = self:byte()
|
||||||
|
self.stereoPanning = packed
|
||||||
|
self.forcePanning = false
|
||||||
if stereoEnabled then
|
if stereoEnabled then
|
||||||
local mask = bit.lshift(1, self.hardware - 1)
|
local mask = bit.lshift(1, self.hardware - 1)
|
||||||
local default = bit.bor(bit.lshift(mask, 4), mask)
|
local default = bit.bor(bit.lshift(mask, 4), mask)
|
||||||
@@ -1298,14 +1321,30 @@ function Engine:sampleStereo()
|
|||||||
local left, right = 0, 0
|
local left, right = 0, 0
|
||||||
for _, channel in ipairs(self.channels) do
|
for _, channel in ipairs(self.channels) do
|
||||||
local value = channel:sample()
|
local value = channel:sample()
|
||||||
local event = channel.event
|
local panLeft, panRight
|
||||||
if not event or event.panLeft ~= false then left = left + value end
|
if self.generation == 2 then
|
||||||
if not event or event.panRight ~= false then right = right + value end
|
-- 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
|
end
|
||||||
return analogOut(self, left, "hpfCapLeft", "lpfLeft"),
|
return analogOut(self, left, "hpfCapLeft", "lpfLeft"),
|
||||||
analogOut(self, right, "hpfCapRight", "lpfRight")
|
analogOut(self, right, "hpfCapRight", "lpfRight")
|
||||||
end
|
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)
|
function Engine:sampleChannel(number)
|
||||||
local selected = 0
|
local selected = 0
|
||||||
for _, channel in ipairs(self.channels) do
|
for _, channel in ipairs(self.channels) do
|
||||||
@@ -1360,4 +1399,10 @@ ChipSynth.newEngine = Engine.new
|
|||||||
ChipSynth.soundData = soundData
|
ChipSynth.soundData = soundData
|
||||||
ChipSynth.renderEffectData = renderEffectData
|
ChipSynth.renderEffectData = renderEffectData
|
||||||
|
|
||||||
|
function ChipSynth.applyStereo(engine)
|
||||||
|
if type(engine) == "table" and engine.applyStereo then
|
||||||
|
engine:applyStereo()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return ChipSynth
|
return ChipSynth
|
||||||
|
|||||||
+12
-1
@@ -507,7 +507,18 @@ function Music.applyOptions(opts)
|
|||||||
Music.setVolumeLevel(opts and opts.musicVol or 7)
|
Music.setVolumeLevel(opts and opts.musicVol or 7)
|
||||||
Music.setFilterLevel(opts and opts.musicFilter or 0)
|
Music.setFilterLevel(opts and opts.musicFilter or 0)
|
||||||
-- engine/menus/options_menu.asm SOUND row (wOptions STEREO bit)
|
-- 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
|
end
|
||||||
|
|
||||||
local function sourceStopped(src)
|
local function sourceStopped(src)
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
-- Protocol -- main thread pushes command tables onto the "chipaudio_cmd"
|
-- Protocol -- main thread pushes command tables onto the "chipaudio_cmd"
|
||||||
-- channel and drains produced buffers off "chipaudio_out":
|
-- channel and drains produced buffers off "chipaudio_out":
|
||||||
-- cmd = "play" { gen, header, allowLoops, audio,
|
-- cmd = "play" { gen, header, allowLoops, audio,
|
||||||
-- channelVolumes?, channelPitches? }
|
-- channelVolumes?, channelPitches?, stereo?, stereoEpoch? }
|
||||||
-- cmd = "stop" halt production
|
-- 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 = "invalidate" drop the bank cache
|
||||||
-- cmd = "quit" end the thread
|
-- cmd = "quit" end the thread
|
||||||
-- out buffers are tagged with the play's `gen` so the main thread can
|
-- 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 engine = nil -- the ChipSynth engine producing the current song
|
||||||
local finished = false -- the current song ran out (non-looping)
|
local finished = false -- the current song ran out (non-looping)
|
||||||
local data = nil -- { audio = <slim audio tables> } for ROM bank/wave reads
|
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)
|
local function handle(cmd)
|
||||||
if cmd.cmd == "play" then
|
if cmd.cmd == "play" then
|
||||||
@@ -56,6 +58,7 @@ local function handle(cmd)
|
|||||||
if cmd.stereo ~= nil then
|
if cmd.stereo ~= nil then
|
||||||
ChipSynth.setStereo(cmd.stereo)
|
ChipSynth.setStereo(cmd.stereo)
|
||||||
end
|
end
|
||||||
|
if cmd.stereoEpoch ~= nil then stereoEpoch = cmd.stereoEpoch end
|
||||||
local ok, eng = pcall(ChipSynth.newEngine, data, cmd.header,
|
local ok, eng = pcall(ChipSynth.newEngine, data, cmd.header,
|
||||||
{ allowLoops = cmd.allowLoops })
|
{ allowLoops = cmd.allowLoops })
|
||||||
if ok then
|
if ok then
|
||||||
@@ -73,6 +76,11 @@ local function handle(cmd)
|
|||||||
if cmd.volumes ~= nil then ChipSynth.setChannelVolumes(cmd.volumes) end
|
if cmd.volumes ~= nil then ChipSynth.setChannelVolumes(cmd.volumes) end
|
||||||
if cmd.pitches ~= nil then ChipSynth.setChannelPitches(cmd.pitches) end
|
if cmd.pitches ~= nil then ChipSynth.setChannelPitches(cmd.pitches) end
|
||||||
if cmd.stereo ~= nil then ChipSynth.setStereo(cmd.stereo) 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
|
elseif cmd.cmd == "invalidate" then
|
||||||
ChipSynth.invalidateBanks()
|
ChipSynth.invalidateBanks()
|
||||||
elseif cmd.cmd == "quit" then
|
elseif cmd.cmd == "quit" then
|
||||||
@@ -95,12 +103,13 @@ while true do
|
|||||||
local activeGen = gen
|
local activeGen = gen
|
||||||
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
|
local ok, sd = pcall(ChipSynth.soundData, engine, BUF, 2)
|
||||||
if not ok then
|
if not ok then
|
||||||
outCh:push({ gen = activeGen, error = tostring(sd) })
|
outCh:push({ gen = activeGen, error = tostring(sd),
|
||||||
|
stereoEpoch = stereoEpoch })
|
||||||
finished = true
|
finished = true
|
||||||
else
|
else
|
||||||
outCh:push({ gen = activeGen, sd = sd })
|
outCh:push({ gen = activeGen, sd = sd, stereoEpoch = stereoEpoch })
|
||||||
if engine:finished() then
|
if engine:finished() then
|
||||||
outCh:push({ gen = activeGen, done = true })
|
outCh:push({ gen = activeGen, done = true, stereoEpoch = stereoEpoch })
|
||||||
finished = true
|
finished = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -355,6 +355,11 @@ function OptionsMenu:cycle(row, delta)
|
|||||||
if next_ < 1 then next_ = #row.values end
|
if next_ < 1 then next_ = #row.values end
|
||||||
if next_ > #row.values then next_ = 1 end
|
if next_ > #row.values then next_ = 1 end
|
||||||
self.options[row.key] = row.values[next_]
|
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
|
end
|
||||||
|
|
||||||
function OptionsMenu:leave_()
|
function OptionsMenu:leave_()
|
||||||
|
|||||||
Reference in New Issue
Block a user