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
2 changes: 1 addition & 1 deletion .github/workflows/rawlease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ jobs:
path=Path(name.replace('\\','/'))
assert hashlib.sha256(path.read_bytes()).hexdigest()==wanted, name
assert all(row['passed'] for row in receipt['results'])
assert {row['name'] for row in receipt['results']} == {'winamp','speaker','paired'}
assert {row['name'] for row in receipt['results']} == {'winamp','speaker','paired','fxmerge','rebase'}
print('PASS shared sound ownership source receipt')
PY
2 changes: 1 addition & 1 deletion docs/BUILD.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Host UI checks:
python libtandywin/TESTS/RUN.PY
Host production format, MOD and RAWCLK checks:
python libtandywin/AUDIO/HOST/RUNHOST.PY --out <new-output>
Host shared PSG/PIT ownership checks:
Host shared PSG/PIT ownership and pure FX state merger checks:
python libtandywin/AUDIO/HOST/RUNRAW.PY --out <new-output>
Host layout and typed facade checks:
python libtandywin/AUDIO/TESTS/FACADE/RUN.PY
Expand Down
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 <libtandy>/sound/tests/fxmerge.py --source libtandywin/AUDIO/SOURCE --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.
36 changes: 36 additions & 0 deletions docs/FX-ROLL.TXT
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Prepared burst clock rolling origin

The canonical libtandywin/AUDIO/SOURCE RAWCLK adds TCBRebase(DWORD amount),
ordinal37. Ordinals1-36 and frame-mode behavior are unchanged. This is an
owner-task operation for an active, non-ended, error-free burst session.
It also works while user-paused. It does not acquire another owner or timer.

Return0 means success;1 means wrong owner/mode/state;3 means zero or an
unsafe amount. The amount must not exceed the logical clock, latest published
stamp, last emitted deadline or last literal-burst write stamp. Validation and
all changes occur under one SaveIRQ/RestoreIRQ interval.

The API advances one DWORD burst origin and subtracts the amount from logical
clock/deadline fields. Published ring entries retain modulo32 physical stamps;
queuedAt subtracts the origin. It never walks the ring, writes sound, pauses,
restarts a timer, or changes the physical epoch/lastStamp. Flush/open reset the
origin. A consumer that never calls ordinal37 retains origin0.

The producer must preflight a phase-aligned rebase of its own cursor, then
adjust it and any prepared-but-unpublished timestamp immediately after the
successful call, without yielding between those operations. Keep all logical
timestamps in the original safe horizon. This does not automatically turn an
arbitrary producer into an unbounded session.

Beats uses a14400-state (four-minute) shift once its logical clock reaches
eight minutes.14400 preserves every supported integer tempo's eight-step
phase and the exact rational60Hz PIT clock. Rebase never resets a producer to
tick0, where an initial complete-state/noise attack would be inappropriate.

HOST/RUNRAW.PY includes actual RAWCLK mocks, a three-virtual-hour physical
DWORD-wrap test (44 rolls), pending FULL retry, partial/full queue, pause,
wrong owner/frame mode, and checks for no added PSG/PIT output at rebases.
Native timing, Win16 ABI and sound recordings are separate application evidence.

