Skip to content

feat: OplChip bare-chip wrapper over nuked-opl3#3

Merged
csinkers merged 4 commits into
csinkers:masterfrom
abedegno:feat/bare-opl-chip
May 18, 2026
Merged

feat: OplChip bare-chip wrapper over nuked-opl3#3
csinkers merged 4 commits into
csinkers:masterfrom
abedegno:feat/bare-opl-chip

Conversation

@abedegno
Copy link
Copy Markdown
Contributor

@abedegno abedegno commented Apr 15, 2026

Summary

Adds a thin managed wrapper, ADLMidi.NET.OplChip, exposing libadlmidi's embedded nuked-opl3 chip at the register-write level (OPL3_Reset, OPL3_WriteReg, OPL3_GenerateStream). Lets callers synthesize their own OPL register streams — bypassing the MIDI / bank / sequencer layers — for use cases like rendering proprietary effect formats (my specific driver: Ultima Underworld 1 TVFX sound effects, which encode each SFX as a state-machine over raw OPL2 parameters, not as MIDI patches).

The three native symbols are already exported from the shipped libadlmidi.dylib / .so / .dll, so no native-side changes are needed — this PR is pure C# + one test CSProj tweak.

What changed

  • src/ADLMidi.NET/OplChip.cs — new public class. Allocates the opl3_chip struct via Marshal.AllocHGlobal (hardcoded 64 KB — nuked-opl3's struct is ~54 KB, dominated by writebuf[2048]; 10 KB headroom).
  • src/ADLMidi.NET.Tests/OplChipTests.cs — 3 tests (silent chip → zero samples; OPL2 note-on → audible output; reset → silence).
  • src/ADLMidi.NET.Tests/ADLMidi.NET.Tests.csproj — multi-target net8.0;net10.0 so tests build on machines with either framework installed.

Stacking

Stacks on #1 (macOS ARM64 test-fixture support) and #2 (adl_rt). This PR's actual changes are independent of both — only the branch base is stacked so the test infra resolves the dylib on arm64 Macs during development. GitHub will auto-rebase onto master as #1 and #2 land.

Tradeoffs

  • Struct-size assumption. If a future libadlmidi update grows opl3_chip past 64 KB, the allocation bound will need to bump. Happy to expose a sizeof helper from native side if you prefer not to carry the assumption.
  • Reset at nominal 44100 Hz. OplChip.Reset() re-initialises at 44100 since the sample-rate is stashed inside the opaque struct. Callers needing a different rate dispose + re-create. Documented on the method.

Test plan

  • dotnet test --filter OplChipTests — 3/3 pass (net10.0 on macOS arm64)
  • CI to confirm net8.0 path on Linux / Windows

@csinkers
Copy link
Copy Markdown
Owner

@abedegno looks like you might need to resolve the conflict from the new binary on the other PR before I can merge this one

abedegno added 4 commits May 18, 2026 11:10
Exposes OPL3_Reset / OPL3_WriteReg / OPL3_GenerateStream directly so
callers can synthesize their own register streams (e.g. Ultima Underworld
TVFX sound effects) without going through the MIDI/bank layer.

Multi-targets net8.0;net10.0 for the test project so CI continues to pass
on machines with only one framework installed.
The previous revision P/Invoked OPL3_Reset / OPL3_WriteReg /
OPL3_GenerateStream directly out of libadlmidi's internal nuked-opl3
source. Those symbols happened to leak out of the macOS dylib (where
the existing build linked without visibility filtering) but are not
part of libadlmidi's public ABI and are absent from the Windows DLL
and Linux .so export tables — so a reviewer on Windows got
EntryPointNotFoundException.

Rework against the new adl_barechip_* ABI
(https://github.com/abedegno/libADLMIDI feat/barechip-wrapper, based
on Wohlstand/libADLMIDI 89284b0, v1.6.2). The new API owns chip
allocation (returns an opaque handle), which also lets us drop the
64 KB AllocHGlobal trick we used to carve space for the opl3_chip
struct from managed code.

Rebuilt binaries for osx-arm64 (native clang), win-x64, win-x86
(mingw-w64 via Docker), and linux-x64 (gcc via Docker). All four now
export adl_barechip_new / _free / _write_reg / _generate / _reset,
verified with nm (macOS / Linux) and objdump -p (Windows). The full
ADLMIDI_DECLSPEC-decorated adl_* ABI is still exported intact on
Windows (213 exports total in libADLMIDI.dll).
Reworked per upstream feedback on libADLMIDI PR #310: drop the
parallel adl_barechip_* ABI and instead sit on two additions to
libadlmidi's existing public API:

  * adl_rt_rawOplCommand(device, chipId, reg, val) — raw register
    write reusing the same internal path used by IMF/KLM playback.
  * adl_reserveChipChannels(device, chipId, mask) — mark chip
    channels as off-limits to the MIDI voice allocator so raw writes
    and MIDI playback can share a chip.

OplChip now:
  - Wraps an ADL_MIDIPlayer* rather than a bare chip handle.
  - Create(sr) allocates its own device, sets 1 chip, reserves all
    23 per-chip channels so MIDI never touches it.
  - FromPlayer(device, chipId, mask) lets callers reuse an
    already-initialised MidiPlayer device for coexisting MIDI + raw
    SFX on the same chip.
  - Reset() calls adl_reset() (owning instances only) and re-asserts
    the channel reservation.

Test updates:
  - OplChipTests.Note_on sets OPL3 panning bits (0xC0 |= 0x30)
    because libadlmidi initialises chips in OPL3-extended mode where
    those bits gate L/R output.
  - DllImportFixture guards SetDllImportResolver with a static flag:
    with a second test class (OplChipTests) sharing the fixture,
    duplicate registration threw "A resolver is already set".

Native binaries refreshed for all 4 RIDs from
abedegno/libADLMIDI feat/barechip-wrapper at commit caa5db4.
All 5 ADLMidi.NET tests pass; all 47 UnderworldGodot Sfx tests pass.
Follows the API rename in libADLMIDI (review feedback on PR #310):
adl_rt_rawOplCommand -> adl_rt_rawOPL3. Refresh native libs from
the updated libADLMIDI build across all 4 RIDs.
@abedegno abedegno force-pushed the feat/bare-opl-chip branch from 14b0ac1 to 49d2568 Compare May 18, 2026 10:11
@abedegno
Copy link
Copy Markdown
Contributor Author

@csinkers - Rebased on top of the macOS PR merge, CI is green.

@csinkers csinkers merged commit ebc62e1 into csinkers:master May 18, 2026
2 checks passed
@csinkers
Copy link
Copy Markdown
Owner

Nice one, I've pushed out a NuGet package with your changes (version 1.2.0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants