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
44 changes: 44 additions & 0 deletions docs/FX-MERGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Prepared effects and music

`sound/FXMERGE.H` and `FXMERGE.C` provide a pure C89 state merger and delta
encoder. They own no timer, hardware, memory allocation, or sound claim.
The application supplies a validated base PSG/PIT state and five effect
slots: tone0, tone1, tone2, noise, and speaker. Periods are 1..1023,
attenuation is 0..15, noise mode is 0..7, and PIT is 0 (off) or 151..65535.
Inactive slots restore the current base state. A new trigger replaces its
lane's prior effect; cursor, retrigger and clock policy belong to the producer.

Pitched noise sets `borrow2` in slot3. It wins over both music and an effect
on tone2, sets that divider, and mutes tone2's audible output. The reservation
continues during gated/silent rows. When the noise slot becomes inactive,
tone2's current state returns, including a tone effect that continued advancing
while hidden. Fixed-rate noise leaves tone2 alone. An active speaker slot
replaces the current base PIT divisor, including zero for a gate-off row.

`FXMMerge(out,base,layers)` produces a complete state. `FXMBytes` compares it
with the last emitted state and writes at most 11 bytes into a 16-byte buffer.
Set `initial` only at whole-owner startup. Set `noiseAttack` for an actual
noise retrigger, including restoration of a sounding base noise voice.
Other updates write the noise level without resetting the LFSR. Buffer and
input objects must not overlap; out may alias base. Inputs must be validated.

An owning application should merge first, publish the resulting timestamped
PSG/PIT writes to its existing transport, then cache the complete state.
Never insert Effects Lab `FXBytes`' initial whole-PSG mute sequence into an
individual song lane. The compiler's trajectory rows can feed the slots
directly. These are synthesized PSG/PIT effects; there is no PCM layer.

OEMSound Beats Lab supplies the application producer, TFX1 patch import,
per-step transposition and DWA2 song persistence. libwintandy's
`libtandywin/AUDIO/SOURCE/FXMERGE.*` is the matching Windows source mirror.
Existing MIDI banks, driver APIs, and transport ordinals are unchanged.

Run the standalone check on Windows with the managed GCC launcher:

```
python sound/tests/fxmerge.py --out <new-private-output>
```