The distinct sound/TIMECLK.C compatibility producer is intentionally unchanged.
The matching OEMSound engine mirror supplies the same current Windows source.
6 changes: 3 additions & 3 deletions libtandywin/AUDIO/HOST/RUNRAW.PY
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ if os.name == 'nt':
rows = []
for name, source, extra in [('winamp', 'TEST_BURST.C', []), ('speaker', 'TEST_BURST.C', [
'-DCLOCK_SOURCE="../../../sound/TIMECLK.C"', '-DSPEAKER_CLOCK']),
('paired', 'TEST_PAIR.C', [])]:
('paired', 'TEST_PAIR.C', []), ('fxmerge', 'TEST_FX.C', []), ('rebase', 'TEST_REBASE.C', [])]:
exe = out / (name + ('.exe' if os.name == 'nt' else ''))
command = cc + ['-x', 'c', '-std=c89', '-Wall', '-Wextra',
'-I', str(HERE/'STUB'), *extra,
Expand All @@ -51,10 +51,10 @@ for name, source, extra in [('winamp', 'TEST_BURST.C', []), ('speaker', 'TEST_BU
'scope': 'Actual RAWCLK source, stub Win16/hardware; native IRQ timing separate',
'source_sha256': {
path.resolve().relative_to(HERE.parents[2].resolve()).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest()
for path in (HERE/'TEST_BURST.C', HERE/'TEST_PAIR.C', HERE/'STUB/windows.h',
for path in (HERE/'TEST_BURST.C', HERE/'TEST_PAIR.C', HERE/'TEST_FX.C', HERE/'TEST_REBASE.C', HERE/'STUB/windows.h',
*(HERE/'../SOURCE'/name for name in
('TIMECLK.C', 'TIMECLK.H', 'PSGOWN.H', 'TCBURST.H',
'TCXBASS.H', 'PITLINK.H', 'WINHOOK.ASM', 'TIMECLK.DEF')),
'TCXBASS.H', 'PITLINK.H', 'WINHOOK.ASM', 'TIMECLK.DEF', 'FXMERGE.C', 'FXMERGE.H')),
*(HERE/'../../../sound'/name for name in
('TIMECLK.C', 'TIMECLK.H', 'PSGOWN.H', 'TCBURST.H',
'TCXBASS.H', 'TCNOISE.H', 'PITLINK.H')))},
Expand Down
30 changes: 30 additions & 0 deletions libtandywin/AUDIO/HOST/TEST_FX.C
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include "../SOURCE/FXMERGE.C"
/* SPDX-License-Identifier: MIT */
#include <stdio.h>
#include <string.h>
#include "../SOURCE/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;
}
48 changes: 48 additions & 0 deletions libtandywin/AUDIO/HOST/TEST_REBASE.C
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* Actual RAWCLK, three virtual hours, physical DWORD wraps and no extra I/O. */
#define SPEAKER_CLOCK 1
#define main BurstBaselineMain
#include "TEST_BURST.C"
#undef main
int main(void)
{
TCXBURST b;unsigned long i;DWORD period=286363200UL,physical,logical,oldBase;
WORD n,rebases=0,oldTrace,oldNoise,oldPit,oldWrites;
C(BurstBaselineMain()==0);C(TCBRebase(1)==1);
C(TCXOpen(1)==0);now=0;memset(&b,0,sizeof(b));b.count=1;b.data[0]=0x90;b.pitDivisor=1000;
C(TCXPush(&b)==0);b.at=119318UL;b.count=0;b.pitDivisor=1001;C(TCXPush(&b)==0);
C(TCStart()==0);tick(0);C(TCBRebase(0)==3);
for(i=1;i<=108000UL;++i){
tick((DWORD)(i*119318UL));C(!error&&count==0);C(bassLast==1000+(i&1));
b.at=TCClock()+119318UL;b.pitDivisor=(WORD)(1000+((i+1)&1));C(TCXPush(&b)==0);
if(TCClock()>=period*2UL){
physical=bursts[head].at;logical=queuedAt(head);oldBase=burstBase;
oldTrace=traceCount;oldWrites=writes;oldNoise=noiseWrites;oldPit=pitDivisor;n=count;
C(TCBRebase(clockCycles+1)==3);C(burstBase==oldBase);
task=2;C(TCBRebase(period)==1);task=1;
C(TCBRebase(period)==0);++rebases;
C(burstBase==(DWORD)(oldBase+period)&&bursts[head].at==physical);
C(queuedAt(head)==logical-period&&count==n&&traceCount==oldTrace&&writes==oldWrites);
C(noiseWrites==oldNoise&&pitDivisor==oldPit&&hi&&owner);
}
}
C(rebases>=40&&burstBase!=0);C(TCPause()==0);oldTrace=traceCount;oldWrites=writes;
physical=bursts[head].at;logical=queuedAt(head);C(TCBRebase(1000)==0);
C(!hi&&!pitDivisor&&traceCount==oldTrace&&writes==oldWrites&&bursts[head].at==physical&&queuedAt(head)==logical-1000);
C(TCUnpause()==0);C(TCClose()==0);C(!owner&&!sound&&!pitToken&&!unsafe);
C(TCBOpen()==0&&burstBase==0);C(TCClose()==0);
/* A pending FULL retry and the published ring retain exact deadlines. */
C(TCBOpen()==0);memset(&b,0,sizeof(b));b.count=1;b.data[0]=0xf8;
for(i=0;i<TCB_CAPACITY;++i){b.at=(DWORD)i*TCB_MIN_CYCLES;C(TCBPush((TCBURST *)&b)==0);}
now=0;C(TCStart()==0);tick(0);
b.at=(DWORD)TCB_CAPACITY*TCB_MIN_CYCLES;C(TCBPush((TCBURST *)&b)==0);
b.at+=TCB_MIN_CYCLES;C(TCBPush((TCBURST *)&b)==2);
tick(TCB_MIN_CYCLES);C(count==TCB_CAPACITY-1);
physical=bursts[head].at;logical=queuedAt(head);oldWrites=writes;
C(TCBRebase(5000UL)==0);b.at-=5000UL;
C(bursts[head].at==physical&&queuedAt(head)==logical-5000UL&&writes==oldWrites);
C(TCBPush((TCBURST *)&b)==0&&count==TCB_CAPACITY);
b.at+=TCB_MIN_CYCLES;C(TCBPush((TCBURST *)&b)==2);C(TCClose()==0);
C(TCOpen()==0);oldBase=burstBase;C(TCBRebase(1)==1&&burstBase==oldBase);C(TCClose()==0);
printf("PASS rolling RAWCLK three-hour/wrap/ownership checks=%d failures=%d rebases=%u\n",checks,failures,rebases);
return failures!=0;
}
32 changes: 32 additions & 0 deletions libtandywin/AUDIO/SOURCE/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 libtandywin/AUDIO/SOURCE/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
2 changes: 2 additions & 0 deletions libtandywin/AUDIO/SOURCE/TCBURST.H
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ WORD TCAPI TCBGetCaps(WORD,TCBINFO FAR *,WORD);
WORD TCAPI TCBOpen(void);
WORD TCAPI TCBPush(TCBURST FAR *);
WORD TCAPI TCBEnd(void);
/* Optional ordinal37: shift active burst clock/deadlines without sound writes. */
WORD TCAPI TCBRebase(DWORD);
/* 0 success,1 ownership/lifecycle,2 FULL,3 invalid,4 spacing/horizon. */
#endif
23 changes: 18 additions & 5 deletions libtandywin/AUDIO/SOURCE/TIMECLK.C
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ HTASK owner=0;static HTASK probeOwner;
WORD timer=0,probeTimer=0,head=0,tail=0,count=0,end=0,active=0,finished=0,error=0;
static DWORD epoch,published,lastDue,lastStamp,clockCycles;
static WORD burstMode,burstLatch,burstRegs[8],burstPublished;
static DWORD burstLastWrite;
static DWORD burstLastWrite,burstBase;
static WORD noiseWrites,burstHash,burstWriteCount;
static WORD bassQueue[TC_CAP],bassMode,bassEnabled,bassLast,bassSuspended;
static WORD bassStatus=TCX_UNAVAILABLE;
Expand Down Expand Up @@ -95,7 +95,7 @@ static void bassRelease(void){
bassLease.token=0;bassGet=0;bassMode=bassEnabled=bassLast=bassSuspended=0;bassStatus=TCX_UNAVAILABLE;
pairMode=pairUsingFold=0;
}
static DWORD queuedAt(WORD i){return burstMode?bursts[i].at:queue[i].at;}
static DWORD queuedAt(WORD i){return burstMode?bursts[i].at-burstBase:queue[i].at;}
static WORD queueMask(void){return burstMode?TCB_CAPACITY-1:TC_CAP-1;}
static WORD wideClock,userPaused;
static WORD due(DWORD at,DWORD now){return wideClock?(LONG)(now-at)>=0:now>=at;}
Expand Down Expand Up @@ -197,7 +197,7 @@ WORD TCAPI TCOpen(void){WORD flags;
if(OpenSound()<0)return 1;
owner=GetCurrentTask();if(!owner){CloseSound();return 1;}StopSound();
if(rawClaim()){CloseSound();owner=0;return 1;}
flags=SaveIRQ();head=tail=count=end=active=finished=error=0;calls=sent=maxBatch=maxLate=maxGap=overflows=writes=underruns=0;published=lastDue=0;lastValid=restorePending=wideClock=userPaused=burstMode=burstPublished=noiseWrites=0;
flags=SaveIRQ();head=tail=count=end=active=finished=error=0;calls=sent=maxBatch=maxLate=maxGap=overflows=writes=underruns=0;published=lastDue=burstBase=0;lastValid=restorePending=wideClock=userPaused=burstMode=burstPublished=noiseWrites=0;
bassMode=bassEnabled=bassLast=bassSuspended=pairMode=pairUsingFold=0;
bassStatus=TCX_UNAVAILABLE;mute();RestoreIRQ(flags);return 0;
}
Expand Down Expand Up @@ -270,7 +270,7 @@ WORD TCAPI TCUnpause(void){WORD r;if(!owns()||!userPaused)return 1;userPaused=0;
WORD TCAPI TCFlush(void){WORD flags,n;if(!owns())return 1;
if(timer&&HiMetric(9)&&HiStop())return 1;
for(n=0;timer&&HiStatus()&&n<4000;n++)Yield();if(timer&&HiStatus())return 1;
flags=SaveIRQ();active=0;userPaused=0;restorePending=lastValid=0;head=tail=count=end=finished=error=0;published=lastDue=clockCycles=lastStamp=burstLastWrite=0;burstPublished=0;epoch=0xffffffffUL;sent=0;bassLast=pairUsingFold=0;mute();RestoreIRQ(flags);return 0;}
flags=SaveIRQ();active=0;userPaused=0;restorePending=lastValid=0;head=tail=count=end=finished=error=0;published=lastDue=clockCycles=lastStamp=burstLastWrite=burstBase=0;burstPublished=0;epoch=0xffffffffUL;sent=0;bassLast=pairUsingFold=0;mute();RestoreIRQ(flags);return 0;}
WORD TCAPI TCRearm(void){WORD r;if(!owns()||!timer||active||!count)return 1;
active=1;HiPrepare(nextInterval(0));epoch=0xffffffffUL;lastStamp=clockCycles=0;r=HiRestart();if(r)active=0;return r;}

Expand Down Expand Up @@ -313,7 +313,7 @@ static WORD burstPush(TCBURST FAR *source,WORD divisor,WORD extended){WORD f,t,i
if(at>2147483647UL||((count||active||sent)&&at<published))return 4;
if(burstPublished&&at-burstLastWrite<TCB_MIN_CYCLES)return 4;
f=SaveIRQ();if(count==TCB_CAPACITY){overflows++;RestoreIRQ(f);return 2;}t=tail;RestoreIRQ(f);
bursts[t].at=at;bursts[t].count=source->count;bassQueue[t]=divisor;
bursts[t].at=at+burstBase;bursts[t].count=source->count;bassQueue[t]=divisor;
for(i=0;i<source->count;i++)bursts[t].data[i]=source->data[i];
f=SaveIRQ();if(error||end||(active&&at<clockCycles)){RestoreIRQ(f);return 4;}
burstLastWrite=at;burstPublished=1;
Expand All @@ -324,6 +324,19 @@ WORD TCAPI TCBEnd(void){WORD f;if(!owns()||!burstMode)return 1;
end=1;if(!count){finished=1;active=0;mute();}RestoreIRQ(f);return 0;
}

/* Owner-task rolling origin. O(1) under the IRQ lock: queued stamps retain
their modulo32 physical value; queuedAt subtracts the shared origin. No
PSG/PIT write, pause, parsing or queue walk occurs here. */
WORD TCAPI TCBRebase(DWORD amount){WORD f;
if(!owns()||!burstMode||!timer||!active||end||error)return 1;
if(!amount)return 3;f=SaveIRQ();
if(!active||end||error){RestoreIRQ(f);return 1;}
if(amount>clockCycles||amount>published||amount>lastDue||
(burstPublished&&amount>burstLastWrite)){RestoreIRQ(f);return 3;}
burstBase+=amount;clockCycles-=amount;published-=amount;lastDue-=amount;
if(burstPublished)burstLastWrite-=amount;
RestoreIRQ(f);return 0;
}
WORD TCAPI TCBPush(TCBURST FAR *s){return burstPush(s,0,0);}

WORD TCAPI TCXGetCaps(WORD version,TCXINFO FAR *out,WORD bytes){WORD status;
Expand Down
1 change: 1 addition & 0 deletions libtandywin/AUDIO/SOURCE/TIMECLK.DEF
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ EXPORTS
TCXPairCaps @34
TCXPairPush @35
TCXPairLive @36
TCBRebase @37
IMPORTS
CreateSystemTimer=SYSTEM.2
KillSystemTimer=SYSTEM.3
Loading
Loading