diff --git a/.gitattributes b/.gitattributes index d92d407..2eca737 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/AGENTS.md b/AGENTS.md index ab0c45c..5e1e2e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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 @@ -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 diff --git a/README.md b/README.md index 75ea42d..e009fe0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/PSG-BANKS.md b/docs/PSG-BANKS.md index 9d89572..0aea777 100644 --- a/docs/PSG-BANKS.md +++ b/docs/PSG-BANKS.md @@ -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 ` +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. diff --git a/sound/INSTR.C b/sound/INSTR.C index d0fe567..3d23dc6 100644 --- a/sound/INSTR.C +++ b/sound/INSTR.C @@ -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 */ @@ -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<P_VIBRATO) || v->modulation) && elapsed>=8UL && diff --git a/sound/INSTR.H b/sound/INSTR.H index d70f57b..e8f558e 100644 --- a/sound/INSTR.H +++ b/sound/INSTR.H @@ -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 *); diff --git a/sound/MIDISYN.C b/sound/MIDISYN.C index ed5eabd..c2fa1bf 100644 --- a/sound/MIDISYN.C +++ b/sound/MIDISYN.C @@ -18,6 +18,7 @@ void MSReset(MSYN *s){unsigned i;LTP_U8 *p=(LTP_U8 *)s;for(i=0;ibaseVolume[i]=(LTP_U8)(s->volume[i]=15); for(i=0;i<16;i++){s->channelVolume[i]=s->expression[i]=127;s->pan[i]=64;s->pitchBend[i]=8192;s->bendFactor[i]=4096;}s->flags=3;s->renderDirty=1;s->renderValid=0;} void MSInvalidate(MSYN *s){s->renderDirty=1;} +LTP_U16 MSBank(MSYN *s,LTP_U16 bank){if(bank>MS_BANK_CHAMBER)return 1;s->bank=(LTP_U8)bank;MSInvalidate(s);return 0;} void MSSpeakerMode(MSYN *s,LTP_U16 mode){MSInvalidate(s);s->speakerMode=mode;s->speakerNote=s->speakerDivisor=s->speakerBase=0;s->speakerReleased=0;} /* Cache the extra 2dB attenuation only when a controller changes. PSG gain is quantized; PIT2 has fixed amplitude and supports only the zero/nonzero gate. */ @@ -119,7 +120,7 @@ static LTP_U16 applyMessage(MSYN *s,LTP_U32 ms,LTP_U32 value,unsigned renderNow) } if(s->counter==65535){for(i=0;i<3;i++){ranks[i]=1;for(j=0;j<3;j++)if(s->age[j]age[i])ranks[i]++;}for(i=0;i<3;i++)s->age[i]=ranks[i];s->counter=3;} s->note[slot]=pitch;s->key[slot]=a+1;s->channel[slot]=ch;s->age[slot]=++s->counter;s->p[slot]=periods[pitch];s->volume[slot]=volume;s->baseVolume[slot]=(LTP_U8)volume;s->released[slot]=0; - InstStart(&s->voice[slot],s->program[ch]>>4,volume,s->p[slot],step(ms));if(renderNow)MSRender(s,ms);return 0; + InstStart(&s->voice[slot],s->program[ch]>>4,volume,s->p[slot],step(ms));s->voice[slot].bank=s->bank;if(renderNow)MSRender(s,ms);return 0; } LTP_U16 MSMessage(MSYN *s,LTP_U32 ms,LTP_U32 value) {return applyMessage(s,ms,value,1);} diff --git a/sound/MIDISYN.H b/sound/MIDISYN.H index 79550e9..9d3ce5a 100644 --- a/sound/MIDISYN.H +++ b/sound/MIDISYN.H @@ -5,8 +5,14 @@ typedef struct {LTP_U16 note[3],channel[3],age[3],program[16],counter,noiseNote,noise,flags;TVOICE voice[4];LTP_U16 p[3],volume[4];LTP_U32 ms;LTP_U16 key[3],speakerMode,speakerNote,speakerChannel, speakerDivisor,speakerAttacks,bassFolded,pitchBend[16],bendFactor[16],modulation[16]; LTP_U8 channelVolume[16],expression[16],pan[16],sustain[16],gain[16]; - LTP_U8 released[4],speakerReleased,baseVolume[4],renderDirty,renderValid;LTP_U16 speakerBase,renderFlags;LTP_U32 renderQuantum;} MSYN; + LTP_U8 released[4],speakerReleased,baseVolume[4],renderDirty,renderValid,bank;LTP_U16 speakerBase,renderFlags;LTP_U32 renderQuantum;} MSYN; void MSReset(MSYN *); +/* Instrument banks latch at note-on: held and released voices keep theirs. + MSReset restores Default; percussion and speaker routing ignore banks. */ +#define MS_BANK_DEFAULT 0 +#define MS_BANK_CHAMBER 1 +/* Returns 0 on success; nonzero (state unchanged) for an unknown bank. */ +LTP_U16 MSBank(MSYN *,LTP_U16); /* 0 legacy; 1 prepared speaker; 2 octave-fold low MIDI into PSG. */ void MSSpeakerMode(MSYN *,LTP_U16); LTP_U16 MSMessage(MSYN *,LTP_U32,LTP_U32); diff --git a/sound/README.md b/sound/README.md index 1b109d4..2e4cd2a 100644 --- a/sound/README.md +++ b/sound/README.md @@ -1,8 +1,8 @@ # Portable MIDI core This is the first-party, MIT-licensed foreground MIDI parser and renderer from -the approved playback-opt9 engine. The 11 core files are preserved byte for -byte; `SOURCE.json` records their SHA-256 hashes. They use C89, fixed-size +the approved playback-opt9 engine. The 11 core files are unchanged except the +selectable-bank edits in `INSTR.*` and `MIDISYN.*`; `SOURCE.json` records their current SHA-256 hashes. They use C89, fixed-size state, integer arithmetic, and no OS, heap, port-I/O, timer, interrupt, or third-party instrument-bank dependency. The local instrument curves in `INSTR.C` are code tables, not imported patches or song assets. @@ -28,7 +28,9 @@ are octave-folded into the PIT range. The PSG's 10-bit integer divider quantizes pitches, especially high notes: labels 126 and 127 both use divider 8. Acceptance of 114 nominal labels 14–127 does **not** imply 114 distinct or perfectly tuned pitches. Drum note-on admission is 35–81 on MIDI channel 10. -One local instrument bank is supported; nonzero bank selection is rejected. +MIDI bank-select (CC0/CC32) is still rejected. Instrument banks are chosen by +the host API `MSBank` (`MS_BANK_DEFAULT`, `MS_BANK_CHAMBER`); see +[`docs/PSG-BANKS.md`](../docs/PSG-BANKS.md). On a host with a C89 compiler, run `python sound/tests/run.py --cc cc`. `CORE-MIT.TXT` contains the required notice for this module. diff --git a/sound/SOURCE.json b/sound/SOURCE.json index 427c4e4..b76a6f8 100644 --- a/sound/SOURCE.json +++ b/sound/SOURCE.json @@ -4,13 +4,13 @@ "source": "approved-playback-opt9/SOURCE/libtandy/sound", "license": "MIT; see CORE-MIT.TXT", "core_files": { - "INSTR.C": "cec224198dad374731f740d845cec8c25dbeae5f5cdfd22c74f8b0211320db72", - "INSTR.H": "ec0a1845a298fdd67d573dbd8cc623838d87b90d82c1727b4686c4ac175b2dd6", + "INSTR.C": "c9943748efe1d3fa3ada09c928f94eb1792799833ea930aa31e4308b445c700e", + "INSTR.H": "8540ba94b1fe9d8d80ac452f3926e607928e6cd629108f8ce9f43c7dfc6daba1", "LTMATH.H": "6dae15406fcc117cbdb3075423db2cfce3f4fd5663472207867952a0f3f80a6c", "LTPBASE.H": "5b7d46083bf4b761bdd1bebbf238436e16a97f26ce85fccabc897e5c83ade4c6", "MIDIMSG.H": "4eb8e78a83846bf625ebc0f84a0da9b3469c4c1a0fc578f185b8f8cd4147d43c", - "MIDISYN.C": "d8927fb136b2e36afcaeb0a948a87a479b5547c85c3e8d3fd3849c1a624e0d49", - "MIDISYN.H": "b920312440e2da974f7b9ab480bc3dbe0aed33bb52f8109d7efe937a0d0a76ac", + "MIDISYN.C": "5c3e715b7422bbfe358dba2c8a40513f7420e11de7d66f2ede5054eeb01c4815", + "MIDISYN.H": "ce3c04791c4d9584bad0c00dca5fa003a289107d909333e28e97d4622993d748", "PERIODS.H": "5ae78d5167aae9ca223bcfb0855312f4d6669dc5f3c40ed398cab5c61f80df93", "PITNOTES.H": "2ec6e70221ad49ffb71baec4b1c6798241d22a850f1289cf8d6108b7a6740840", "SMF.C": "841980adf9a1e95c59fc51acaf0dd94a339776f946e63a062789e63243e2c0f8", diff --git a/sound/tests/audition/AUDHOOK.H b/sound/tests/audition/AUDHOOK.H new file mode 100644 index 0000000..b5e4350 --- /dev/null +++ b/sound/tests/audition/AUDHOOK.H @@ -0,0 +1,78 @@ +/* Developer-only observation of the actual audition mixer. No synth edits. */ +typedef struct { + unsigned event,ch,note,velocity,slot,preset,bank,allocated; + LTP_U32 on,off; + unsigned long samples,first,last; +} AUDNOTE; +static AUDNOTE audNotes[MAX_EVENTS]; +static int audOwner[4];static double audParts[4]; +static unsigned audCount,audScene,audBank,audNegative; +static int audMuted; +static void AudBegin(unsigned scene,unsigned bank,unsigned negative) +{ + unsigned i;memset(audNotes,0,sizeof(audNotes));audCount=0; + audScene=scene;audBank=bank;audNegative=negative;audMuted=-1; + for(i=0;i<4;i++)audOwner[i]=-1; +} +static int AudEvent(unsigned event,LTP_U32 ms,LTP_U32 m,int rejected) +{ + unsigned i,ch=(unsigned)m&15,note=(unsigned)(m>>8)&127; + unsigned status=(unsigned)m&240,velocity=(unsigned)(m>>16)&127; + if(IsNoteOn(m)){ + AUDNOTE *a=&audNotes[audCount];a->event=event;a->ch=ch; + a->note=note;a->velocity=velocity;a->on=ms;a->off=0xffffffffUL; + a->slot=4;a->first=0xffffffffUL; + if(!rejected){ + if(ch==PERC_CH){if(synth.noiseNote==note)a->slot=3;} + else for(i=0;i<3;i++)if(synth.note[i]&&synth.channel[i]==ch&& + (synth.key[i]?synth.key[i]-1:synth.note[i])==note){a->slot=i;break;} + } + if(a->slot<4){a->allocated=1;a->preset=synth.voice[a->slot].preset; + a->bank=synth.voice[a->slot].bank;audOwner[a->slot]=(int)audCount;} + if(audNegative&&audScene==SC_PHRASE&&audBank==0&&ch==0&&audMuted<0) + audMuted=(int)audCount; + audCount++; + }else if(status==0x80||(status==0x90&&velocity==0)){ + for(i=audCount;i>0;i--)if(audNotes[i-1].ch==ch&&audNotes[i-1].note==note&& + audNotes[i-1].off==0xffffffffUL){audNotes[i-1].off=ms;break;} + } + return rejected; +} +static void AudSampleBegin(void){unsigned i;for(i=0;i<4;i++)audParts[i]=0.0;} +static double AudPart(unsigned voice,double value) +{ + if(audNegative&&audOwner[voice]==audMuted)value=0.0; + audParts[voice]=value;return value; +} +static short AudQuantize(double value) +{ + value*=32767.0; + if(value>32767.0)value=32767.0;else if(value< -32768.0)value= -32768.0; + return (short)(value<0.0?value-0.5:value+0.5); +} +static void AudCommit(unsigned long sample,short mixed,double sum) +{ + unsigned i;int owner; + for(i=0;i<4;i++){ + owner=audOwner[i]; + if(owner>=0&&audParts[i]!=0.0&&AudQuantize(sum-audParts[i])!=mixed){ + AUDNOTE *a=&audNotes[owner];if(!a->samples)a->first=sample; + a->samples++;a->last=sample; + } + } +} +static unsigned AudReport(FILE *f) +{ + unsigned i,missing=0; + for(i=0;isamples){first=1000.0*(double)a->first/(double)AUD_RATE; + last=1000.0*(double)a->last/(double)AUD_RATE;}else missing++; + fprintf(f,"%s,%s,%u,%u,%u,%lu,%lu,%u,%u,%u,%lu,%.3f,%.3f,%.3f,%s\n", + scene_names[audScene],bank_names[audBank],i,a->ch+1,a->note, + (unsigned long)a->on,(unsigned long)a->off,a->slot,a->preset,a->bank, + a->samples,first,last,first<0?-1.0:first-(double)a->on, + a->samples?"CONTRIBUTED":"MISSING"); + } + return missing; +} diff --git a/sound/tests/audition/AUDMAIN.H b/sound/tests/audition/AUDMAIN.H new file mode 100644 index 0000000..f42959f --- /dev/null +++ b/sound/tests/audition/AUDMAIN.H @@ -0,0 +1,51 @@ +/* No WAV writing: compare actual normal renders with the existing takes. */ +static int AudCompare(const char *directory,unsigned scene,unsigned bank,RENDER *r) +{ + char path[PATH_LEN];FILE *f;unsigned char h[44];unsigned long n;int lo,hi,value; + sprintf(path,"%s/%s_%s.wav",directory,scene_names[scene],bank_names[bank]); + f=fopen(path,"rb");if(!f)return 1; + if(fread(h,1,44,f)!=44||memcmp(h,"RIFF",4)||memcmp(h+8,"WAVE",4)|| + memcmp(h+36,"data",4)){fclose(f);return 1;} + for(n=0;nn;n++){ + lo=fgetc(f);hi=fgetc(f);if(lo<0||hi<0){fclose(f);return 1;} + value=lo|(hi<<8);if(value>=32768)value-=65536; + if(r->data[n]!=(short)value){fclose(f);return 1;} + } + value=fgetc(f);fclose(f);return value!=EOF; +} +int main(int argc,char **argv) +{ + unsigned scene,bank,negative,missing,total=0,notes=0;FILE *csv; + static const unsigned expected[4]={8,8,8,53}; + if(argc!=3&&argc!=4)return 2; + negative=argc==4&&!strcmp(argv[3],"--muted-note"); + csv=fopen(argv[2],"w");if(!csv)return 2; + fprintf(csv,"scene,bank,on_id,channel,note,on_ms,keyoff_ms,slot,preset,latched_bank,contribution_samples,first_pcm_ms,last_pcm_ms,attack_delay_ms,status\n"); + /* A velocity-zero note-on is an off, not an expected sounding note. */ + AudBegin(0,0,0);MSReset(&synth); + if(IsNoteOn(MakeMsg(0x90,60,0))|| + AudEvent(0,0,MakeMsg(0x90,60,0),MSMessage(&synth,0,MakeMsg(0x90,60,0)))||audCount){fclose(csv);return 2;} + puts("PASS velocity-zero note-on classified as off"); + for(scene=0;scenerejected++;', + 'if (AudEvent(ei, ms, m, MSMessage(&synth, ms, m))) r->rejected++;') + change('sum = 0.0;', 'AudSampleBegin();sum = 0.0;') + change('sum += (tp[i] < 0.5 ? 1.0 : -1.0)\n' + ' * Amp(synth.volume[i]) * TONE_GAIN;', + 'sum += AudPart(i,(tp[i] < 0.5 ? 1.0 : -1.0)\n' + ' * Amp(synth.volume[i]) * TONE_GAIN);') + change('sum += ((lfsr & 1U) ? 1.0 : -1.0)\n' + ' * Amp(synth.volume[3]) * NOISE_GAIN;', + 'sum += AudPart(3,((lfsr & 1U) ? 1.0 : -1.0)\n' + ' * Amp(synth.volume[3]) * NOISE_GAIN);') + change('r->data[n] = s;', 'r->data[n] = s;AudCommit(n,s,sum);') + change('int main(int argc, char **argv)', + 'static int OriginalMain(int argc, char **argv)') + source += '\n#include "AUDMAIN.H"\n' + private_source = out/'psgaudit.c' + private_source.write_text(source, newline='\n') + exe = out/('audnotes.exe' if os.name == 'nt' else 'audnotes') + records = [] + + def run(command, expected=0): + result = subprocess.run(command, capture_output=True, text=True, timeout=60) + records.append({'command': command, 'returncode': result.returncode, + 'stdout': result.stdout, 'stderr': result.stderr}) + (out/'results.json').write_text(json.dumps(records, indent=2)+'\n') + print(result.stdout, end='') + print(result.stderr, end='', file=sys.stderr) + if result.returncode != expected: + raise RuntimeError('Unexpected result: '+str(result.returncode)) + return result.stdout + + run(cc+['-x', 'c', '-std=c89', '-pedantic-errors', '-Wall', '-Wextra', + '-Werror', '-Wno-unused-function', '-Wno-misleading-indentation', + '-Wno-sign-compare', '-O2', '-DSYNTH_HOST', '-I', str(SOURCE), + '-I', str(HERE), str(private_source), str(SOURCE/'INSTR.C'), + str(SOURCE/'MIDISYN.C'), '-o', str(exe)]) + audio = str(args.audio.resolve()) if args.audio else '-' + positive = run([str(exe), audio, str(out/'pernote.csv')]) + if 'notes=154 missing=0' not in positive: + raise RuntimeError('Missing positive per-note result') + negative = run([str(exe), audio, str(out/'muted.csv'), '--muted-note'], 1) + if 'EXPECTED_NEGATIVE_DETECTED' not in negative: + raise RuntimeError('Muted-note negative control did not detect silence') + + +if __name__ == '__main__': + main() diff --git a/sound/tests/bank_test.c b/sound/tests/bank_test.c new file mode 100644 index 0000000..30d8568 --- /dev/null +++ b/sound/tests/bank_test.c @@ -0,0 +1,476 @@ +/* SPDX-License-Identifier: MIT; PSG instrument bank checks for the core. */ +#include +#include "MIDISYN.H" + +#define STEPS 20 +#define STEP_MS 55UL +#define SWITCH_K 5 +#define MAX_EVENTS 10 +#define SNAP_WIDTH 12 +#define COUNT(a) (sizeof(a) / sizeof((a)[0])) + +static unsigned checks, failures; +#define CHECK(x) do { ++checks; if (!(x)) { \ + printf("FAIL %u: %s\n", (unsigned)__LINE__, #x); ++failures; } } while (0) + +typedef struct { + unsigned char vol[STEPS]; + unsigned per[STEPS]; + unsigned char live[STEPS]; +} TRACE; + +typedef struct { + LTP_U32 ms; + unsigned status, a, b; +} EVENT; + +typedef struct { + unsigned vol[4], p[3], note[3], base[4]; +} SNAP; + +static LTP_U16 emit(MSYN *s, LTP_U32 ms, unsigned status, unsigned a, + unsigned b) +{ + return MSMessage(s, ms, (LTP_U32)status | ((LTP_U32)a << 8) | + ((LTP_U32)b << 16)); +} + +static void voice_on(MSYN *s, unsigned ch, unsigned prog, unsigned note, + unsigned vel) +{ + emit(s, 0, 0xc0 | ch, prog, 0); + emit(s, 0, 0x90 | ch, note, vel); +} + +static unsigned find_slot(const MSYN *s, unsigned ch, unsigned note) +{ + unsigned i; + for (i = 0; i < 3; ++i) + if (s->note[i] && s->channel[i] == ch && s->note[i] == note) + return i; + return 3; +} + +/* Renders absolute step k and records the voice's volume and period. + A voice that is not sounding is recorded as volume 15, not live. */ +static void capture(MSYN *s, unsigned ch, unsigned note, unsigned k, + TRACE *t) +{ + unsigned i; + MSRender(s, (LTP_U32)k * STEP_MS); + if (ch == 9) { + i = (note && s->noiseNote == note) ? 3 : 4; + } else { + i = find_slot(s, ch, note); + if (i == 3) + i = 4; + } + if (i < 4) { + t->vol[k] = (unsigned char)s->volume[i]; + t->per[k] = i < 3 ? s->p[i] : 0; + t->live[k] = 1; + } else { + t->vol[k] = 15; + t->per[k] = 0; + t->live[k] = 0; + } +} + +static void run(MSYN *s, unsigned ch, unsigned note, TRACE *t) +{ + unsigned k; + for (k = 0; k < STEPS; ++k) + capture(s, ch, note, k, t); +} + +static unsigned same_n(const TRACE *a, const TRACE *b, unsigned n) +{ + unsigned k; + for (k = 0; k < n; ++k) + if (a->vol[k] != b->vol[k] || a->per[k] != b->per[k] || + a->live[k] != b->live[k]) + return 0; + return 1; +} + +static unsigned same_trace(const TRACE *a, const TRACE *b) +{ + return same_n(a, b, STEPS); +} + +/* Fresh synth, bank selected, channel 0 note-on at ms 0, STEPS captured. */ +static void play(MSYN *s, LTP_U16 bank, unsigned prog, unsigned note, + TRACE *t) +{ + MSReset(s); + MSBank(s, bank); + voice_on(s, 0, prog, note, 100); + run(s, 0, note, t); +} + +/* Note-on on channel ch at SWITCH_K with no earlier voices. */ +static void late_on(MSYN *s, LTP_U16 bank, unsigned ch, unsigned note, + TRACE *t) +{ + unsigned k; + MSReset(s); + MSBank(s, bank); + for (k = 0; k < STEPS; ++k) { + if (k == SWITCH_K) + emit(s, (LTP_U32)SWITCH_K * STEP_MS, 0x90 | ch, note, 100); + capture(s, ch, note, k, t); + } +} + +static void take(const MSYN *s, SNAP *d) +{ + unsigned i; + for (i = 0; i < 4; ++i) { + d->vol[i] = s->volume[i]; + d->base[i] = s->baseVolume[i]; + } + for (i = 0; i < 3; ++i) { + d->p[i] = s->p[i]; + d->note[i] = s->note[i]; + } +} + +static unsigned same_snap(const SNAP *a, const SNAP *b) +{ + unsigned i; + for (i = 0; i < 4; ++i) + if (a->vol[i] != b->vol[i] || a->base[i] != b->base[i]) + return 0; + for (i = 0; i < 3; ++i) + if (a->p[i] != b->p[i] || a->note[i] != b->note[i]) + return 0; + return 1; +} + +static void test_groups(void) +{ + MSYN s; + TRACE def, def2, cha, cha2; + unsigned g, k, ok; + for (g = 0; g < 8; ++g) { + play(&s, MS_BANK_DEFAULT, g * 16, 60, &def); + play(&s, MS_BANK_DEFAULT, g * 16, 60, &def2); + play(&s, MS_BANK_CHAMBER, g * 16, 60, &cha); + play(&s, MS_BANK_CHAMBER, g * 16, 60, &cha2); + CHECK(def.live[0] && cha.live[0]); + CHECK(!same_trace(&def, &cha)); + CHECK(same_trace(&def, &def2)); + CHECK(same_trace(&cha, &cha2)); + ok = 1; + for (k = 0; k < STEPS; ++k) { + if (def.vol[k] > 15 || cha.vol[k] > 15) + ok = 0; + if (cha.live[k] && (cha.per[k] < 1 || cha.per[k] > 1023)) + ok = 0; + if (def.live[k] && (def.per[k] < 1 || def.per[k] > 1023)) + ok = 0; + } + CHECK(ok); + } +} + +static void test_invalid(void) +{ + static const LTP_U16 bad[3] = {2, 255, 65535U}; + MSYN s; + TRACE ref, got; + unsigned base, i; + for (base = 0; base < 2; ++base) { + play(&s, (LTP_U16)base, 0, 60, &ref); + for (i = 0; i < 3; ++i) { + MSReset(&s); + MSBank(&s, (LTP_U16)base); + CHECK(MSBank(&s, bad[i]) != 0); + CHECK(s.bank == base); + voice_on(&s, 0, 0, 60, 100); + run(&s, 0, 60, &got); + CHECK(same_trace(&got, &ref)); + } + } +} + +static void test_repeat(void) +{ + MSYN s; + TRACE ref, got; + unsigned i; + play(&s, MS_BANK_CHAMBER, 0, 60, &ref); + MSReset(&s); + for (i = 0; i < 3; ++i) { + CHECK(MSBank(&s, MS_BANK_CHAMBER) == 0); + CHECK(s.bank == MS_BANK_CHAMBER); + } + voice_on(&s, 0, 0, 60, 100); + run(&s, 0, 60, &got); + CHECK(same_trace(&got, &ref)); + for (i = 0; i < 3; ++i) { + CHECK(MSBank(&s, MS_BANK_DEFAULT) == 0); + CHECK(s.bank == MS_BANK_DEFAULT); + } +} + +static void test_switch(void) +{ + MSYN s; + TRACE def, cha, held, late, late_def, late_cha; + unsigned k; + + play(&s, MS_BANK_DEFAULT, 0, 60, &def); + play(&s, MS_BANK_CHAMBER, 0, 60, &cha); + + MSReset(&s); + voice_on(&s, 0, 0, 60, 100); + for (k = 0; k < STEPS; ++k) { + if (k == SWITCH_K) + MSBank(&s, MS_BANK_CHAMBER); + capture(&s, 0, 60, k, &held); + } + CHECK(same_trace(&held, &def)); + CHECK(s.bank == MS_BANK_CHAMBER); + + MSReset(&s); + MSBank(&s, MS_BANK_CHAMBER); + voice_on(&s, 0, 0, 60, 100); + for (k = 0; k < STEPS; ++k) { + if (k == SWITCH_K) + MSBank(&s, MS_BANK_DEFAULT); + capture(&s, 0, 60, k, &held); + } + CHECK(same_trace(&held, &cha)); + CHECK(s.bank == MS_BANK_DEFAULT); + + /* A note started after the switch takes the new bank, and the held + Default voice is unaffected by it. */ + late_on(&s, MS_BANK_DEFAULT, 1, 64, &late_def); + late_on(&s, MS_BANK_CHAMBER, 1, 64, &late_cha); + CHECK(!same_trace(&late_def, &late_cha)); + MSReset(&s); + voice_on(&s, 0, 0, 60, 100); + for (k = 0; k < STEPS; ++k) { + if (k == SWITCH_K) { + MSBank(&s, MS_BANK_CHAMBER); + emit(&s, (LTP_U32)SWITCH_K * STEP_MS, 0x91, 64, 100); + } + capture(&s, 1, 64, k, &late); + capture(&s, 0, 60, k, &held); + } + CHECK(same_trace(&late, &late_cha)); + CHECK(same_trace(&held, &def)); +} + +static void test_pedal(void) +{ + MSYN s; + TRACE def, rel, fresh, ref62; + unsigned k; + + play(&s, MS_BANK_DEFAULT, 0, 60, &def); + late_on(&s, MS_BANK_CHAMBER, 0, 62, &ref62); + + MSReset(&s); + voice_on(&s, 0, 0, 60, 100); + emit(&s, 0, 0xb0, 64, 127); + for (k = 0; k < STEPS; ++k) { + if (k == 2) + emit(&s, 2 * STEP_MS, 0x80, 60, 0); + if (k == SWITCH_K) { + MSBank(&s, MS_BANK_CHAMBER); + emit(&s, (LTP_U32)SWITCH_K * STEP_MS, 0x90, 62, 100); + } + if (k == 10) + emit(&s, 10 * STEP_MS, 0xb0, 64, 0); + capture(&s, 0, 60, k, &rel); + capture(&s, 0, 62, k, &fresh); + } + /* Pedal-released voice keeps the Default curve until the lift. */ + CHECK(same_n(&rel, &def, 10)); + CHECK(rel.live[9] && !rel.live[10]); + CHECK(!rel.live[STEPS - 1]); + /* Notes started under the pedal after the switch use Chamber. */ + CHECK(same_trace(&fresh, &ref62)); + CHECK(fresh.live[STEPS - 1]); +} + +static void test_reset(void) +{ + MSYN s; + TRACE def, cha, got; + + play(&s, MS_BANK_DEFAULT, 0, 60, &def); + play(&s, MS_BANK_CHAMBER, 0, 60, &cha); + + MSReset(&s); + CHECK(MSBank(&s, MS_BANK_CHAMBER) == 0); + CHECK(s.bank == MS_BANK_CHAMBER); + MSReset(&s); + CHECK(s.bank == MS_BANK_DEFAULT); + voice_on(&s, 0, 0, 60, 100); + run(&s, 0, 60, &got); + CHECK(same_trace(&got, &def)); + + MSReset(&s); + MSBank(&s, MS_BANK_CHAMBER); + emit(&s, 0, 0xb0, 121, 0); + CHECK(s.bank == MS_BANK_CHAMBER); + emit(&s, 0, 0xb0, 123, 0); + CHECK(s.bank == MS_BANK_CHAMBER); + voice_on(&s, 0, 0, 60, 100); + run(&s, 0, 60, &got); + CHECK(same_trace(&got, &cha)); +} + +static void test_cache(void) +{ + MSYN s; + SNAP def_snap, first, second, third; + + MSReset(&s); + voice_on(&s, 0, 0, 60, 100); + MSRender(&s, (LTP_U32)SWITCH_K * STEP_MS); + take(&s, &def_snap); + MSBank(&s, MS_BANK_CHAMBER); + MSRender(&s, (LTP_U32)SWITCH_K * STEP_MS); + take(&s, &first); + CHECK(same_snap(&def_snap, &first)); + + MSReset(&s); + MSBank(&s, MS_BANK_CHAMBER); + voice_on(&s, 0, 0, 60, 100); + MSRender(&s, (LTP_U32)SWITCH_K * STEP_MS); + take(&s, &first); + MSRender(&s, (LTP_U32)SWITCH_K * STEP_MS); + take(&s, &second); + CHECK(same_snap(&first, &second)); + MSBank(&s, MS_BANK_CHAMBER); + MSRender(&s, (LTP_U32)SWITCH_K * STEP_MS); + take(&s, &third); + CHECK(same_snap(&first, &third)); +} + +static void test_percussion(void) +{ + static const unsigned notes[3] = {35, 38, 42}; + MSYN s; + TRACE def, cha; + unsigned i; + for (i = 0; i < 3; ++i) { + MSReset(&s); + emit(&s, 0, 0x99, notes[i], 100); + run(&s, 9, notes[i], &def); + MSReset(&s); + MSBank(&s, MS_BANK_CHAMBER); + emit(&s, 0, 0x99, notes[i], 100); + run(&s, 9, notes[i], &cha); + CHECK(def.live[0] && cha.live[0]); + CHECK(same_trace(&def, &cha)); + } +} + +static const EVENT phrase_a[] = { + {0, 0x90, 60, 100}, {55, 0x90, 64, 100}, {110, 0x90, 67, 100}, + {165, 0x80, 60, 0}, {220, 0x91, 72, 100}, {275, 0x80, 64, 0}, + {330, 0x90, 62, 100}, {385, 0x80, 67, 0}, {440, 0x80, 62, 0}, + {495, 0x81, 72, 0} +}; + +static const EVENT phrase_b[] = { + {0, 0x90, 60, 100}, {55, 0x90, 62, 100}, {110, 0x90, 64, 100}, + {165, 0x90, 65, 100}, {220, 0x90, 67, 100}, + {275, 0x80, 64, 0}, {330, 0x80, 65, 0}, {385, 0x80, 67, 0}, + {440, 0x80, 60, 0}, {495, 0x80, 62, 0} +}; + +/* Records note, channel, key and age of each slot after every event. */ +static void phrase(MSYN *s, LTP_U16 bank, const EVENT *ev, unsigned n, + unsigned snap[][SNAP_WIDTH]) +{ + unsigned e, i; + MSReset(s); + MSBank(s, bank); + for (e = 0; e < n; ++e) { + emit(s, ev[e].ms, ev[e].status, ev[e].a, ev[e].b); + for (i = 0; i < 3; ++i) { + snap[e][i] = s->note[i]; + snap[e][3 + i] = s->channel[i]; + snap[e][6 + i] = s->key[i]; + snap[e][9 + i] = s->age[i]; + } + } +} + +static unsigned same_slots(unsigned a[][SNAP_WIDTH], + unsigned b[][SNAP_WIDTH], unsigned n) +{ + unsigned e, j; + for (e = 0; e < n; ++e) + for (j = 0; j < SNAP_WIDTH; ++j) + if (a[e][j] != b[e][j]) + return 0; + return 1; +} + +static void test_slots(void) +{ + MSYN s; + unsigned a[MAX_EVENTS][SNAP_WIDTH], b[MAX_EVENTS][SNAP_WIDTH]; + unsigned occupied, i; + + phrase(&s, MS_BANK_DEFAULT, phrase_a, COUNT(phrase_a), a); + phrase(&s, MS_BANK_CHAMBER, phrase_a, COUNT(phrase_a), b); + CHECK(same_slots(a, b, COUNT(phrase_a))); + CHECK(a[COUNT(phrase_a) - 1][0] == 0 && a[COUNT(phrase_a) - 1][1] == 0 + && a[COUNT(phrase_a) - 1][2] == 0); + + phrase(&s, MS_BANK_DEFAULT, phrase_b, COUNT(phrase_b), a); + phrase(&s, MS_BANK_CHAMBER, phrase_b, COUNT(phrase_b), b); + CHECK(same_slots(a, b, COUNT(phrase_b))); + occupied = 0; + for (i = 0; i < 3; ++i) + if (a[4][i]) + ++occupied; + CHECK(occupied == 3); + CHECK(a[4][0] == 67 || a[4][1] == 67 || a[4][2] == 67); + CHECK(a[COUNT(phrase_b) - 1][0] == 0 && a[COUNT(phrase_b) - 1][1] == 0 + && a[COUNT(phrase_b) - 1][2] == 0); +} + +static void test_mute(void) +{ + MSYN s; + TRACE t; + unsigned b, k, before; + for (b = 0; b < 2; ++b) { + MSReset(&s); + MSBank(&s, (LTP_U16)b); + voice_on(&s, 0, 0, 60, 100); + for (k = 0; k < 3; ++k) + capture(&s, 0, 60, k, &t); + before = s.volume[0]; + CHECK(before < 15 && s.note[0] == 60); + emit(&s, 3 * STEP_MS, 0x80, 60, 0); + CHECK(s.volume[0] == 15 && s.note[0] == 0); + MSRender(&s, 4 * STEP_MS); + CHECK(s.volume[0] == 15 && s.note[0] == 0); + } +} + +int main(void) +{ + test_groups(); + test_invalid(); + test_repeat(); + test_switch(); + test_pedal(); + test_reset(); + test_cache(); + test_percussion(); + test_slots(); + test_mute(); + printf("bank: %u checks, %u failures\n", checks, failures); + return failures != 0; +} diff --git a/sound/tests/banks.json b/sound/tests/banks.json new file mode 100644 index 0000000..e80ca4c --- /dev/null +++ b/sound/tests/banks.json @@ -0,0 +1,35 @@ +{ + "status": "PASS_CURRENT_MAIN_CANONICAL_BANKS", + "base": "728be2bf6aefd1f2356d3772f85bfbff7fcc8a8a", + "source_handoff": "c5fbc4511852658587fc24507068d1261cc3c452", + "Default_trace": "70D11B5D", + "trace_events": 6634, + "trace_renders": 9798, + "bank_checks": 111, + "portable_MIDI_checks": 76, + "expected_notes": 154, + "missing": 0, + "clipped": 0, + "rejected": 0, + "steals": 0, + "muted_negative_detected": true, + "approved_audio_unchanged": { + "attacks_ab.wav": "7930bd140768d7e218df46b25325b60145f2083433ba7c04e7fa929a5b0b8a4e", + "attacks_chamber.wav": "f8a72541452767ae8f4bbda58283cbe23e5fdbd7b455ff5d96f566816405aba3", + "attacks_default.wav": "5064434978d5216d3c94e3117aac2f265a21b1e7dc62d1ae6b70845c84db9404", + "held_ab.wav": "bc188c0b76466fa5ad88243db376f55afdd855cf468670dde04b9b08b4d28ef5", + "held_chamber.wav": "f708435af9403a14047b0f6f03e584f028f0b6085af01b9e8345e9e44a124a1f", + "held_default.wav": "85bd346a24d2dbdc93b0decdcbc997fdc5dee4ba2977be9b42fc5e5da8113b74", + "phrase_ab.wav": "a14b0384d62ed771c1cd016c20403d192ed01a25bc4c5b5f043a20251fb722f9", + "phrase_chamber.wav": "f7a5a20e7b7f274e20fd23973185bff139c86ea8c3959ee23e3601f4a7348547", + "phrase_default.wav": "855f135eca665b846bb37e7642d885b0325c69651472bf5ab9baf02caad50cd3", + "release_ab.wav": "e0d13bc52bf2ffb2da299c63960efe4d8e42ec6677a417e88f07710e80441493", + "release_chamber.wav": "996dcd166df34b481c7b875a9fa0ca5214ba54fd8407aa84af5512b738a39eb2", + "release_default.wav": "50d0e56ad7881d932e63c007e4f45b5b57d060ef34b01b9bf2b569bf277e937d", + "report.txt": "7580051136fa41dc6edcacf970361998746cef533e3091cdd9b191f05657c264" + }, + "doebel_and_existing_native_core_unchanged": true, + "native_tests_repeated": false, + "Windows_GCC_approved_launcher_only": true, + "log_sha256": "956361ba4da363618d31eddb59af48e9388d48ba4f539db03b09c0f90b42b9e8" +} diff --git a/sound/tests/run.py b/sound/tests/run.py index af0587a..7dfaed5 100644 --- a/sound/tests/run.py +++ b/sound/tests/run.py @@ -3,6 +3,7 @@ import argparse import os import subprocess +import sys ROOT = Path(__file__).resolve().parents[1] REPO = ROOT.parent @@ -10,18 +11,36 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument('--cc', default='cc') + parser.add_argument('--gcc-launcher', type=Path) args = parser.parse_args() + wrapper = Path('C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py') + if os.name == 'nt' and (not args.gcc_launcher or + args.gcc_launcher.resolve() != wrapper.resolve()): + parser.error('Windows GCC requires the approved gcc-ucrt64.py launcher') + cc = ([sys.executable, str(args.gcc_launcher.resolve()), '--'] + if args.gcc_launcher else [args.cc]) out = REPO / 'build' / 'host' out.mkdir(parents=True, exist_ok=True) - program = out / ('portable_midi.exe' if os.name == 'nt' else 'portable_midi') - command = [args.cc, '-x', 'c', '-std=c89', '-pedantic-errors', '-Wall', '-Wextra', - '-Werror', '-Wno-misleading-indentation', '-Wno-sign-compare', - '-Wno-unused-function', '-O2', '-DSYNTH_HOST', '-I' + str(ROOT), - str(ROOT / 'INSTR.C'), str(ROOT / 'MIDISYN.C'), - str(ROOT / 'SMF.C'), str(ROOT / 'tests/portable_midi.c'), - '-o', str(program)] - subprocess.run(command, check=True, timeout=30) - subprocess.run([str(program)], check=True, timeout=30) + for name, extra, defs in (('portable_midi', ['SMF.C'], []), + ('bank_test', [], []), + ('trace_default', [], []), + ('trace_default', [], ['-DWITH_BANK'])): + tag = name + ('_bank' if defs else '') + program = out / (tag + ('.exe' if os.name == 'nt' else '')) + command = cc + ['-x', 'c', '-std=c89', '-pedantic-errors', + '-Wall', '-Wextra', '-Werror', + '-Wno-misleading-indentation', '-Wno-sign-compare', + '-Wno-unused-function', '-O2', '-DSYNTH_HOST'] + defs + [ + '-I' + str(ROOT), str(ROOT / 'INSTR.C'), + str(ROOT / 'MIDISYN.C')] + command += [str(ROOT / f) for f in extra] + command += [str(ROOT / 'tests' / (name + '.c')), '-o', str(program)] + subprocess.run(command, check=True, timeout=30) + subprocess.run([str(program)], check=True, timeout=30) + audit = [sys.executable, str(ROOT/'tests/audition/run.py'), '--cc', args.cc] + if args.gcc_launcher: + audit += ['--gcc-launcher', str(args.gcc_launcher.resolve())] + subprocess.run(audit, check=True, timeout=60) if __name__ == '__main__': main() diff --git a/sound/tests/trace_default.c b/sound/tests/trace_default.c new file mode 100644 index 0000000..085735c --- /dev/null +++ b/sound/tests/trace_default.c @@ -0,0 +1,378 @@ +/* Default-bank regression driver for the libtandy PSG MIDI synth. + Prints one FNV-1a 32-bit hash over observable state only. Pass 1 adds + mid-stream MSBank(0) calls when built with -DWITH_BANK. Argument "-v" + prints a per-event and per-step text trace. */ +#include +#include +#include +#include "MIDISYN.H" + +/* Baseline (libtandy-base, no MSBank) hash; all builds must match. */ +#define EXPECTED_HASH 0x70D11B5DUL +#define FNV_OFFSET 2166136261UL +#define FNV_PRIME 16777619UL +#define MASK32 0xFFFFFFFFUL +#define MAX_EV 4096 +#define RENDER_STEP 10UL +#define STALL_AT 1000UL +#define STALL_JUMP 5000UL +#define PASSES 2 + +#define SUITE_GROUPS 0 +#define SUITE_CONTROL 1 +#define SUITE_STALL 2 +#define SUITE_SPEAKER1 3 +#define SUITE_SPEAKER2 4 +#define SUITE_LOW 5 + +#define MSG2(st,a) (((LTP_U32)(st))|(((LTP_U32)(a))<<8)) +#define MSG3(st,a,b) (MSG2(st,a)|(((LTP_U32)(b))<<16)) + +typedef struct { + LTP_U32 ms; + LTP_U32 msg; +} EVENT; + +static EVENT events[MAX_EV]; +static unsigned event_count; +static unsigned long hash_state = FNV_OFFSET; +static unsigned long total_events; +static unsigned long total_steps; +static int verbose; +static MSYN synth; + +static void die(const char *text) +{ + fprintf(stderr, "trace_default: %s\n", text); + exit(2); +} + +static void hash_byte(unsigned long b) +{ + hash_state = ((hash_state ^ (b & 0xFFUL)) * FNV_PRIME) & MASK32; +} + +/* Observables are hashed as 16-bit little-endian values. */ +static void hash16(unsigned long v) +{ + hash_byte(v); + hash_byte(v >> 8); +} + +static void put(LTP_U32 ms, LTP_U32 msg) +{ + if (event_count >= MAX_EV) + die("event buffer full"); + events[event_count].ms = ms; + events[event_count].msg = msg; + event_count++; +} + +/* Stable insertion sort by time, so same-time events keep build order. */ +static void sort_events(void) +{ + unsigned i, j; + EVENT t; + for (i = 1; i < event_count; i++) { + t = events[i]; + j = i; + while (j > 0 && events[j - 1].ms > t.ms) { + events[j] = events[j - 1]; + j--; + } + events[j] = t; + } +} + +/* Program groups 0..7, notes 45..100 at velocities 1/64/127, drums on + channel 9, plus out-of-range rejections (34 melodic, 82 percussion). */ +static LTP_U32 build_groups(void) +{ + static const unsigned vel[3] = {1, 64, 127}; + static const unsigned drum[8] = {35, 36, 38, 40, 42, 44, 46, 81}; + unsigned g, k, j, base, n; + for (g = 0; g < 8; g++) { + base = g * 1400; + put(base, MSG2(0xC0u, g * 16)); + put(base + 30, MSG3(0x90u, 34, 64)); + put(base + 40, MSG3(0x99u, 82, 64)); + for (k = 0; k < 56; k++) { + n = 45 + k; + put(base + 20 + k * 20, MSG3(0x90u, n, vel[k % 3])); + put(base + 120 + k * 20, MSG3(0x80u, n, 0)); + } + for (j = 0; j < 8; j++) { + put(base + 50 + j * 150, MSG3(0x99u, drum[j], vel[j % 3])); + put(base + 90 + j * 150, MSG3(0x89u, drum[j], 0)); + } + } + return 8 * 1400; +} + +/* Controllers 1/7/10/11/0/64/91/93/120/121/123, pitch bend, sustain + pedal with note-offs, voice stealing and a drum on channel 9. */ +static LTP_U32 build_control(void) +{ + static const unsigned cc7[4] = {0, 40, 100, 127}; + static const unsigned cc11[4] = {127, 90, 30, 1}; + static const unsigned cc1[3] = {0, 64, 127}; + static const unsigned bend[5] = {0, 8192, 16383, 4096, 12288}; + unsigned b, t, v; + for (b = 0; b < 20; b++) { + t = b * 400; + put(t, MSG3(0xB0u, 7, cc7[b % 4])); + put(t, MSG3(0xB0u, 11, cc11[b % 4])); + put(t + 10, MSG3(0xB0u, 1, cc1[b % 3])); + put(t + 10, MSG3(0xB0u, 64, 127)); + put(t + 20, MSG3(0x90u, 60, 64)); + put(t + 30, MSG3(0xB0u, 10, (b * 9) & 127)); + put(t + 40, MSG3(0x90u, 64, 127)); + put(t + 50, MSG3(0x99u, 36, 100)); + put(t + 60, MSG3(0x90u, 67, 1)); + put(t + 80, MSG3(0x90u, 72, 100)); + put(t + 200, MSG3(0x80u, 60, 0)); + put(t + 210, MSG3(0x80u, 64, 0)); + put(t + 220, MSG3(0x80u, 67, 0)); + put(t + 230, MSG3(0x80u, 72, 0)); + put(t + 250, MSG3(0xB0u, 64, 0)); + put(t + 250, MSG3(0x89u, 36, 0)); + put(t + 260, MSG3(0xB0u, 91, 40)); + put(t + 270, MSG3(0xB0u, 0, 0)); + put(t + 280, MSG3(0xB0u, 0, 1)); + put(t + 60, MSG3(0x91u, 48, 100)); + v = bend[b % 5]; + put(t + 80, MSG3(0xE1u, v & 127, (v >> 7) & 127)); + v = bend[(b + 2) % 5]; + put(t + 160, MSG3(0xE1u, v & 127, (v >> 7) & 127)); + put(t + 170, MSG3(0xB1u, 1, cc1[(b + 1) % 3])); + put(t + 180, MSG3(0xB1u, 7, cc7[(b + 1) % 4])); + put(t + 300, MSG3(0x81u, 48, 0)); + put(t + 100, MSG3(0x92u, 72, 127)); + put(t + 150, MSG3(0xB2u, 121, 0)); + put(t + 350, MSG3(0x82u, 72, 0)); + put(t + 120, MSG3(0x93u, 55, 90)); + put(t + 130, MSG3(0x93u, 52, 90)); + put(t + 140, MSG3(0x93u, 59, 90)); + put(t + 320, MSG3(0xB3u, 123, 0)); + } + return 19 * 400 + 400; +} + +/* A long GUI stall: no events between STALL_AT and STALL_AT+5000. */ +static LTP_U32 build_stall(void) +{ + put(0, MSG2(0xC0u, 16)); + put(100, MSG3(0x90u, 60, 100)); + put(300, MSG3(0x90u, 64, 100)); + put(500, MSG3(0x90u, 67, 100)); + put(700, MSG3(0x90u, 71, 100)); + put(900, MSG3(0x80u, 60, 0)); + put(950, MSG3(0x80u, 64, 0)); + put(6000, MSG3(0x80u, 67, 0)); + put(6010, MSG3(0x80u, 71, 0)); + put(6100, MSG3(0x90u, 48, 90)); + put(6200, MSG3(0xE0u, 0, 96)); + put(6400, MSG3(0x80u, 48, 0)); + put(6500, MSG3(0x90u, 45, 127)); + put(6900, MSG3(0x80u, 45, 0)); + put(7000, MSG3(0xB0u, 123, 0)); + return 7100; +} + +/* Speaker mode 1: channel 15 and low notes route to the PIT; CC11 0 and + 127 drive the gain-15 gate. */ +static LTP_U32 build_speaker1(void) +{ + unsigned k, n, ch, t; + for (k = 0; k < 48; k++) { + t = k * 100; + n = 30 + (k * 7) % 71; + ch = (k & 1) ? 15 : 0; + if (k % 6 == 0) + put(t, MSG3(0xB0u | ch, 11, 0)); + put(t + 10, MSG3(0x90u | ch, n, 100)); + put(t + 70, MSG3(0x80u | ch, n, 0)); + if (k % 6 == 0) + put(t + 80, MSG3(0xB0u | ch, 11, 127)); + } + return 4800; +} + +/* Speaker mode 2: low MIDI octave-folds into the PSG, with bends. */ +static LTP_U32 build_speaker2(void) +{ + unsigned k, n, ch, t; + for (k = 0; k < 60; k++) { + t = k * 80; + n = 24 + (k * 11) % 77; + ch = k % 4; + put(t, MSG3(0x90u | ch, n, (k * 17) % 127 + 1)); + put(t + 40, MSG3(0x80u | ch, n, 0)); + if (k % 5 == 0) + put(t + 20, MSG3(0xE0u | ch, (k * 3) & 127, k & 127)); + } + return 4800; +} + +/* Speaker mode 0: notes 24..44 are rejected (MSMessage returns 1). */ +static LTP_U32 build_low(void) +{ + unsigned k, n, t; + for (k = 0; k < 40; k++) { + t = k * 50; + n = 24 + k; + put(t, MSG3(0x90u, n, 80)); + put(t + 30, MSG3(0x80u, n, 0)); + if (k % 4 == 0) + put(t + 40, MSG3(0x90u, n, 0)); + } + return 2000; +} + +static LTP_U32 build_suite(unsigned suite) +{ + LTP_U32 end; + event_count = 0; + switch (suite) { + case SUITE_GROUPS: + end = build_groups(); + break; + case SUITE_CONTROL: + end = build_control(); + break; + case SUITE_STALL: + end = build_stall(); + break; + case SUITE_SPEAKER1: + end = build_speaker1(); + break; + case SUITE_SPEAKER2: + end = build_speaker2(); + break; + case SUITE_LOW: + end = build_low(); + break; + default: + die("unknown suite"); + return 0; + } + sort_events(); + return end; +} + +#ifdef WITH_BANK +static void mid_bank(unsigned pass) +{ + if (pass == 1 && MSBank(&synth, 0) != 0) + die("mid-stream MSBank(0) rejected"); +} +#else +static void mid_bank(unsigned pass) +{ + (void)pass; +} +#endif + +static void observe(unsigned pass, LTP_U32 now) +{ + unsigned i; + for (i = 0; i < 3; i++) + hash16(synth.p[i]); + for (i = 0; i < 4; i++) + hash16(synth.volume[i]); + hash16(synth.noise); + hash16(synth.noiseNote); + for (i = 0; i < 3; i++) + hash16(synth.note[i]); + for (i = 0; i < 4; i++) + hash16(synth.released[i]); + hash16(synth.speakerDivisor); + hash16(synth.speakerNote); + if (verbose) { + printf("S %u %lu p", pass, (unsigned long)now); + for (i = 0; i < 3; i++) + printf(" %u", (unsigned)synth.p[i]); + printf(" v"); + for (i = 0; i < 4; i++) + printf(" %u", (unsigned)synth.volume[i]); + printf(" n %u %u nt", (unsigned)synth.noise, + (unsigned)synth.noiseNote); + for (i = 0; i < 3; i++) + printf(" %u", (unsigned)synth.note[i]); + printf(" r"); + for (i = 0; i < 4; i++) + printf(" %u", (unsigned)synth.released[i]); + printf(" sd %u sn %u\n", (unsigned)synth.speakerDivisor, + (unsigned)synth.speakerNote); + } +} + +static void run_suite(unsigned suite, unsigned pass, unsigned flags, + unsigned mode, int stall) +{ + LTP_U32 end, now; + unsigned i, rc; + end = build_suite(suite); + MSReset(&synth); +#ifdef WITH_BANK + if (MSBank(&synth, 0) != 0) + die("MSBank(0) rejected after MSReset"); +#endif + synth.flags = (LTP_U16)flags; + MSSpeakerMode(&synth, (LTP_U16)mode); + if (verbose) + printf("RUN suite %u pass %u flags %u mode %u\n", suite, pass, + flags, mode); + i = 0; + now = 0; + for (;;) { + while (i < event_count && events[i].ms <= now) { + if (events[i].ms != now) + die("event not on render grid"); + mid_bank(pass); + rc = MSMessage(&synth, now, events[i].msg); + hash16(rc); + if (verbose) + printf("E %lu %08lX rc %u\n", (unsigned long)now, + (unsigned long)events[i].msg, rc); + i++; + } + mid_bank(pass); + MSRender(&synth, now); + observe(pass, now); + total_steps++; + if (now >= end) + break; + now += (stall && now == STALL_AT) ? STALL_JUMP : RENDER_STEP; + if (now > end) + now = end; + } + if (i != event_count) + die("events remain after end"); + total_events += event_count; +} + +int main(int argc, char **argv) +{ + unsigned pass; + verbose = (argc > 1 && strcmp(argv[1], "-v") == 0); + for (pass = 0; pass < PASSES; pass++) { + run_suite(SUITE_GROUPS, pass, 3, 0, 0); + run_suite(SUITE_CONTROL, pass, 3, 0, 0); + run_suite(SUITE_CONTROL, pass, 0, 0, 0); + run_suite(SUITE_CONTROL, pass, 1, 0, 0); + run_suite(SUITE_STALL, pass, 3, 0, 1); + run_suite(SUITE_SPEAKER1, pass, 3, 1, 0); + run_suite(SUITE_SPEAKER2, pass, 3, 2, 0); + run_suite(SUITE_LOW, pass, 3, 0, 0); + } + printf("hash %08lX events %lu steps %lu\n", hash_state, + total_events, total_steps); +#ifdef EXPECTED_HASH + if (hash_state != EXPECTED_HASH) { + printf("MISMATCH expected %08lX\n", (unsigned long)EXPECTED_HASH); + return 1; + } +#endif + return 0; +} diff --git a/tools/psgaudit.c b/tools/psgaudit.c new file mode 100644 index 0000000..deb4887 --- /dev/null +++ b/tools/psgaudit.c @@ -0,0 +1,695 @@ +/* psgaudit.c - developer A/B audition of the MIDI PSG instrument banks. + + Each scene's event list is built once and sent through the real synth + (MSReset, MSBank, MSMessage, MSRender from sound/MIDISYN.C) once per + bank. Only MSBank differs between the two takes. + + The PSG output stage below is a HOST APPROXIMATION of an SN76496: square + tones, a 15-bit LFSR noise voice and 2 dB attenuation steps, with fixed + per-voice gains and no normalization. It is NOT Tandy 1000 output. + Host-only developer tool; no driver or foreground code uses it. + + Usage: psgaudit (outdir must already exist) + The directory must already exist. Writes _default.wav, + _chamber.wav, _ab.wav and report.txt. */ + +#include +#include +#include +#include "MIDISYN.H" + +#define AUD_RATE 44100UL +#define AUD_CLOCK 3579545.0 +#define TONE_GAIN 0.2 +#define NOISE_GAIN 0.2 +#define TAIL_MS 1200UL +#define SILENCE_MS 500UL +#define UNIT_MS 150UL +#define GATE_TRIM 10UL +#define MAX_EVENTS 1024 +#define SCENE_COUNT 4 +#define BANK_COUNT 2 +#define TONE_VOICES 3 +#define PERC_CH 9 +#define SC_ATTACKS 0 +#define SC_HELD 1 +#define SC_RELEASE 2 +#define SC_PHRASE 3 +#define MAX_MELODIC 3 +#define PATH_LEN 1024 + +typedef struct { + LTP_U32 ms; + LTP_U32 msg; +} EVENT; + +typedef struct { + EVENT ev[MAX_EVENTS]; + unsigned count; + unsigned errors; +} EVENT_LIST; + +typedef struct { + unsigned long noteOns; + unsigned long noteOffs; + unsigned long percussion; + unsigned long controls; + unsigned long bends; + unsigned long programs; + unsigned long maxMelodic; + unsigned long checksum; +} SCENE_INFO; + +typedef struct { + short *data; + unsigned long n; + unsigned long rejected; + unsigned long steals; + unsigned long clipped; + unsigned long peak; + double rawPeak; + double sumSq; +} RENDER; + +static const char *const scene_names[SCENE_COUNT] = { + "attacks", "held", "release", "phrase" +}; + +static const char *const scene_desc[SCENE_COUNT] = { + "8 staccato notes, one per program group (program g*16), " + "150 ms each, 100 ms gaps, channel 1.", + "8 notes, one per program group, each held 1.2 s then off, " + "played in sequence.", + "CC64 pedal down in 3 windows (groups 0-2, 3-5, 6-7), each note " + "held by the pedal, pedal lifted after each window.", + "Original 8-bar phrase. Melody ch1 (programs 0,16,48,80,96), " + "bass ch2 (program 32, pad at bar 5), chord ch3 (program 64, " + "bars 4 and 8), kick/snare/hat on ch10 (notes 36,38,42). " + "CC7/CC11/CC1 and one pitch-bend gesture." +}; + +static const char *const bank_names[BANK_COUNT] = { + "default", "chamber" +}; + +static MSYN synth; +static EVENT_LIST list; +static SCENE_INFO infos[SCENE_COUNT]; +static RENDER result[SCENE_COUNT][BANK_COUNT]; +static unsigned long diffs[SCENE_COUNT]; + +static int IsNoteOn(LTP_U32 msg) +{ + return ((msg & 0xF0UL) == 0x90UL) && (((msg >> 16) & 0xFFUL) != 0UL); +} + +static LTP_U32 MakeMsg(unsigned status, unsigned a, unsigned b) +{ + return (LTP_U32)status | ((LTP_U32)a << 8) | ((LTP_U32)b << 16); +} + +/* Stable insertion by time. At equal times a note-on sorts after every + other event, so note-offs and controllers always precede it. */ +static void AddEvent(LTP_U32 ms, LTP_U32 msg) +{ + unsigned i; + int rank = IsNoteOn(msg) ? 1 : 0; + + if (list.count >= MAX_EVENTS) { + list.errors++; + return; + } + i = list.count++; + while (i > 0) { + EVENT *prev = &list.ev[i - 1]; + int prevRank = IsNoteOn(prev->msg) ? 1 : 0; + + if (prev->ms < ms || (prev->ms == ms && prevRank <= rank)) + break; + list.ev[i] = *prev; + i--; + } + list.ev[i].ms = ms; + list.ev[i].msg = msg; +} + +static void AddNoteOn(LTP_U32 ms, unsigned ch, unsigned note, unsigned vel) +{ + AddEvent(ms, MakeMsg(0x90U | ch, note, vel)); +} + +static void AddNoteOff(LTP_U32 ms, unsigned ch, unsigned note) +{ + AddEvent(ms, MakeMsg(0x80U | ch, note, 0)); +} + +static void AddCc(LTP_U32 ms, unsigned ch, unsigned cc, unsigned val) +{ + AddEvent(ms, MakeMsg(0xB0U | ch, cc, val)); +} + +static void AddProgram(LTP_U32 ms, unsigned ch, unsigned prog) +{ + AddEvent(ms, MakeMsg(0xC0U | ch, prog, 0)); +} + +static void AddBend(LTP_U32 ms, unsigned ch, unsigned value) +{ + AddEvent(ms, MakeMsg(0xE0U | ch, value & 127U, (value >> 7) & 127U)); +} + +/* Scene 1: staccato attack on each program group, channel 0. */ +static void BuildAttacks(void) +{ + unsigned g; + + for (g = 0; g < 8; g++) { + LTP_U32 t = (LTP_U32)g * 250UL; + + AddProgram(t, 0, g * 16U); + AddNoteOn(t, 0, 60, 100); + AddNoteOff(t + 150UL, 0, 60); + } +} + +/* Scene 2: each group holds a note for 1.2 s, in sequence, channel 0. */ +static void BuildHeld(void) +{ + unsigned g; + + for (g = 0; g < 8; g++) { + LTP_U32 t = (LTP_U32)g * 1300UL; + + AddProgram(t, 0, g * 16U); + AddNoteOn(t, 0, 60, 100); + AddNoteOff(t + 1200UL, 0, 60); + } +} + +/* Scene 3: three pedal windows. At most three notes are held by the pedal + in any window, so the synth never reclaims a pedal-held voice. */ +static void BuildRelease(void) +{ + static const unsigned char first[3] = {0, 3, 6}; + static const unsigned char count[3] = {3, 3, 2}; + unsigned w, j; + LTP_U32 base = 0UL; + + for (w = 0; w < 3; w++) { + LTP_U32 lift; + + AddCc(base, 0, 64, 127); + for (j = 0; j < count[w]; j++) { + unsigned g = first[w] + j; + LTP_U32 t = base + (LTP_U32)j * 300UL; + + AddProgram(t, 0, g * 16U); + AddNoteOn(t, 0, 60, 100); + AddNoteOff(t + 200UL, 0, 60); + } + lift = base + (LTP_U32)count[w] * 300UL + 100UL; + AddCc(lift, 0, 64, 0); + base = lift + 200UL; + } +} + +/* Scene 4: original 8-bar phrase, 64 units of 150 ms (9.6 s). Melody and + bass never exceed three sounding melodic notes together; the chord + bars drop the bass so melody + two chord tones make three. */ +static void BuildPhrase(void) +{ + static const unsigned char melP[] = { + 72, 74, 72, 74, 72, 67, 69, 72, 71, 69, 67, 65, 64, 62, 64, 67, + 72, 76, 79, 76, 81, 79, 77, 76, 74, 72, 71, 69, 67, 72 + }; + static const unsigned char melU[] = { + 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 4, 4 + }; + static const unsigned char bassP[] = { + 48, 55, 45, 52, 53, 48, 0, 55, 48, 45, 53, 50, 45, 0, 48 + }; + static const unsigned char bassU[] = { + 4, 4, 4, 4, 4, 4, 4, 4, 8, 4, 4, 4, 4, 4, 4 + }; + static const unsigned char percU[6] = {0, 12, 20, 26, 48, 60}; + static const unsigned char percN[6] = {36, 38, 36, 42, 36, 38}; + unsigned i, u = 0; + unsigned melCount = sizeof(melP) / sizeof(melP[0]); + unsigned bassCount = sizeof(bassP) / sizeof(bassP[0]); + LTP_U32 t; + + if (melCount != sizeof(melU) / sizeof(melU[0]) || + bassCount != sizeof(bassU) / sizeof(bassU[0])) { + list.errors++; + return; + } + + /* Melody, channel 0. */ + for (i = 0; i < melCount; i++) { + t = (LTP_U32)u * UNIT_MS; + AddNoteOn(t, 0, melP[i], 100); + AddNoteOff(t + (LTP_U32)melU[i] * UNIT_MS - GATE_TRIM, 0, melP[i]); + u += melU[i]; + } + if (u != 64) list.errors++; + + /* Bass, channel 1. */ + u = 0; + for (i = 0; i < bassCount; i++) { + t = (LTP_U32)u * UNIT_MS; + if (bassP[i]) { + AddNoteOn(t, 1, bassP[i], 90); + AddNoteOff(t + (LTP_U32)bassU[i] * UNIT_MS - GATE_TRIM, + 1, bassP[i]); + } + u += bassU[i]; + } + + /* Chord, channel 2: two tones in bar 4 and bar 8. */ + t = 24UL * UNIT_MS; + AddNoteOn(t, 2, 65, 70); + AddNoteOn(t, 2, 69, 70); + AddNoteOff(t + 4UL * UNIT_MS - GATE_TRIM, 2, 65); + AddNoteOff(t + 4UL * UNIT_MS - GATE_TRIM, 2, 69); + t = 56UL * UNIT_MS; + AddNoteOn(t, 2, 60, 70); + AddNoteOn(t, 2, 64, 70); + AddNoteOff(t + 4UL * UNIT_MS - GATE_TRIM, 2, 60); + AddNoteOff(t + 4UL * UNIT_MS - GATE_TRIM, 2, 64); + + /* Percussion, channel 10 (index 9). */ + for (i = 0; i < 6; i++) { + unsigned vel = percN[i] == 42 ? 80 : (percN[i] == 38 ? 100 : 110); + + AddNoteOn((LTP_U32)percU[i] * UNIT_MS, PERC_CH, percN[i], vel); + } + + /* Programs and controllers. */ + AddProgram(0UL, 0, 0); + AddProgram(0UL, 1, 32); + AddProgram(0UL, 2, 64); + AddCc(0UL, 0, 7, 100); + AddCc(0UL, 0, 11, 110); + AddCc(0UL, 0, 1, 0); + AddProgram(16UL * UNIT_MS, 0, 16); + AddCc(16UL * UNIT_MS, 0, 11, 90); + AddProgram(32UL * UNIT_MS, 0, 48); + AddCc(32UL * UNIT_MS, 0, 1, 100); + AddCc(32UL * UNIT_MS, 0, 7, 127); + AddProgram(32UL * UNIT_MS, 1, 48); + AddCc(32UL * UNIT_MS, 1, 1, 127); + AddProgram(48UL * UNIT_MS, 0, 80); + AddCc(48UL * UNIT_MS, 0, 1, 40); + AddProgram(48UL * UNIT_MS, 1, 32); + AddCc(48UL * UNIT_MS, 1, 1, 0); + AddBend(50UL * UNIT_MS, 0, 12288UL); + AddBend(52UL * UNIT_MS, 0, 8192UL); + AddProgram(56UL * UNIT_MS, 0, 96); + AddCc(56UL * UNIT_MS, 0, 11, 110); +} + +static void AnalyzeList(SCENE_INFO *si) +{ + unsigned i, status, ch, vel; + unsigned long active = 0UL; + LTP_U32 m; + + memset(si, 0, sizeof(*si)); + for (i = 0; i < list.count; i++) { + m = list.ev[i].msg; + status = (unsigned)(m & 0xF0UL); + ch = (unsigned)(m & 0x0FUL); + vel = (unsigned)((m >> 16) & 0xFFUL); + si->checksum = si->checksum * 31UL + + (unsigned long)list.ev[i].ms * 7UL + (unsigned long)m; + if (status == 0x90 && vel != 0 && ch != PERC_CH) { + si->noteOns++; + active++; + if (active > si->maxMelodic) si->maxMelodic = active; + } else if (status == 0x80 || (status == 0x90 && vel == 0)) { + if (ch != PERC_CH) { + si->noteOffs++; + if (active) active--; + } + } else if (status == 0x90) { + si->percussion++; + } else if (status == 0xB0) { + si->controls++; + } else if (status == 0xC0) { + si->programs++; + } else if (status == 0xE0) { + si->bends++; + } + } +} + +static int BuildScene(unsigned scene) +{ + list.count = 0; + list.errors = 0; + switch (scene) { + case SC_ATTACKS: + BuildAttacks(); + break; + case SC_HELD: + BuildHeld(); + break; + case SC_RELEASE: + BuildRelease(); + break; + default: + BuildPhrase(); + break; + } + if (list.errors || list.count == 0) return 1; + AnalyzeList(&infos[scene]); + return 0; +} + +/* Attenuation: 2 dB per step, amplitude 10^(-0.1*v). 15 is silent. */ +static double Amp(unsigned v) +{ + static double table[16]; + static int ready = 0; + unsigned i; + + if (!ready) { + table[0] = 1.0; + for (i = 1; i < 15; i++) + table[i] = table[i - 1] * 0.7943282347242815; + table[15] = 0.0; + ready = 1; + } + return table[v > 15 ? 15 : v]; +} + +static double SqrtD(double x) +{ + double g; + unsigned i; + + if (x <= 0.0) return 0.0; + g = x > 1.0 ? x : 1.0; + for (i = 0; i < 100; i++) g = 0.5 * (g + x / g); + return g; +} + +static int PrepareRender(RENDER *r, unsigned long n) +{ + memset(r, 0, sizeof(*r)); + r->n = n; + r->data = (short *)malloc(n * sizeof(short)); + return r->data ? 0 : 1; +} + +/* Renders the current list through the real synth for one bank. The + event delivery and MSRender calls are identical for both banks. */ +static int RenderBank(unsigned bank, RENDER *r) +{ + LTP_U32 ms = 0UL, lastMs = 0xFFFFFFFFUL; + unsigned long n, acc = 0UL; + unsigned ei = 0, i, nz, ndiv; + unsigned lfsr = 0x4000U, fb; + double tp[TONE_VOICES], np = 0.0, sum, val, a; + short s; + + MSReset(&synth); + if (MSBank(&synth, (LTP_U16)bank) != 0) return 1; + for (i = 0; i < TONE_VOICES; i++) tp[i] = 0.0; + + for (n = 0; n < r->n; n++) { + while (ei < list.count && list.ev[ei].ms <= ms) { + LTP_U32 m = list.ev[ei].msg; + int melodicOn = IsNoteOn(m) && ((m & 0x0FUL) != PERC_CH); + + if (melodicOn && synth.note[0] && synth.note[1] && + synth.note[2]) + r->steals++; + if (MSMessage(&synth, ms, m)) r->rejected++; + ei++; + } + if (ms != lastMs) { + MSRender(&synth, ms); + lastMs = ms; + } + + sum = 0.0; + for (i = 0; i < TONE_VOICES; i++) { + unsigned p = synth.p[i]; + + if (synth.note[i] && p) { + tp[i] += AUD_CLOCK / (32.0 * (double)p) / (double)AUD_RATE; + while (tp[i] >= 1.0) tp[i] -= 1.0; + sum += (tp[i] < 0.5 ? 1.0 : -1.0) + * Amp(synth.volume[i]) * TONE_GAIN; + } + } + + /* Noise: bit 2 selects white (1) or periodic (0), bits 0-1 the + rate: div 16/32/64. Setting 3 (tone-2 clocked) is treated as + div 64 here; MIDISYN.C never produces it. */ + nz = synth.noise & 7U; + ndiv = (nz & 3U) == 3U ? 64U : (16U << (nz & 3U)); + np += AUD_CLOCK / (32.0 * (double)ndiv) / (double)AUD_RATE; + while (np >= 1.0) { + np -= 1.0; + fb = (nz & 4U) ? ((lfsr ^ (lfsr >> 3)) & 1U) : (lfsr & 1U); + lfsr = (lfsr >> 1) | (fb << 14); + } + if (synth.volume[3] < 15) { + sum += ((lfsr & 1U) ? 1.0 : -1.0) + * Amp(synth.volume[3]) * NOISE_GAIN; + } + + a = sum < 0.0 ? -sum : sum; + if (a > r->rawPeak) r->rawPeak = a; + + val = sum * 32767.0; + if (val > 32767.0) { + val = 32767.0; + r->clipped++; + } else if (val < -32768.0) { + val = -32768.0; + r->clipped++; + } + s = (short)(val < 0.0 ? val - 0.5 : val + 0.5); + r->data[n] = s; + { + unsigned long mag = s < 0 ? (unsigned long)(-(long)s) + : (unsigned long)s; + + if (mag > r->peak) r->peak = mag; + } + r->sumSq += (double)s * (double)s; + + acc += 1000UL; + if (acc >= AUD_RATE) { + acc -= AUD_RATE; + ms++; + } + } + return 0; +} + +static void PutLe16(FILE *f, unsigned long v) +{ + fputc((int)(v & 0xFFUL), f); + fputc((int)((v >> 8) & 0xFFUL), f); +} + +static void PutLe32(FILE *f, unsigned long v) +{ + PutLe16(f, v & 0xFFFFUL); + PutLe16(f, (v >> 16) & 0xFFFFUL); +} + +/* Canonical 44-byte PCM WAV header, mono 16-bit 44100 Hz. */ +static int WriteWav(const char *path, const short *data, unsigned long n) +{ + FILE *f; + unsigned long i, bytes = n * 2UL; + + f = fopen(path, "wb"); + if (!f) return 1; + fwrite("RIFF", 1, 4, f); + PutLe32(f, 36UL + bytes); + fwrite("WAVEfmt ", 1, 8, f); + PutLe32(f, 16UL); + PutLe16(f, 1UL); + PutLe16(f, 1UL); + PutLe32(f, AUD_RATE); + PutLe32(f, AUD_RATE * 2UL); + PutLe16(f, 2UL); + PutLe16(f, 16UL); + fwrite("data", 1, 4, f); + PutLe32(f, bytes); + for (i = 0; i < n; i++) + PutLe16(f, (unsigned long)(unsigned short)data[i]); + return fclose(f) != 0; +} + +static int WriteScene(const char *dir, unsigned scene) +{ + char path[PATH_LEN]; + RENDER *d = &result[scene][0]; + RENDER *c = &result[scene][1]; + unsigned long silence = SILENCE_MS * AUD_RATE / 1000UL; + unsigned long k; + short *ab; + + sprintf(path, "%s/%s_default.wav", dir, scene_names[scene]); + if (WriteWav(path, d->data, d->n)) return 1; + sprintf(path, "%s/%s_chamber.wav", dir, scene_names[scene]); + if (WriteWav(path, c->data, c->n)) return 1; + + ab = (short *)malloc((d->n + silence + c->n) * sizeof(short)); + if (!ab) return 1; + memcpy(ab, d->data, d->n * sizeof(short)); + memset(ab + d->n, 0, silence * sizeof(short)); + memcpy(ab + d->n + silence, c->data, c->n * sizeof(short)); + sprintf(path, "%s/%s_ab.wav", dir, scene_names[scene]); + k = WriteWav(path, ab, d->n + silence + c->n); + free(ab); + if (k) return 1; + + diffs[scene] = 0UL; + for (k = 0; k < d->n; k++) + if (d->data[k] != c->data[k]) diffs[scene]++; + return 0; +} + +static int WriteReport(const char *dir) +{ + char path[PATH_LEN]; + FILE *f; + unsigned s, b; + + sprintf(path, "%s/report.txt", dir); + f = fopen(path, "w"); + if (!f) return 1; + + fprintf(f, "psgaudit report\n\n"); + fprintf(f, "Host-rendered approximation, NOT real Tandy 1000 output.\n\n"); + fprintf(f, "Engine: real synth, sound/MIDISYN.C and sound/INSTR.C\n"); + fprintf(f, " (MSReset, MSBank, MSMessage, MSRender). Banks: default =\n"); + fprintf(f, " MS_BANK_DEFAULT (0), chamber = MS_BANK_CHAMBER (1).\n"); + fprintf(f, " Only MSBank differs between the takes of a scene.\n\n"); + fprintf(f, "Output stage (host model of an SN76496, not hardware):\n"); + fprintf(f, " 3 square tones at 3579545/(32*period) Hz, 2 dB steps\n"); + fprintf(f, " attenuation (15 = silent), 15-bit LFSR noise.\n"); + fprintf(f, " Noise setting:\n"); + fprintf(f, " bit 2 = white (1) or periodic (0), bits 0-1 = rate\n"); + fprintf(f, " div 16/32/64; setting 3 is treated as div 64 (unused).\n"); + fprintf(f, " Fixed gains, identical for both banks: tone %.2f, noise\n", + TONE_GAIN); + fprintf(f, " %.2f. No normalization; saturating clamp to int16 only.\n", + NOISE_GAIN); + fprintf(f, " Vibrato (presets 3-5) and percussion are bank-independent\n"); + fprintf(f, " in the synth: the banks differ only in melodic " + "envelopes.\n\n"); + fprintf(f, "Format: mono 16-bit PCM, 44100 Hz, 44-byte header.\n"); + fprintf(f, "Tail after last event: %lu ms. ab silence: %lu ms.\n\n", + TAIL_MS, SILENCE_MS); + + fprintf(f, "Scenes:\n"); + for (s = 0; s < SCENE_COUNT; s++) + fprintf(f, " %s: %s\n", scene_names[s], scene_desc[s]); + fprintf(f, "\n"); + + fprintf(f, "Event lists (identical for both banks):\n"); + for (s = 0; s < SCENE_COUNT; s++) { + SCENE_INFO *si = &infos[s]; + + fprintf(f, " %-8s noteOn %lu noteOff %lu perc %lu cc %lu " + "bend %lu prog %lu maxMelodic %lu checksum %08lX\n", + scene_names[s], si->noteOns, si->noteOffs, si->percussion, + si->controls, si->bends, si->programs, si->maxMelodic, + si->checksum); + } + fprintf(f, "\n"); + + fprintf(f, "Results (int16 units; full scale 32767):\n"); + fprintf(f, "%-8s %-8s %8s %6s %8s %10s %8s %8s %6s\n", + "scene", "bank", "samples", "peak", "rms", "preclamp", + "clipped", "rejected", "steal"); + for (s = 0; s < SCENE_COUNT; s++) { + for (b = 0; b < BANK_COUNT; b++) { + RENDER *r = &result[s][b]; + + fprintf(f, "%-8s %-8s %8lu %6lu %8.1f %10.1f %8lu %8lu %6lu\n", + scene_names[s], bank_names[b], r->n, r->peak, + SqrtD(r->sumSq / (double)r->n), + r->rawPeak * 32767.0, r->clipped, r->rejected, + r->steals); + } + } + fprintf(f, "\n"); + fprintf(f, "Samples that differ, default vs chamber:\n"); + for (s = 0; s < SCENE_COUNT; s++) + fprintf(f, " %-8s %lu\n", scene_names[s], diffs[s]); + fprintf(f, "\nFiles (in this directory):\n"); + for (s = 0; s < SCENE_COUNT; s++) + fprintf(f, " %s_default.wav %s_chamber.wav %s_ab.wav\n", + scene_names[s], scene_names[s], scene_names[s]); + fprintf(f, " report.txt\n"); + return fclose(f) != 0; +} + +int main(int argc, char **argv) +{ + unsigned scene, bank; + + if (argc != 2) { + fprintf(stderr, "usage: psgaudit \n"); + return 2; + } + for (scene = 0; scene < SCENE_COUNT; scene++) { + unsigned long n, lastMs; + + if (BuildScene(scene)) { + fprintf(stderr, "scene %s: bad event list\n", scene_names[scene]); + return 1; + } + if (scene == SC_PHRASE && infos[scene].maxMelodic > MAX_MELODIC) { + fprintf(stderr, "phrase: more than %d melodic notes at once\n", + MAX_MELODIC); + return 1; + } + lastMs = list.ev[list.count - 1].ms; + n = ((lastMs + TAIL_MS) * AUD_RATE) / 1000UL; + for (bank = 0; bank < BANK_COUNT; bank++) { + RENDER *r = &result[scene][bank]; + + if (PrepareRender(r, n) || RenderBank(bank, r)) { + fprintf(stderr, "scene %s: render failed\n", + scene_names[scene]); + return 1; + } + if (r->rejected) { + fprintf(stderr, "scene %s: %s bank rejected %lu events\n", + scene_names[scene], bank_names[bank], r->rejected); + return 1; + } + if (scene == SC_PHRASE && r->steals) { + fprintf(stderr, "phrase: %s bank stole %lu voices\n", + bank_names[bank], r->steals); + return 1; + } + } + if (WriteScene(argv[1], scene)) { + fprintf(stderr, "scene %s: write failed\n", scene_names[scene]); + return 1; + } + for (bank = 0; bank < BANK_COUNT; bank++) { + free(result[scene][bank].data); + result[scene][bank].data = NULL; + } + } + if (WriteReport(argv[1])) { + fprintf(stderr, "report write failed\n"); + return 1; + } + printf("wrote audition set to %s\n", argv[1]); + return 0; +}