The merger uses 78 bytes of state/layers on a 16-bit target, plus the
caller's previous state and output buffer. It contains no tables. Host checks
do not establish native code size, IRQ timing, or audible quality.
32 changes: 32 additions & 0 deletions sound/FXMERGE.C
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* SPDX-License-Identifier: MIT */
#include "FXMERGE.H"
void FXMMerge(FXMSTATE *out,const FXMSTATE *base,const FXMLAYER *fx)
{
unsigned i;*out=*base;
for(i=0;i<3;++i)if(fx[i].active){
out->period[i]=fx[i].period;out->level[i]=fx[i].level;
}
if(fx[3].active){
out->noise=fx[3].noise;out->level[3]=fx[3].level;
if(fx[3].borrow2){out->period[2]=fx[3].period;out->level[2]=15;}
}
if(fx[4].active)out->pit=fx[4].pit;
}
unsigned FXMBytes(const FXMSTATE *s,const FXMSTATE *old,unsigned initial,
unsigned noiseAttack,unsigned char *b)
{
unsigned i,n=0,p;
for(i=0;i<3;++i){p=s->period[i];
if(initial||p!=old->period[i]){
b[n++]=(unsigned char)(0x80|(i<<5)|(p&15));
b[n++]=(unsigned char)(p>>4);
}
if(initial||s->level[i]!=old->level[i])
b[n++]=(unsigned char)(0x90|(i<<5)|s->level[i]);
}
if(initial||noiseAttack||s->noise!=old->noise)
b[n++]=(unsigned char)(0xe0|s->noise);
if(initial||s->level[3]!=old->level[3])
b[n++]=(unsigned char)(0xf0|s->level[3]);
return n;
}
20 changes: 20 additions & 0 deletions sound/FXMERGE.H
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* SPDX-License-Identifier: MIT. Pure C89 PSG/PIT state merger. No IO/clock. */
#ifndef FXMERGE_H
#define FXMERGE_H
typedef struct {
unsigned period[3],level[4],noise,pit;
} FXMSTATE;
/* Slots 0..2 are tones, 3 noise, 4 speaker. Inactive slots restore base.
borrow2 is valid only for noise: it owns tone2's divider AND mutes tone2.
It wins over slot2 until inactive, even during a gated/silent noise row.
A speaker slot replaces base.pit. Retrigger/cursor ownership is caller's. */
typedef struct {
unsigned active,period,level,noise,pit,borrow2;
} FXMLAYER;
void FXMMerge(FXMSTATE *,const FXMSTATE *,const FXMLAYER *);
/* Delta encoder: at most 11 bytes. Noise latch written only on mode change,
explicit noise attack, or initial state; level-only rows never reset LFSR.
initial is a whole-owner start, never a per-effect global mute prefix. */
unsigned FXMBytes(const FXMSTATE *,const FXMSTATE *,unsigned,unsigned,
unsigned char *);
#endif
29 changes: 29 additions & 0 deletions sound/tests/fxmerge.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include "FXMERGE.H"
static unsigned checks;
#define CK(x) do{++checks;if(!(x)){printf("FAIL %u %s\n",(unsigned)__LINE__,#x);return 1;}}while(0)
int main(void)
{
FXMSTATE base,out,old;FXMLAYER fx[5];unsigned i,n;unsigned char bytes[16];
memset(&base,0,sizeof(base));memset(fx,0,sizeof(fx));
for(i=0;i<3;++i){base.period[i]=100+i;base.level[i]=i+2;}
base.noise=4;base.level[3]=6;base.pit=21000;
FXMMerge(&out,&base,fx);CK(!memcmp(&out,&base,sizeof(out)));
fx[0].active=1;fx[0].period=400;fx[0].level=1;
fx[2].active=1;fx[2].period=500;fx[2].level=3;
fx[3].active=fx[3].borrow2=1;fx[3].period=600;fx[3].level=2;fx[3].noise=7;
fx[4].active=1;fx[4].pit=1234;FXMMerge(&out,&base,fx);
CK(out.period[0]==400&&out.level[0]==1&&out.period[1]==101&&out.level[1]==3);
CK(out.period[2]==600&&out.level[2]==15&&out.level[3]==2&&out.noise==7&&out.pit==1234);
fx[3].level=15;FXMMerge(&out,&base,fx);CK(out.level[2]==15);
fx[3].active=fx[4].active=0;FXMMerge(&out,&base,fx);
CK(out.period[2]==500&&out.level[2]==3&&out.noise==4&&out.pit==21000);
old=out;CK(!FXMBytes(&out,&old,0,0,bytes));
out.level[3]=4;n=FXMBytes(&out,&old,0,0,bytes);CK(n==1&&bytes[0]==0xf4);
n=FXMBytes(&out,&old,0,1,bytes);CK(n==2&&bytes[0]==0xe4&&bytes[1]==0xf4);
n=FXMBytes(&out,&old,1,0,bytes);CK(n==11);
for(i=0;i<3;++i)CK(bytes[i*3+2]!=(0x9f|(i<<5)));
printf("PASS shared FX merger checks=%u\n",checks);return 0;
}
16 changes: 16 additions & 0 deletions sound/tests/fxmerge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Standalone pure C89 merger checks, also usable against a Windows mirror."""
from pathlib import Path
import argparse, hashlib, json, os, subprocess, sys
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--source',type=Path,default=Path(__file__).resolve().parents[1])
p.add_argument('--out',type=Path,required=True)
a=p.parse_args();out=a.out.resolve();out.mkdir(parents=True,exist_ok=False)
source=a.source.resolve();test=Path(__file__).with_suffix('.c')
cc=[sys.executable,'C:/Users/chad/Documents/Codex/toolchains/gcc-ucrt64.py','--'] if os.name=='nt' else ['cc']
env=dict(os.environ)
if os.name=='nt':env['PATH']='C:/msys64/ucrt64/bin;'+env.get('PATH','')
cmd=cc+['-x','c','-std=c89','-pedantic-errors','-Wall','-Wextra','-Werror','-I',str(source),str(source/'FXMERGE.C'),str(test),'-o',str(out/'fxmerge.exe')]
subprocess.run(cmd,check=True,env=env,timeout=30)
r=subprocess.run([str(out/'fxmerge.exe')],check=True,capture_output=True,text=True,env=env,timeout=10)
(out/'RESULT.JSON').write_text(json.dumps(dict(status='PASS',result=r.stdout.strip(),sources={str(s):hashlib.sha256(s.read_bytes()).hexdigest() for s in (source/'FXMERGE.C',source/'FXMERGE.H',test)}),indent=2))
print(r.stdout.strip())
1 change: 1 addition & 0 deletions sound/tests/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def main():
out.mkdir(parents=True, exist_ok=True)
for name, extra, defs in (('portable_midi', ['SMF.C'], []),
('bank_test', [], []),
('fxmerge', ['FXMERGE.C'], []),
('trace_default', [], []),
('trace_default', [], ['-DWITH_BANK'])):
tag = name + ('_bank' if defs else '')
Expand Down
Loading