Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.py text eol=lf
*.md text eol=lf
*.json text eol=lf
sound/tests/audition/*.H text eol=crlf

# Preserve the approved binary stream and literal per-note audit bytes.
examples/doebel/DOEBEL.VGM binary
Expand Down
11 changes: 5 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ instrument banks, start in `sound/INSTR.C` and `sound/MIDISYN.C`, not the
native `src/lt_audio.c` / `lt_score.c` / `lt_sfx.c` APIs. Read
[`docs/PSG-BANKS.md`](docs/PSG-BANKS.md) before changing the bank contract.

- Keep the existing bank as Default, with identical output. The next bank
task adds one contrasting bank and a developer host A/B audition using
- Keep the existing bank as Default, with identical output. Chamber adds one contrasting bank; the developer host A/B audition uses
identical notes, timing, programs and controllers. No accepted song edits,
SoundFont/sample claims, extra sound driver or consumer Python preparation.
- `sound/` owns portable instrument behavior; libwintandy owns Windows
Expand All @@ -15,8 +14,8 @@ native `src/lt_audio.c` / `lt_score.c` / `lt_sfx.c` APIs. Read
- Use C89, fixed-size state and integer arithmetic. No heap, OS calls, port
I/O, timer ownership or interrupt work in the portable renderer. Preserve
16-bit near/far contracts and bounded work after long foreground stalls.
- `MSReset`, `MSSpeakerMode` and flags control existing behavior; no bank
selector exists yet. Preserve controller, allocation, reset and mute
- `MSReset`, `MSSpeakerMode` and flags control existing behavior; the
selector is `MSBank` (Default/Chamber). Preserve controller, allocation, reset and mute
semantics. Direct state changes require `MSInvalidate` before rendering.
- Host GCC checks do not qualify 8088 code size, Win16 DGROUP/ABI, audibility
or hardware timing. Report those separately and measure added table/state
Expand All @@ -25,8 +24,8 @@ native `src/lt_audio.c` / `lt_score.c` / `lt_sfx.c` APIs. Read
- On Chad's Windows host, run GCC only through
`C:\Users\chad\Documents\Codex\toolchains\gcc-ucrt64.py`; never start raw
GCC/cc1. Uppercase `.C` inputs need `-x c`. Developer Python is allowed.
- For the current documentation/bank handoff, do not launch emulators or
touch the separate VGM player/format/decompression task. Keep changes in
- For portable bank changes, do not launch emulators or touch the separate
Windows player, transport or format/decompression task. Keep changes in
isolated checkouts. Publication requires explicit approval.

There is no `BUILDING` file: `README.md`, `tools/build.py`, and
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ needed. The launcher runs its DLL preflight before compiler startup. These
options do not change the DOS build.

On Chad's Windows host, use the established launcher; do not start raw
GCC/cc1. The standalone MIDI runner accepts only one compiler executable,
so its Windows invocation is documented separately in
[the PSG bank source map](docs/PSG-BANKS.md#windows-host-checks).
GCC/cc1. Both host runners accept the approved launcher. Selectable
Default/Chamber banks and the per-note audition regression are documented
in [the PSG bank source map](docs/PSG-BANKS.md).

```powershell
python tools/build.py --target host --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py --runtime-bin C:/msys64/ucrt64/bin
python sound/tests/run.py --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py
```

Build logs and a result manifest are local under `build/`. Host tests exercise
Expand Down
224 changes: 100 additions & 124 deletions docs/PSG-BANKS.md
Original file line number Diff line number Diff line change
@@ -1,131 +1,107 @@
# Selectable PSG banks: source map and handoff
# Default and Chamber PSG banks

The portable MIDI renderer offers Default (0) and Chamber (1). Default keeps
the original output. Chamber adds eight melodic attenuation curves. Notes,
programs, controllers, percussion and speaker routing keep their meaning.

## Selection and lifetime

`MSBank(MSYN *, id)` returns zero on success. An invalid ID returns nonzero
without changing state. A melodic note latches selection in `TVOICE.bank`
at note-on. Held and pedal-released voices retain their existing curves.
`TVOICE.preset` keeps its original 0..10 meaning; never encode bank in it.
`MSReset` restores Default; callers reapply their explicit selection after
each reset and preserve per-voice bank state during live rebuilding.

Program changes, CC121/123 and zero CC0/32 do not select a bank. Nonzero
CC0/32 remain unsupported by low-level admission; the Windows file producer
skips them separately. `MSSpeakerMode` controls speaker/folding behavior;
synth flags control envelopes/vibrato. Neither selects instruments. Direct
voice/controller changes require `MSInvalidate` before `MSRender`.

## Curves and source ownership

`sound/INSTR.C` owns `curves[19][16]`. Original rows0..10 are unchanged:
eight melodic presets and kick/snare/hat. Chamber rows11..18 provide
plucked keys, swelling organ, soft bass, slow pad, breathy reed, fading
lead, mallet and blip. Values are PSG attenuation0..15 at55ms steps.
Step15 holds unless it is15, which ends the voice. There is no separate
note-off release envelope. Chamber attacks take up to165ms to contribute
PCM in the audition; short bell/hit curves can decay before key release.

Melodic programs select `program[channel] >> 4`. Presets3..5 retain
divider vibrato after eight steps. CC1 changes its depth; CC7/11 affect
attenuation; CC64 defers note-off; bend is fixed at +/-2 semitones. The
optional PIT speaker ignores PSG curves. No samples or SoundFonts are added.
Production rendering remains C89, integer, fixed storage and bounded lookup,
with no heap, OS, ports, timer or IRQ ownership.

| Repository/path | Role |
| --- | --- |
| libtandy `sound/INSTR.*`, `MIDISYN.*` | Canonical bank tables, allocation and rendering. |
| libtandy `sound/SMF.*` | Buffered parser:60000bytes,32768events,16tracks. |
| libtandy `tools/psgaudit.c` | Developer host A/B mixer, an approximation of PSG output. |
| libwintandy `libtandywin/AUDIO/SOURCE/` | Windows vendor and MIDI/speaker integration. |
| OEMSound `experimental/WINLIB30/SOURCE/` | Matching current player engine mirror. |
| OEMSound `src/win30/PLAYER/` | MINIPLAY UI and persistent Default/Chamber choice. |
| OEMSound `src/win30/DRIVER/` | The one shared SOUND.DRV. |

Windows preserves newer speaker fallback and streaming SMF (1MiB,
65000events,16tracks,64-byte windows). Port focused hunks, never overwrite
that parser/renderer with this portable version. libtandy `src/lt_audio.c`,
`lt_score.c`, `lt_sfx.c` and libwintandy `sound/` are separate APIs.

The qualified Windows integration uses `MDInstrumentBank` (ordinal19),
`MDSeedBank` (20), and `MciBank` (905). Both MIDICLK renderers (`synth` and
`folded`) reapply selection after resets, and live seeding preserves each
voice's bank. MINIPLAY stores selection in app-local WINAMP.INI. Windows
evidence is in libwintandy `docs/BANKS.TXT` and the source-pinned
`libtandywin/AUDIO/TESTS/NATIVE/BANKS.JSN`.

## Host checks and unchanged auditions

On POSIX use `python3 sound/tests/run.py --cc gcc`. On Chad's Windows host:

Audited against libtandy main `8a0313625382bf1b9fd16843336d9f4b2f577e44`,
libwintandy main `0e2a65eeedd2b49d5b5fce689eeac39e97fb9477`, and
OEMSound main `537ebc5363c3d6750f554bb955d96ec9f4ce833f` on 2026-10-25.
This guides future implementation; this documentation change adds no bank
or selection API.
```powershell
python sound/tests/run.py --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py
python tools/build.py --target host --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py --runtime-bin C:/msys64/ucrt64/bin
```

## Source ownership
The Windows runner refuses raw GCC and uses the launcher's DLL preflight.
The suite includes76 MIDI checks,111 bank checks and two Default traces
(including repeated `MSBank(0)`): `70D11B5D`,6634events,9798renders. Cases
cover presets, controllers, flat/envelope/vibrato, speaker, cache and stalls.

| Owner / path | Current role |
| --- | --- |
| libtandy `sound/INSTR.C`, `INSTR.H` | Canonical compact curves, `TVOICE`, `InstStart`, `InstRead`. |
| libtandy `sound/MIDISYN.C`, `MIDISYN.H` | `MSYN`, program-to-preset mapping, allocation, controllers, note-off, render cache. |
| libtandy `sound/MIDIMSG.H`, `LTPBASE.H` | MIDI admission, integer widths, envelope/vibrato flags. |
| libtandy `sound/tests/portable_midi.c`, `tests/run.py` | Standalone parser/renderer checks. |
| libwintandy `libtandywin/AUDIO/SOURCE/` | Vendored `INSTR.*` / `MIDISYN.*`; `MIDICLK.C` owns foreground rendering and paired speaker/PSG fallback. `MDAPI.H` / `MIDICLK.DEF` define its API. |
| Same Windows directory: `MCITNDY.C`, `MCIPSG.C`, `SMF.*`, `MIDISKIP.H` | Streamed validation, admission and event production into MIDICLK. |
| OEMSound `experimental/WINLIB30/SOURCE/` | Matching Windows source mirror consumed by `tools/WINAMP/PACK.PY` / `VERIFY.PY`. Despite the directory name, it supplies the current player engine. |
| OEMSound `src/win30/DRIVER/` | Canonical unified `SOUND.DRV`; retain existing ownership and speaker transport. Bank tables do not belong here. |

`libtandy/src/lt_audio.c`, `lt_score.c`, and `lt_sfx.c` are separate native
PSG APIs, not MIDI banks. `libwintandy/sound/` is a separate pinned producer
compatibility module, not the player's synth. OEMSound's Winamp UI lives in
`experimental/WINAMP30/`; its player, format and decompression work is active
in another task and outside this handoff.

At these revisions `INSTR.C` / `INSTR.H` match across the canonical core,
Windows vendor and OEMSound mirror. Windows `MIDISYN.*` / `SMF.*` differ
from the standalone core and match the OEMSound mirror. Preserve the newer
Windows speaker-fallback and streamed-file changes when porting focused
instrument changes. Coordinate mirror updates explicitly; do not overwrite
the published Windows engine with the older portable parser/renderer.

## Actual instrument controls

- `INSTR.C` has `curves[11][16]`: eight melodic presets (keys, organ,
bass, pad, reed, lead, bell, hit) and three percussion presets (kick,
snare, hat). The table occupies 176 bytes before linker overhead.
- Entries are PSG attenuation 0..15 at 55 ms steps. Index 15 holds unless
its value is 15, which ends the voice. Updates jump to the current index
after a stall, with no catch-up loop. Attack/decay/held-level shapes are
table values, not ADSR parameters.
- Melodic program 0..127 selects `program[channel] >> 4`: eight groups of
sixteen programs. Program changes affect subsequent note-ons. Channel 10
percussion chooses presets 8..10 and noise settings in `MIDISYN.C`,
independently of the melodic program mapping.
- Velocity adds attenuation; CC7/CC11 add quantized channel gain. CC64
defers note-off until pedal lift. Other note-off mutes immediately:
there is no separate release-envelope stage or release-time knob.
- Presets 3..5 support divider-based vibrato after eight steps, with an
eight-step motion cycle. CC1 affects depth; divider quantization limits
high notes. Bend is fixed at +/-2 semitones. Pan is stored but output
remains mono; CC91/93 add no reverb/chorus effect.
- The PSG has three square-wave tones and one noise voice. The optional
PIT voice has fixed amplitude with a zero/nonzero gain gate and does not
use PSG curves. No samples, filters, PWM, arbitrary waveform timbres or
SoundFont loading are provided.

## Existing hooks and missing selector

`MSReset` initializes flags to 3 (envelopes plus optional vibrato).
`MSSpeakerMode` controls legacy/prepared-speaker/low-note-fold routing, not
banks. `MSMessage[Deferred]` / `MSRender` apply events and render state;
`MSInvalidate` marks direct changes for the render cache.

Windows `MDVoiceMode` (export ordinal 16) accepts flags 0, 1 or 3 in
`MD_OPEN`, before the first successful `MDPush` or `MDCommit` (`haveTime`
and `committed` must both be zero); `MDOpen` defaults voice flags to 1.
This controls envelopes/vibrato, not banks. The older direct-driver
`MODM_SYNTHCONFIG` in `MIDIAPI.H` also controls flags. No bank field, setter
or export exists in `MSYN` / `MDAPI.H`; player MCI `MC_SET` handles time
format only.

Low-level `MIDIMSG.H` rejects nonzero CC0/CC32. The Windows file producer
uses `MIDISKIP.H` to skip unsupported nonzero bank selects, keeping the one
bank and honoring program changes. Zero bank select is an admitted no-op.
These are different policies, neither an alternate-bank selector. A future
explicit API must define invalid IDs, reset/default behavior, active-note
behavior and cache invalidation, keeping Windows `synth` / `folded` in sync.

## Scope, size and acceptance

Preserve Default output exactly, add one contrasting local bank, and provide
a developer host A/B audition of the same event sequence. Keep notes,
programs, controllers, velocity and timing identical between takes. Use
original synthetic fixtures; do not edit accepted songs or require consumer
Python/modern-PC preparation. Traces establish output equivalence; an audible
host preview must actually render audio and label its limits.

Keep C89, integer math, fixed storage, O(1) lookup and existing foreground
scheduling. No heap, new driver, timer/IRQ owner or large per-voice bank copy.
Account for tables once and per-`MSYN` additions twice in MIDICLK (`synth`
and `folded`). Preserve the DS near-pointer scratch storage noted in
`MSRender`; Win16 DLL SS and DS need not match.

The current Windows receipt `libtandywin/AUDIO/TESTS/NATIVE/MIDI8.MD`
records MIDICLK CODE/DGROUP minima 14352/12018 bytes, MCITNDY 10948/3398,
and RAWCLK 13800/27168. MIDICLK code is movable/discardable/preloaded;
data remains fixed/shared. The DLLs have zero local heap/private stack.
Report added code/data and native map/segment evidence; host `sizeof` does
not prove 16-bit layout. Respect segment limits and the 640 KB real-mode
budget; host checks alone cannot establish that a new bank fits.

Parser versions differ: standalone `sound/SMF.H` is buffered, limited to
60000 bytes / 32768 channel events / 16 tracks. Windows `SMF.H` streams
up to 1 MiB / 65000 events / 16 tracks with 64-byte track windows; its
memory-buffer convenience API retains a single-segment limit.

Implementation checks: Default traces before/after across all presets and
flat/envelope/vibrato modes; alternate audible difference; invalid IDs and
reset/switch/active-note behavior; note-off/sustain, CC7/11/1, pitch bend,
steal, percussion, long stall and render cache; paired speaker/folded
consistency. Retain parser/controller regressions. Native qualification is
a separate explicitly scheduled step; no emulator launch in this handoff.

## Windows host checks

From the libtandy root (developer Python only):
The actual audition mixer is privately instrumented without changing its
source or writing WAVs. Every expected nonzero-velocity note-on must allocate
and contribute to quantized final PCM, allowing attack delay. All154 notes
contribute, with zero missing/clipped/rejected/stolen notes in these scenes.
A deliberately muted phrase note must be detected as exactly one missing
voice despite nonzero mixed output. Velocity-zero note-ons are note-offs.
This is stronger than whole-scene RMS/peak alone.

For a read-only comparison against all eight existing approved takes:

```powershell
python tools/build.py --target host --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py --runtime-bin C:/msys64/ucrt64/bin
python C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py -- -x c -std=c89 -pedantic-errors -Wall -Wextra -Werror -Wno-misleading-indentation -Wno-sign-compare -Wno-unused-function -O2 -DSYNTH_HOST -I sound sound/INSTR.C sound/MIDISYN.C sound/SMF.C sound/tests/portable_midi.c -o build/host/portable_midi.exe
& ./build/host/portable_midi.exe
python sound/tests/audition/run.py --gcc-launcher C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py --audio C:/Users/chad/work/out/audition
```

The first command creates `build/host`. `sound/tests/run.py --cc gcc` is
the POSIX/ordinary compiler recipe; it accepts one executable and cannot
invoke the Python launcher via multiword `--cc`. Use the explicit Windows
command above. Integration MIDI checks are in libwintandy `docs/BUILD.TXT`.
Host checks do not qualify native timing, audio or code size.
The separate developer tool `tools/psgaudit.c <existing-output-directory>`
renders A/B WAVs with identical events and gain, without normalization.
These host previews do not claim physical Tandy output. No audition or
accepted song is changed by these tests.

## Size and native evidence

Tables add128bytes. Open Watcom `-ms -0 -os` measured INSTR code+19 and
MIDISYN+47bytes. Two-byte packing uses existing padding; packed-one-byte
layouts add five bytes per MSYN. Windows MSC6 measured TVOICE10/MSYN332,
MIDICLK CODE14844/DGROUP12148 (+492/+130), MCITNDY11192/3404 (+244/+6),
and MINIPLAY48202/16016 (+688/+416). Corrected RAWCLK paired-live handling
adds67code bytes; DGROUP27168 and SOUND.DRV remain unchanged.

Host checks alone do not establish Win16 ABI or hardware timing. Windows
evidence keeps85 file-player checks and28 corrected live-bank checks distinct
from the17-check exact-final-ZIP install/play/Stop/Close/DOS-cleanup smoke.
The older file suite is not relabelled as a rerun. No physical hardware
test is claimed. The merged Doebel example remains unchanged.
18 changes: 14 additions & 4 deletions sound/INSTR.C
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* Original 55 ms attenuation curves; no game data or interrupt code.
Indexed snapshots make each update O(1), even after a long GUI stall. */
#include "INSTR.H"
static LTP_U8 curves[11][16]={
static LTP_U8 curves[19][16]={
{0,0,1,2,3,4,5,6,7,8,8,8,8,8,8,8}, /* keys */
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, /* organ */
{0,0,1,1,2,2,3,3,4,4,5,5,5,5,5,5}, /* bass */
Expand All @@ -12,20 +12,30 @@ static LTP_U8 curves[11][16]={
{0,2,4,6,8,10,12,14,15,15,15,15,15,15,15,15}, /* hit */
{1,2,4,7,10,13,15,15,15,15,15,15,15,15,15,15}, /* kick */
{0,2,5,8,11,15,15,15,15,15,15,15,15,15,15,15}, /* snare */
{2,6,10,15,15,15,15,15,15,15,15,15,15,15,15,15} /* hat */
{2,6,10,15,15,15,15,15,15,15,15,15,15,15,15,15}, /* hat */
/* Chamber bank (rows 11..18): same preset order. Peaks match Default;
contrast is attack, decay and held level. 15 ends a voice anywhere. */
{0,1,3,5,7,9,10,10,10,10,10,10,10,10,10,10}, /* plucked keys */
{7,5,3,2,1,0,0,0,0,0,0,0,0,0,0,0}, /* swelling organ */
{0,1,2,4,6,7,8,8,8,8,8,8,8,8,8,8}, /* soft bass */
{14,12,10,8,6,5,4,3,2,2,2,2,2,2,2,2}, /* slow pad */
{9,6,4,2,1,0,0,1,1,1,1,1,1,1,1,1}, /* breathy reed */
{0,0,1,3,5,6,6,6,6,6,6,6,6,6,6,6}, /* fading lead */
{0,2,5,8,11,13,15,15,15,15,15,15,15,15,15,15}, /* mallet */
{0,3,7,11,15,15,15,15,15,15,15,15,15,15,15,15} /* blip */
};
static signed char motion[8]={0,1,2,1,0,-1,-2,-1};
void InstStart(TVOICE *v,unsigned preset,unsigned volume,
unsigned period,LTP_U32 step)
{
v->began=step;v->period=period;
v->velocity=(LTP_U8)volume;v->preset=(LTP_U8)preset;
v->velocity=(LTP_U8)volume;v->preset=(LTP_U8)preset;v->bank=0;
}
int InstRead(TVOICE *v,LTP_U32 step,LTP_U16 flags,LTP_U16 *period,LTP_U16 *volume)
{
LTP_U32 elapsed;unsigned n,a,depth;int delta,p;
elapsed=step-v->began;n=elapsed>15UL?15:(unsigned)elapsed;
a=curves[v->preset][n];
a=curves[v->bank?v->preset+11:v->preset][n];
if(a==15){*volume=15;*period=v->period;return 0;}
a+=v->velocity;*volume=a>15?15:a;p=v->period;
if(((flags&LTP_VIBRATO) || v->modulation) && elapsed>=8UL &&
Expand Down
2 changes: 1 addition & 1 deletion sound/INSTR.H
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
typedef struct {
LTP_U32 began;
LTP_U16 period;
LTP_U8 velocity,preset,modulation;
LTP_U8 velocity,preset,modulation,bank;
} TVOICE;
void InstStart(TVOICE *,unsigned,unsigned,unsigned,LTP_U32);
int InstRead(TVOICE *,LTP_U32,LTP_U16,LTP_U16 *,LTP_U16 *);
Expand Down
Loading
Loading