diff --git a/.gitignore b/.gitignore
index 302a40ec..78895914 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ just-stuff/
dvx.log
claudeDocs/
toolchains/
+dist/
diff --git a/Makefile b/Makefile
index 18a6b20a..84f4dab9 100644
--- a/Makefile
+++ b/Makefile
@@ -39,9 +39,9 @@
LIBS = $(sort $(notdir $(patsubst %/Makefile,%,$(wildcard src/libs/kpunch/*/Makefile))))
CORE_LIBS = libdvx libtasks
-.PHONY: all clean $(LIBS) loader widgets apps tools deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-samples coverage
+.PHONY: all clean $(LIBS) loader widgets apps tools installer dist deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-samples coverage
-all: $(LIBS) loader tools widgets apps deploy-helpsrc compile-help deploy-sdk
+all: $(LIBS) loader tools widgets apps installer deploy-helpsrc compile-help deploy-sdk
$(LIBS):
$(MAKE) -C src/libs/kpunch/$@
@@ -60,6 +60,17 @@ widgets: libdvx libtasks texthelp listhelp tools
tools:
$(MAKE) -C src/tools
+# SETUP.EXE (bin/setup.exe, a static build of the core it needs) and the
+# package tool bin/host/dvxpkg.
+installer:
+ $(MAKE) -C src/installer
+
+# Release media in dist/: the CD image and floppy images (FLOPPY=360K,
+# 720K, 1.2M, 1.44M (default), 2.88M or a number of bytes per disk).
+# See src/installer/mkDist.sh.
+dist: all
+ src/installer/mkDist.sh $(or $(FLOPPY),1.44M)
+
apps: libdvx libtasks dvxshell tools
$(MAKE) -C src/apps/kpunch
@@ -172,7 +183,7 @@ deploy-sdk: tools apps
@echo "Building SDK..."
@# Start from an empty tree so a header dropped from the lists goes away.
rm -rf $(SDKDIR)
- @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/fileops $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
+ @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/fileops $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
cp $(SDK_CORE_H) $(SDKDIR)/include/core/
cp $(SDK_SHELL_H) $(SDKDIR)/include/shell/
cp $(SDK_TASKS_H) $(SDKDIR)/include/tasks/
@@ -183,6 +194,10 @@ deploy-sdk: tools apps
cp $(SDK_XFER_H) $(SDKDIR)/include/xfer/
cp $(SDK_CONN_H) $(SDKDIR)/include/conn/
cp $(SDK_PRINT_H) $(SDKDIR)/include/print/
+ cp $(SDK_GAME_H) $(SDKDIR)/include/game/
+ cp $(SDK_JOYSTICK_H) $(SDKDIR)/include/joystick/
+ cp $(SDK_AUDIO_H) $(SDKDIR)/include/audio/
+ cp $(SDK_MUSIC_H) $(SDKDIR)/include/music/
@# Networking: dvxNet.h and the standard socket headers, which keep their
@# subdirectories (sys/, netinet/, arpa/); sdk.mk adds include/net only
@# for programs that set NET = 1.
@@ -225,6 +240,7 @@ clean:
$(MAKE) -C src/apps/kpunch clean
$(MAKE) -C src/tools clean
$(MAKE) -C src/tools/proxy clean
- rm -rf obj bin/host bin/test bin/system
+ $(MAKE) -C src/installer clean
+ rm -rf obj bin/host bin/test bin/system dist
rm -rf bin/config bin/widgets bin/libs bin/sdk
rm -f docs/*.html
diff --git a/README.md b/README.md
index 263e1925..936e2a20 100644
--- a/README.md
+++ b/README.md
@@ -89,12 +89,16 @@ DOSBox Staging instead (much faster); `SCRIPTS="boot apps"` runs a
subset. The emulators never touch your home directory. See
`src/test/dos/runDos.sh --help`.
-`./mkcd.sh` builds everything, then wraps `bin/` into an ISO 9660
-image for use with an emulator or for burning to physical media. On
-the target machine, change to the CD drive and type `INSTALL`: DVX
-starts from the CD with its Setup program, which copies DVX to a
-hard-disk folder (and can add it to AUTOEXEC.BAT). Holding Shift while
-DVX starts (or `DVX /SAFE`) starts it in 640x480 safe video mode.
+`make dist` builds the release media in `dist/`: a CD image
+(`DVX.ISO`: `SETUP.EXE` with the DVX package appended, and
+`README.TXT`) and a set of floppy images (`make dist FLOPPY=720K`;
+360K, 720K, 1.2M, 1.44M (default), 2.88M or a number of bytes per
+disk). On the target machine, type `SETUP` on the CD or Disk 1: Setup
+installs DVX in a hard-disk folder (and can add it to AUTOEXEC.BAT);
+the `SETUP.EXE` it leaves in that folder uninstalls. `./mkcd.sh` wraps
+`bin/` as it is into an ISO image, for running DVX straight from a CD
+in an emulator. Holding Shift while DVX starts (or `DVX /SAFE`) starts
+it in 640x480 safe video mode.
## Directory Layout
@@ -104,6 +108,7 @@ dvxgui/
assets/ logo and splash artwork
bin/ build output (runtime tree; mirrors what ships)
config/ shipped INI, themes, wallpapers
+ dist/ release media from `make dist` (not in git)
docs/ HTML exports of the built-in help content
LICENSE.txt MIT license
installToolchain.sh fetches the toolchain and test tools into toolchains/
@@ -112,6 +117,7 @@ dvxgui/
src/
apps/ bundled applications
include/ shared include files (BASIC library declarations)
+ installer/ SETUP.EXE, the package tool and make dist
libs/ core libraries (GUI, tasks, shell, helpers, serial, SQL)
loader/ bootstrap executable
sdk/ SDK project templates, build rules and newProject.sh
diff --git a/config/dvx.ini b/config/dvx.ini
index cdff7fb8..d56d0dc5 100644
--- a/config/dvx.ini
+++ b/config/dvx.ini
@@ -75,3 +75,58 @@ tabWidth = 8
wrap = true
header = &f
footer = Page &p
+
+; Sound settings.
+; driver: auto (find the sound hardware), sb (Sound Blaster family),
+; speaker (PC speaker) or null (no sound)
+; rate: mixing rate in Hz (4000-48000, default 22050); the sound card may
+; run slightly faster or slower
+; bits, channels: 0 = the best the sound card can do, or 8 / 16 bits and
+; 1 (mono) / 2 (stereo) to save work on a slow computer
+; voices: sounds mixed at once (1-32, default 16); when more are started
+; the quietest stops
+; volume: master volume in percent (0-100)
+; speakerPcm: true = play sampled sound on the PC speaker (takes a lot of
+; the computer's time); false = tones only
+; fmTones: true = square and triangle tones play on the music synthesizer
+; chip when there is one; false = always mixed
+; midi: where MIDI music plays: auto (the synthesizer chip, else a
+; built-in synthesizer), mpu401 (an external synthesizer on the
+; MIDI port), fm (the chip) or software (the built-in synthesizer)
+; mpuMode: what is on the MIDI port: gm (a General MIDI synthesizer),
+; mt32 (a Roland MT-32) or raw (send music unchanged)
+; Optional: oplPort (default 388) and mpuPort (default 330), in hex;
+; midiBank = the instrument bank file for MIDI music (OP2 format, path
+; from the DVX folder; default libs\kpunch\music\genmidi.op2)
+; The Sound Blaster's port, IRQ and DMA come from the BLASTER setting (for
+; example SET BLASTER=A220 I5 D1 H5 in AUTOEXEC.BAT).
+
+[audio]
+driver = auto
+rate = 22050
+bits = 0
+channels = 0
+voices = 16
+volume = 100
+speakerPcm = false
+fmTones = true
+midi = auto
+mpuMode = gm
+
+; Desktop sounds (Control Panel's Sounds page): the sound file (WAV or VOC,
+; path from the DVX folder) played for each event; empty = no sound.
+; default: the default beep (also message boxes without an icon)
+; startup, exit: DVX starting and ending
+; error, warning, question, information: message boxes of that kind
+; windowOpen, windowClose: a window opening or closing (not dialogs)
+
+[sounds]
+default = config/sounds/beep.wav
+startup = config/sounds/startup.wav
+exit = config/sounds/exit.wav
+error = config/sounds/error.wav
+warning = config/sounds/warning.wav
+question = config/sounds/question.wav
+information = config/sounds/info.wav
+windowOpen =
+windowClose =
diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html
index 0ae484ea..b969b73e 100644
--- a/docs/dvx_basic_reference.html
+++ b/docs/dvx_basic_reference.html
@@ -77,6 +77,7 @@ img { max-width: 100%; }
A SUB or FUNCTION may use at most 64 local slots (parameters, local variables and hidden temporaries). More is a compile error "Too many local variables in X (n, limit is 64)" instead of a runtime error 9; READ and INPUT # into many array elements no longer use a slot per target.
SHARED naming a variable that does not exist at module level creates it there; in a form's code it becomes a variable of that form, not a program-wide one.
+
Sound, Joysticks and Frames
+
+
New statements from QBasic: BEEP, SOUND frequency, duration (in clock ticks, 18.2 a second) and PLAY with the whole music macro language (notes, octaves, lengths, dots, pauses, tempo, MN/ML/MS, and MF/MB for foreground or background music), plus the PLAY(n) function (notes still waiting in the background). They play on whatever sound hardware the computer has, or silently when there is none.
+
PLAY's new I command plays the notes that follow on a General MIDI instrument (I1 to I128) through the music synthesizer; I0 goes back to tones.
+
New functions from QBasic: STICK(n) (joystick position, 1 to 200) and STRIG(n) (joystick buttons), on any joystick DVX knows, or the keyboard when there is none.
+
New statement FRAMEWAIT rate: waits for the next frame of a steady clock, keeping the desktop running, so a game loop runs at the same speed on every computer.
+
BEEP, SOUND, PLAY and FRAMEWAIT are now keywords, and STICK and STRIG built-in functions: a variable, SUB or FUNCTION with one of these names must be renamed.
+
Message boxes (MsgBox) play the desktop sound chosen for their kind on the Control Panel's Sounds tab.
@@ -1969,6 +2035,70 @@ RANDOMIZE TIMER ' Seed from system clock
Terminates program execution immediately. Any form that is loaded is unloaded before the program exits.
END
+
+
Sound, Joysticks and Frames
+
These statements work as they do in QBasic, on whatever sound hardware the computer has: a sound card, the PC speaker, or none at all, in which case the program runs the same but silently. Several programs can make sound at once. For sound files, tones of any shape and music files, see the sound.bas and music.bas include libraries; for more joystick detail, joystick.bas.
+
BEEP
+
Sounds a short beep (800 hertz for a quarter of a second).
+
BEEP
+
SOUND
+
Plays a tone of frequency hertz (37 to 32767) for duration clock ticks; there are 18.2 ticks in a second, and fractions are allowed. A duration of 0 stops every sound the program has queued. Frequencies too high to hear (above 20000) are silent. Anything out of range is error 5 (Illegal function call).
+
SOUND frequency, duration
+SOUND 440, 18.2 ' A for one second
+SOUND 440, 0 ' stop
+
PLAY
+
Plays a tune written in the music macro language. Letters may be upper or lower case and spaces are ignored. The settings (octave, length, tempo, style, instrument, foreground or background) stay from one PLAY to the next.
+
PLAY commands$
+PLAY "T160 O3 L8 E D C D E E E4"
+
Command Meaning
+ ------- -------
+ A to G Play the note in the current octave. Follow it with # or + for sharp, - for flat, then optionally a length (1 to 64, as for L) and dots.
+ N n Play note n (1 to 84, seven octaves from the lowest C); N0 is a rest.
+ O n Octave 0 to 6 (default 4). Octave 3 starts at middle C.
+ > and < Go up or down one octave.
+ L n Length of the notes that follow: 1 a whole note, 2 a half, 4 a quarter (the default) ... 64.
+ P n Pause (rest) of length n (1 to 64).
+ . A dot after a note or pause makes it half as long again; each further dot adds half again.
+ T n Tempo in quarter notes a minute (32 to 255, default 120).
+ MN Music normal: each note sounds for 7/8 of its length (the default).
+ ML Music legato: notes sound for their whole length.
+ MS Music staccato: notes sound for 3/4 of their length.
+ MF Music foreground: PLAY, SOUND and BEEP wait until the sound has finished (the default).
+ MB Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting.
+ I n DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default).
+
A string with a mistake in it plays nothing and raises error 5 (Illegal function call). QBasic's X command (play a string variable) and the "=" form of numbers are not supported; build the string with + instead, such as PLAY "O" + STR$(n). Without I, the notes are square-wave tones like the PC speaker's; with it, they play on the same synthesizer as MIDI music.
+
PLAY "MB T120 L4 CDEF GAB>C" ' a scale in the background
+PLAY "I74 O4 L8 GAB>C" ' on the flute
+
PLAY Function
+
PLAY(n) returns how many notes are still waiting to be played in the background (n is ignored). A game can use it to keep its music going:
+
If PLAY(0) < 4 Then PLAY "MB " + tune$
+
STICK and STRIG
+
STICK(n) returns where a joystick points, from 1 (all the way left or up) to 200 (all the way right or down), about 100 in the middle, or 0 when there is no such joystick. STICK(0) reads joystick A's X and remembers A's Y and joystick B's X and Y, which STICK(1), STICK(2) and STICK(3) then return; so call STICK(0) first.
+
STRIG(n) returns True (-1) or False (0) for the buttons:
+
n Returns True when
+ - -----------------
+ 0 joystick A's first button was pressed since the last STRIG(0)
+ 1 joystick A's first button is down
+ 2 joystick B's first button was pressed since the last STRIG(2)
+ 3 joystick B's first button is down
+ 4 joystick A's second button was pressed since the last STRIG(4)
+ 5 joystick A's second button is down
+ 6 joystick B's second button was pressed since the last STRIG(6)
+ 7 joystick B's second button is down
+
Joystick A is the first joystick; joystick B the second one, or the second stick of a joystick with four axes. When no joystick is connected the keyboard stands in for joystick A (see the Control Panel's Game Controllers tab). STRIG ON, STRIG OFF and ON STRIG are not supported; test STRIG in a Timer or a loop instead.
+
x = STICK(0)
+y = STICK(1)
+If STRIG(0) Then FireShot
+
FRAMEWAIT
+
Waits until the next frame of a steady clock of rate frames a second begins, keeping the desktop and the program's forms running meanwhile. A game loop that calls it once a frame runs at the same speed on every computer. Frames the program was too busy for are skipped, not caught up. rate must be at least 1; rates above 1000 count as 1000. Changing the rate starts the clock over.
flags is the sum of a button-style constant and an icon constant. See Predefined Constants for the complete list.
+
flags is the sum of a button-style constant and an icon constant. See Predefined Constants for the complete list. The message box plays the desktop sound chosen for its icon on the Control Panel's Sounds tab (the default beep without an icon).
MsgBox "Operation complete"
answer = MsgBox("Continue?", vbYesNo + vbQuestion, "Confirm")
If answer = vbYes Then
@@ -2534,7 +2664,11 @@ IniWrite App.Config & "\app.ini", "Display", "FontS
resource.bas Read and write DVX resource blocks (icons, text, binary data) in bundled files
help.bas Compile help source files and open the DVX help viewer
net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, ...), UDP messages and name lookups
- print.bas Printing: text with the page setup and pictures, with or without the Print dialog; the Page Setup dialog
+ print.bas Printing: text with the page setup and pictures, with or without the Print dialog; the Page Setup dialog
+ game.bas Games: whether a key is being held down right now (GameKeyDown)
+ joystick.bas Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...)
+ sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...)
+ music.bas Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, ...)
Using an Include Library
' With commdlg.bas added to the project
Dim path As String
@@ -2565,6 +2699,67 @@ Sub mnuPageSetup_Click ()
PrintPageSetup
End Sub
For printer output a line at a time, see LPRINT and OPEN "LPT1:".
+
Held Keys for Games
+
With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it:
+
Sub Timer1_Timer ()
+ If GameKeyDown(GAMEKEY_LEFT) Then shipX = shipX - 2
+ If GameKeyDown(GAMEKEY_RIGHT) Then shipX = shipX + 2
+End Sub
+
Joysticks
+
With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy) and JoyButtons(joy) describe the joystick.
+
Sub Timer1_Timer ()
+ If JoyCount() = 0 Then Exit Sub
+
+ shipX = shipX + JoyX(1) \ 8192
+ If JoyButton(1, 1) Then FireShot
+End Sub
+
When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.
With sound.bas in the project, a program plays sound files and tones through whatever sound hardware the computer has: a Sound Blaster, the PC speaker (tones only, unless sampled sound on the speaker is switched on) or nothing at all, in which case the program works the same but is silent. Several programs can play at once, and everything a program plays stops when it ends.
+
PlayWave(fileName) plays a WAV or VOC file once and PlayWaveLoop(fileName) over and over; PlayTone(frequency, milliseconds, volume) plays a tone of frequency hertz (20 to 20000) and PlayNoise(frequency, milliseconds, volume) noise, which sounds brighter the higher the frequency; a length of 0 plays until stopped. Each returns a voice number, or SOUND_FAILED (0) when the sound could not be played, and then SoundError$() says why. StopWave(voice) stops one sound, StopAllSounds() all of the program's sounds, and SoundPlaying(voice) is True while a sound is still playing. SoundVolume(volume) sets the volume of everything the program plays. Volumes are percent (0 to 100). SoundInfo$() describes the sound device, like "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo".
+
Dim Shared boom As Long
+
+Sub cmdFire_Click ()
+ boom = PlayWave("BOOM.WAV")
+
+ If boom = SOUND_FAILED Then
+ lblStatus.Caption = SoundError$()
+ End If
+End Sub
+
WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND and PLAY work too, without sound.bas.
With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound, and MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none. Several programs can play music at once, and everything a program plays stops when it ends.
+
MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.
+
Dim Shared tune As Long
+
+Sub Form_Load ()
+ tune = MusicLoad("THEME.MOD")
+
+ If tune = MUSIC_FAILED Then
+ lblStatus.Caption = MusicError$()
+ Else
+ MusicPlay tune, True
+ End If
+End Sub
+
A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a live music that is already playing. MusicNoteOn(music, channel, note, velocity) starts a note and MusicNoteOff(music, channel, note) ends it; MusicProgram(music, channel, program) chooses a channel's instrument, MusicControl(music, channel, controller, value) sets a controller (7 volume, 10 pan, 11 expression, 64 the sustain pedal, 123 all notes off) and MusicPitchBend(music, channel, bend) bends the channel's notes (-8192 to 8191, two semitones either way at the ends). Channels are 1 to 16, and channel 10 (MUSIC_DRUMS) plays drums, one per note. Notes, velocities, instruments and controller values are 0 to 127; middle C is note 60, and instruments are numbered as in General MIDI (0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute). The same calls work on a MIDI file that is playing.
+
Dim Shared keys As Long
+
+Sub Form_Load ()
+ keys = MusicMidiOpen()
+ MusicProgram keys, 1, 73
+End Sub
+
+Sub cmdC_Click ()
+ MusicNoteOn keys, 1, 60, 100
+ tmrNoteOff.Enabled = True
+End Sub
+
+Sub tmrNoteOff_Timer ()
+ MusicNoteOff keys, 1, 60
+ tmrNoteOff.Enabled = False
+End Sub
Network Connections
With net.bas in the project, a program talks to other computers over the network set up in the Control Panel. TcpConnect returns a handle (0 when it failed; NetError$ says why), TcpSend sends text, and TcpRecv$ returns whatever has arrived without waiting, so a Timer keeps the program responsive:
Dim conn As Long
diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html
index 4864ebaa..3e56c3df 100644
--- a/docs/dvx_system_reference.html
+++ b/docs/dvx_system_reference.html
@@ -86,6 +86,7 @@ img { max-width: 100%; }
The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network and the printer are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.
+
The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network, the printer, joysticks and the sounds are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.
Settings you keep are saved, and DVX uses them again the next time it starts.
+
DVX plays a short sound when it starts and when it ends, and when a message appears (a different one for errors, warnings, questions and information). Each sound can be changed or turned off, and the volume set, in the settings program.
Tip: If a new screen resolution leaves the screen unreadable, just wait: DVX goes back to the old one after 15 seconds unless you confirm the change.
@@ -1705,10 +1930,15 @@ img { max-width: 100%; }
If the screen resolution DVX is set to use does not work on your computer (for example after changing video cards), DVX starts in safe video mode instead: 640x480, which every video card can show. A message tells you so and asks whether to use this mode from now on. Answer Yes to keep it, or No to try the old resolution again at the next start.
If DVX starts but the screen cannot be read (the monitor cannot show the saved resolution), hold down a Shift key while DVX starts. DVX then starts in safe video mode and asks the same question. Typing DVX /SAFE at the DOS prompt does the same.
You can pick another resolution at any time with the settings program (see Changing Settings).
+
There is no sound
+
DVX finds the sound hardware by itself. A Sound Blaster needs its BLASTER setting in AUTOEXEC.BAT, for example SET BLASTER=A220 I5 D1 H5 (the card's manual gives the numbers). Without a sound card DVX uses the PC speaker, which plays tones and beeps but not recorded sounds. The settings program shows the sound hardware in use, lets you pick it yourself and sets the volume; if a sound is set to none there, nothing is heard for it. Without any sound hardware DVX and its programs work the same, silently.
An empty floppy or CD-ROM drive
When a program tries to read a drive that has no disk in it, or to write to a write-protected disk, the program simply reports that it could not read or write the file. DVX never stops at the DOS "Abort, Retry, Fail" question. Put the disk in and try again.
-
Running DVX from a CD-ROM or floppy disk
-
DVX can copy itself to your hard disk. Change to the drive and folder that hold DVX.EXE and type INSTALL. DVX starts and opens a window that asks for the folder to copy DVX to, and can make DVX start when the computer starts. Press F1 in that window for help with each step.
+
Installing, upgrading and removing DVX
+
DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. Setup shows the licence, asks for the folder to install DVX in, checks that there is room and unpacks DVX there, asking for each floppy disk in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.
+
To upgrade or repair DVX, run Setup again and choose the same folder: answer Yes to keep your settings. To remove DVX, run SETUP in the DVX folder (for example C:\DVX\SETUP) and choose Uninstall. Files you added to the folder yourself are not removed, and your settings are only removed if you ask.
+
Running DVX from a CD-ROM or a write-protected disk
+
DVX also runs straight from a disk it cannot write to. It then tells you once that changed settings last only until you exit DVX, and the help files are used as they are. To keep your settings, install DVX on your hard disk with Setup.
A program is already running
Most programs can only be open once. Use Alt+Tab or the Task Manager (Ctrl+Esc) to find its window. If you cannot see it, it may be minimized to an icon at the bottom of the screen.
"This program was built for a different version of DVX"
@@ -2250,11 +2480,11 @@ prefsClose(h);
Mouse Input
Polling model. platformMousePoll() returns position and button bitmask. Wheel support via CuteMouse API. platformMouseInit() can be called again after a video-mode change; it re-applies the speed, acceleration and wheel settings after the driver reset.
Keyboard Input
-
platformKeyboardRead() -- non-blocking key read; Shift/Ctrl/Alt+F1..F12 are reported as the plain F-key scancode (platformNormalizeFKeyScan). platformKeyUpRead() -- key release detection (requires INT 9 hook on DOS; its code and data are pinned with platformLockRegion). platformAltScanToChar() -- scancode-to-ASCII lookup for Alt+key combinations.
+
platformKeyboardRead() -- non-blocking key read; Shift/Ctrl/Alt+F1..F12 are reported as the plain F-key scancode (platformNormalizeFKeyScan). The keyboard hook (a chain on INT 9 on DOS; its code and data are pinned with platformLockRegion) decodes every key going down and up into a held-key table and a queue of raw events (platformKeyEventRead, platformKeyIsDown), which feed onKeyUp and game input windows. platformAltScanToChar() -- scancode-to-ASCII lookup for Alt+key combinations.
platformClock() / platformClockHz() -- monotonic UI tick counter. platformMicros() -- high-resolution monotonic clock (PIT, with the TSC between interrupts when there is one). The timer service owns the timer interrupt: callbacks at up to 1000 Hz (platformTimerAdd) while the BIOS keeps its 18.2 Hz tick. platformEntropyStamp() -- fine-grained timestamp (TSC, or BIOS tick + PIT counter) for input-timing entropy.
Crash Recovery
-
platformInstallCrashHandler() -- signal handlers + longjmp for fault tolerance. platformOutOfMemory() -- stb_ds allocation-failure hook; logs and raises SIGSEGV so it takes the same recovery path.
+
platformInstallCrashHandler() -- signal handlers + longjmp for fault tolerance. platformOutOfMemory() -- stb_ds allocation-failure hook; logs and raises SIGSEGV so it takes the same recovery path. platformAppExit() / platformAppAbort() -- exit() and abort() as loaded modules see them: the shell's hook ends only the calling program, by the same recovery path, without a fault message.
DXE Support
platformRegisterDxeExports() -- register C runtime and platform symbols for DXE resolution. platformRegisterSymOverrides() -- register function pointer overrides for module loader (SDK API for embedders and test harnesses; DVX itself does not call it).
@@ -2390,7 +2622,8 @@ prefsClose(h);
.lib, .wgt DVX_ABI_STAMP(baseName) exports <baseName>AbiVersion The loader, right after loading the module. A missing or different stamp stops the boot with a message naming the file (details in DVX.LOG).
.app AppDescriptorT.abiVersion, set by SHELL_APP_DESCRIPTOR The shell, before appMain. The user sees "This program was built for a different version of DVX and cannot run."; DVX.LOG has both versions.
Widget API WgtApiHeaderT.abiVersion, set by WGT_API_HEADER wgtGetApi (which also checks the table's size).
- Widget class WidgetClassT.version (WGT_CLASS_VERSION is DVX_ABI_VERSION) wgtRegisterClass.
+ Widget class WidgetClassT.version (WGT_CLASS_VERSION is DVX_ABI_VERSION) wgtRegisterClass.
+ Driver (.drv) DvxDriverHeaderT.abiVersion, set by DVX_DRIVER_HDR dvxDriversLoad, when the library of its device class loads it (a mismatched driver is skipped and logged).
Every library and widget module writes the stamp once at file scope in its main source file; the argument is the module file's base name (libdvx for LIBDVX.LIB, box for BOX.WGT):
char title[MAX_TITLE_LEN] Window title text (max 128 chars)
bool visible, focused, minimized, maximized, resizable, modal Window state flags
bool iconNeedsRefresh true when contentBuf changed (for minimized icon refresh)
+ bool gameInput Game input: see below
uint8_t paintNeeded PAINT_NONE / PAINT_PARTIAL / PAINT_FULL
int32_t maxW, maxH Maximum dimensions
int32_t preMaxX, preMaxY, preMaxW, preMaxH Saved geometry before maximize
@@ -2511,7 +2745,9 @@ DVX_ABI_STAMP(mylib);
onScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) Scrollbar value changed
int32_t onCursorQuery(WindowT *win, int32_t x, int32_t y) Return CURSOR_* for hit position
onFocus(WindowT *win) Window gained focus
- onBlur(WindowT *win) Window lost focus
+ onBlur(WindowT *win) Window lost focus
+ onRawKey(WindowT *win, const RawKeyEventT *evt) Key pressed or released (gameInput windows)
+
Game input: while a window with gameInput set has the keyboard focus, DVX keeps only Alt+Tab, Ctrl+Esc and Alt+F4 for itself. Every other keystroke, F1, F10, Alt+letter, Ctrl+F12, accelerators and hotkeys included, goes straight to onKey (except while a menu opened with the mouse, or the keyboard move/size mode, is taking keys). onRawKey then receives each key going down and up (RawKeyEventT: set-1 scancode, extended flag, repeat flag, modifiers from the held-key table), after the frame's onKey calls. When the focus moves away the window gets a release for every key still held (with no modifiers), and the held-key table starts over for the next window. onKeyUp keeps working as for any window. Mouse input is unchanged. The game library (game.h) adds gameKeyIsDown, frame pacing and timers.
WindowStackT
Z-ordered window stack (front-to-back: index count-1 is topmost). Owns system-wide drag/resize/scroll interaction state.
Field Description
@@ -2644,6 +2880,20 @@ DVX_ABI_STAMP(mylib);
KEY_CTRL_HOME Ctrl+Home (0x77 | KEY_EXT_FLAG)
Raw Scancodes
KEY_SCAN_* are the raw BIOS scancodes (without KEY_EXT_FLAG): KEY_SCAN_ESC, KEY_SCAN_TAB, KEY_SCAN_SPACE, KEY_SCAN_F1 .. KEY_SCAN_F12, KEY_SCAN_HOME, KEY_SCAN_END, KEY_SCAN_PGUP, KEY_SCAN_PGDN, KEY_SCAN_INSERT, KEY_SCAN_DELETE, KEY_SCAN_UP, KEY_SCAN_DOWN, KEY_SCAN_LEFT, KEY_SCAN_RIGHT, KEY_SCAN_CTRL_LEFT, KEY_SCAN_CTRL_RIGHT, KEY_SCAN_CTRL_END, KEY_SCAN_CTRL_HOME, KEY_SCAN_ALT_TAB.
+
Raw Key Codes
+
A raw key code names one physical key: its set-1 make code (1-127), plus KEY_RAW_EXT (0x80) for keys sent behind an 0xE0 prefix (grey arrows and navigation keys, right Ctrl and Alt, keypad Enter). KEY_RAW_CODE(scancode, extended) builds one. The code is the same whatever modifier is held; letters, digits and punctuation use their KEY_SCAN_* / BIOS scancode. Used by the held-key table (platformKeyIsDown, gameKeyIsDown) and RawKeyEventT.
+
Define Value Key
+ ------ ----- ---
+ KEY_RAW_LSHIFT, KEY_RAW_RSHIFT 0x2A, 0x36 Left / right Shift
+ KEY_RAW_LCTRL, KEY_RAW_RCTRL 0x1D, 0x9D Left / right Ctrl
+ KEY_RAW_LALT, KEY_RAW_RALT 0x38, 0xB8 Left / right Alt
+ KEY_RAW_F11, KEY_RAW_F12 0x57, 0x58 F11, F12
+ KEY_RAW_UP, KEY_RAW_DOWN 0xC8, 0xD0 Grey arrows (the keypad's are 0x48, 0x50)
+ KEY_RAW_LEFT, KEY_RAW_RIGHT 0xCB, 0xCD
+ KEY_RAW_KP_ENTER 0x9C Keypad Enter
+ KEY_RAW_PAUSE 0xC5 Pause (a press and a release at once)
+ KEY_RAW_COUNT 256 Size of the held-key snapshot
+
RawKeyEventT: uint8_t scancode (the make code), bool extended, bool down, bool repeat (a typematic repeat of a key already down), int32_t modifiers (KEY_MOD_* from the held-key table after the event, with KEY_MOD_LSHIFT 0x02 and KEY_MOD_RSHIFT 0x01 apart).
int32_t mouseButtons Current MOUSE_* button bitmask
int32_t mouseWheel Wheel delta this frame (positive = down)
int32_t keyModifiers Current BIOS shift state (KEY_MOD_*)
+ int32_t keyFocusWinId Window the held-key table was last cleared for (0 = none; internal)
int32_t wheelDirection 1 = normal, -1 = reversed
int32_t wheelStep Lines per wheel notch (1-10, default 3)
PlatformTicksT dblClickTicks Double-click interval in platformClock ticks
@@ -3883,6 +4134,35 @@ void dvxSetInputEntropyHook(DvxEntropyHookT fn);
Parameter Description
--------- -----------
fn Entropy sink, or NULL to disable
+
Desktop Sounds
+
The sounds DVX plays for its events. Which file each event plays (or none) is set on the Control Panel's Sounds page and kept in DVX.INI [sounds]; the sound library plays them (see its developer help). Without the sound library, or without sound hardware, nothing is heard and nothing else changes.
dvxMessageBox (and the dialogs built on it: dvxErrorBox, dvxInfoBox, dvxPromptSave, ...) plays the event of its icon: MB_ICONERROR SoundErrorE, MB_ICONWARNING SoundWarningE, MB_ICONQUESTION SoundQuestionE, MB_ICONINFO SoundInfoE, no icon SoundDefaultE.
+
A window created with dvxCreateWindow plays SoundWindowOpenE at the next dvxUpdate, unless it has become modal by then (dialogs are quiet); destroying a window that is not modal plays SoundWindowCloseE.
+
The shell plays SoundStartE once the desktop program is up, and SoundExitE (waiting for it) when DVX ends.
Play the sound of an event. dvxPlaySound returns at once while the sound plays on; dvxPlaySoundWait returns once it has played (at most a few seconds); dvxBeep is dvxPlaySound(SoundDefaultE). An event without a sound is silent.
The function that plays the sounds, set by the sound library when it loads (NULL: silent). Programs do not call it.
Accelerators
dvxCreateAccelTable
AccelTableT *dvxCreateAccelTable(void);
@@ -4102,6 +4382,33 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
text Null-terminated string to hash
Returns: 32-bit hash value.
+
+
dvxDrv.h -- Driver Modules
+
One model for the hardware drivers of every device class (sound, joysticks, ...). A driver is a module of its own, a file <name>.drv in its class's folder under bin/libs (built like a library, but the loader does not load it). The library of the device class loads its drivers only when a program first needs that kind of device, probes them, keeps the ones it uses and unloads the rest, so drivers for hardware that is not there, or for a device class nobody uses, never stay in memory. Nothing lists drivers: adding one is dropping its file into the folder.
+
Each driver exports one descriptor under a name its class chooses (audioDriver, joyDriver, ...). The descriptor is the class's own struct and starts with a DvxDriverHeaderT, written with DVX_DRIVER_HDR (the classes wrap it in their own macro, such as AUDIO_DRIVER or JOY_DRIVER).
+
Types
+
Type Members
+ ---- -------
+ DvxDriverHeaderT abiVersion (DVX_ABI_VERSION the driver was built for), size (sizeof the whole descriptor), name (short name, lower case, no blanks; DVX.INI settings use it), priority (higher is tried first).
+ DvxDriverModT handle (NULL once kept out of the list or unloaded), desc (the descriptor), kind (index of the descriptor symbol found).
+ DvxDriverClassT symbols (descriptor symbol of each kind, as dlsym wants it: DVX_SYM("name")), minSizes (smallest descriptor accepted per kind), kinds, valid (the class's own check of a descriptor, such as its functions being present; NULL for none).
Loads every *.drv in dir (relative to the DVX directory), looks each descriptor up and checks it: the ABI version, the size, a name, and the class's valid function. The valid ones come back as an stb_ds array sorted by priority (highest first, by name on a tie); a module that does not load, has no valid descriptor, or repeats a name a higher priority driver already has is logged to DVX.LOG and unloaded. NULL when there are none.
Takes mods[index] out of the list (so dvxDriversFree leaves it loaded) and returns it; unload it with dvxDriverUnload when done.
+
dvxDriversFree
+
void dvxDriversFree(DvxDriverModT *mods);
+
Unloads every module still in the list and frees the array.
+
dvxDriverUnload
+
void dvxDriverUnload(DvxDriverModT *mod);
+
Unloads one module (nothing when it is not loaded) and clears its handle and descriptor.
+
Writing a Device Class
+
A library that has drivers puts them in drivers/<name>/ in its source folder; its Makefile sets OBJDIR and DRVDIR and writes $(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))) (common.mk), which builds each folder into $(DRVDIR)/<name>.drv. The library defines its descriptor struct (starting with DvxDriverHeaderT), a macro that writes it, and a DvxDriverClassT; it calls dvxDriversLoad when it first needs devices, dvxDriverKeep for the drivers it uses and dvxDriversFree for the rest. Driver code resolves the library's functions when it loads, since the libraries are loaded for everyone to use.
+
dvxWgt.h -- Widget System
Retained-mode widget toolkit layered on the DVX window manager. Widgets form a tree (parent-child) rooted at a per-window VBox container. Layout is automatic: measure minimum sizes bottom-up, then allocate space top-down with flexbox-like weighted distribution. Widget types are registered dynamically at runtime via DXE plugins.
@@ -4901,7 +5208,7 @@ if (dvxDialogRun(ctx, dlg)) {
Save to a specific file, overriding the handle's stored path. The data is written to a sibling file whose extension is replaced by $ and the first two characters of the original extension (8.3-safe, e.g. DVX.INI -> DVX.$IN) and swapped in only after every write succeeded; the previous file is parked as DVX.~IN during the swap and then removed. A failed save (disk full, write error) returns false and leaves the previous file intact.
+
Save to a specific file, overriding the handle's stored path. The data is written to a sibling file whose extension is replaced by $ and the first two characters of the original extension (8.3-safe, e.g. DVX.INI -> DVX.$IN) and swapped in only after every write succeeded; the previous file is parked as DVX.~IN during the swap and then removed. A failed save (disk full, write error) returns false and leaves the previous file intact. When DVX runs from read-only media (platformIsReadOnly), a file that cannot be written is kept in memory instead (the save returns true) and prefsLoad of the same path reads that copy until DVX exits.
Fine-grained, free-running timestamp for input-timing entropy. It is not a clock: units and wrap point are backend-specific. On DOS it returns the CPU time-stamp counter (RDTSC) when CPUID reports one, otherwise the BIOS tick count shifted left 16 bits with the live PIT channel 0 counter (about 0.84 us resolution) in the low 16 bits. Used by dvxSetInputEntropyHook.
+
platformMicros
+
uint64_t platformMicros(void);
+
Monotonic microseconds, the high-resolution clock for games and measurements. On DOS it counts from the start of the timer service (dvxInit): the PIT input clocks the timer interrupt has counted plus the part of the running period, read from the latched channel 0 count (about 0.84 us steps), or on a CPU with a time-stamp counter the TSC cycles since the last interrupt at the rate measured over the interrupt period before it, so the clock never drifts from the PIT and a TSC whose rate changes (power saving, an emulator) does no harm. The host backend returns its fake clock. Not for use inside a timer callback.
One timer interrupt for everything that must run at a steady rate however busy the programs are (audio refill, joystick polling). dvxInit starts the service; dvxShutdown stops it. On DOS it owns PIT channel 0 (rate generator mode) and INT 8: the interrupt runs as fast as the fastest registered timer needs (at most PLATFORM_TIMER_MAX_HZ; the BIOS 18.2 Hz when there is none) and exactly one interrupt per 65536 PIT clocks is passed on to the BIOS handler, so the BIOS tick count, the DOS time of day, clock() and the floppy motor timeout keep their normal rate. A rate change takes effect at once (the PIT is restarted and the part of the period already run is counted). Shutdown restores the BIOS square-wave mode and the old vector; it also runs at exit() and from the crash handler when a crash is not recovered.
+
platformTimerAdd calls fn(userData) hz times a second, 1 to PLATFORM_TIMER_MAX_HZ; exact on average, while single intervals vary by up to one interrupt period. It returns a timer id, or -1 (bad arguments, all PLATFORM_TIMER_SLOTS in use, service not running). ownerAppId is recorded for platformTimerRemoveOwner, which the shell calls when a program ends; use 0 for a library that removes its own timers. gameTimerAdd (game.lib) registers one for the calling program. After platformTimerRemove returns, the callback is not running and will not run again.
+
platformTimerAddFast is the same for a device that must be fed at an audio rate (the PC speaker's digital sound): hz may be up to PLATFORM_TIMER_FAST_MAX_HZ. The whole timer interrupt then runs that often, so the callback must be very short (a few port writes); ask for PLATFORM_PIT_HZ / divisor to be called on every interrupt.
+
Warning: The callback runs INSIDE THE TIMER INTERRUPT, with interrupts disabled, on a small stack. Keep it to a few dozen microseconds; do not call the C library, DVX or DOS; do not use floating point; and pin its code and every byte it touches with platformLockRegion (a page fault there is fatal on DOS). Set flags and counters, fill buffers, write I/O ports.
+
PlatformTimerStatsT reports rateHz (the interrupt rate, rounded), divisor (the PIT reload value), interrupts (since the service started) and biosTicks (of those, passed on to the BIOS).
platformIrqSave disables interrupts and returns the previous state for platformIrqRestore; bracket the few instructions that change data an interrupt handler reads (nothing happens on the host).
+
A shutdown hook runs when DVX ends without its normal shutdown (exit() or a crash with no recovery point), before the timer service and the keyboard hook are given back, so a module that drives hardware from an interrupt (a sound card playing by DMA) can stop it. Each hook runs once. A module removes its hook before it is unloaded; at most PLATFORM_SHUTDOWN_HOOKS exist at once (Add returns false when full; adding one already present does nothing).
Map the BIOS scancodes for Shift/Ctrl/Alt+F1..F12 (Shift F1-F10 0x54-0x5D, Ctrl 0x5E-0x67, Alt 0x68-0x71, Shift/Ctrl/Alt F11-F12 0x87-0x8C) onto the plain F-key scancodes (F1-F10 0x3B-0x44, F11 0x85, F12 0x86). Any other scancode is returned unchanged. Applied by every backend's platformKeyboardRead.
Key-up event detection. Install/uninstall the INT 9 hook around process lifetime. platformKeyUpRead returns true if a key release was detected since the last call.
The keyboard hook (on DOS a chain on INT 9 that reads port 60h before the BIOS handler stores the key for platformKeyboardRead) decodes every set-1 byte: 0xE0 prefixes, the fake Shift codes the keyboard wraps around grey keys (dropped), the Pause burst (one press and one release) and keyboard replies such as ACK and overrun (dropped). Each key going down or up updates the held-key table and is queued as a RawKeyEventT (at most PLATFORM_KEY_EVENT_QUEUE; newer events are dropped when the queue is full, the table still changes). Keys are named by raw code, KEY_RAW_CODE(scancode, extended) (see dvxTypes.h): Left and Right Shift, Ctrl and Alt are keys of their own, and the code does not change with modifiers. dvxInit installs the hook and dvxShutdown removes it; the event loop reads the queue (onKeyUp, onRawKey) and clears the table whenever the keyboard focus moves. platformKeyStateGet copies the table (1 = down). platformInjectScan feeds one byte to the decoder as if the keyboard had sent it (the test script's KEYDOWN / KEYUP use it).
platformAltScanToChar
char platformAltScanToChar(int32_t scancode);
Translate an Alt+key scancode to its corresponding ASCII character. Returns 0 for non-printable scancodes.
platformDriveList fills out[] with the drives that exist, in letter order, and returns the count. On DOS it uses IOCTL calls only (INT 21h AX=4409h remote, 440Eh logical drive map, 4408h removable), never the media, so empty floppy and CD-ROM drives are listed at once; the second letter of a shared floppy drive (B: on a one-floppy system) is left out because using it makes DOS ask for a disk swap. The host backend reports one drive, "/". System Information and the file operations library both use it.
platformDiskSpace reports the size and free space of the disk holding path (DOS: INT 21h AH=36h on the path's drive letter, or the current drive; host: statvfs of the nearest existing folder of path, so a folder about to be created reports its disk). False when unknown.
platformBootDrive returns the letter of the drive DOS started from (INT 21h AX=3305h, DOS 4.0+), whose root holds AUTOEXEC.BAT, or 0 where there is none (older DOS, and every platform but DOS).
The loader tests at startup whether the DVX folder can be written to (platformDirIsWritable creates and removes a scratch file in dir) and records the answer with platformSetReadOnly. When DVX runs from a CD-ROM or a write-protected disk (platformIsReadOnly is true): the log goes to DVX.LOG in the TEMP folder (platformTempDir: the TEMP or else TMP variable, NULL when neither is set) or, without one, to memory; the boot-time help rebuild is skipped (its output and counts could not be saved); prefsSave and prefsSaveAs keep a file they cannot write in memory, and prefsLoad reads it back until DVX exits; the shell says so once at startup; and a program that allows several copies runs only once when there is no TEMP folder (a second copy is a temporary copy of the module). platformSetLogMemory sends dvxLog to a buffer holding the latest PLATFORM_LOG_MEM_MAX bytes (the oldest whole lines are dropped), which platformLogMemory returns; platformSetLogPath goes back to a file. SETUP.EXE logs to memory unless /LOG= is given, so it never writes to its own disk.
void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn);
-
Install signal handlers for SIGSEGV, SIGFPE, SIGILL. On crash, logs platform-specific diagnostics via logFn, stores the signal in *crashSignal, then longjmps to recoveryBuf.
+
Install signal handlers for SIGSEGV, SIGFPE, SIGILL. On crash, logs platform-specific diagnostics via logFn, stores the signal in *crashSignal, then longjmps to recoveryBuf. Without a recovery buffer the timer service and the keyboard hook are removed and the signal's default action runs.
What exit() and abort() mean to loaded modules. The DXE export table maps them to platformAppExit and platformAppAbort (the host build links every app with the same substitution, HOST_APP_LDFLAGS in common.mk). Both call the hook first; the shell's hook ends only the program that is current, the way End Task does (its task, or its callback running on the shell task, is abandoned through the crash-recovery path and the program is removed without its appShutdown and without a fault message; the log says "'name' called exit (status n); ending it"). abort() reports status PLATFORM_ABORT_STATUS. With no program current, or no hook, they end the process like the C library functions. Handlers a program registered with atexit are not run.
Palette index of the loaded splash image closest to an 8-bit RGB color, so overlays drawn on the splash (the progress bar) do not depend on the image's palette layout. Returns 0 when no splash palette is loaded (always 0 on the host test backend).
+
+
Sound Library
+
Sound for DVX programs through whatever sound hardware the computer has: sampled sound from WAV and VOC files, tones and noise, and streams a program feeds itself (music, speech). Every program opens its own audio context; any number of programs play at the same time through one mixer, and everything a program opened is stopped and released when it ends, normally, killed or crashed.
Loaded as: bin/libs/kpunch/audio/audio.lib, drivers in bin/libs/kpunch/audio/drivers/*.drv
+
Contexts and the Device
+
AudioCtxT *audio = audioOpen(); // NULL: no driver could start
+
+int32_t voice = audioPlayFile(audio, "BOOM.WAV", AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, false);
+
+if (voice < 0) {
+ dvxLog("no boom: %s", audioErrorText(voice));
+}
+...
+audioClose(audio); // or let the program end
+
The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth) or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices and the voices playing.
+
None of the functions may be called from an interrupt (a timer-service callback).
+
Samples and Voices
+
audioSampleLoad (file), audioSampleLoadMem (memory) and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.
+
Setting Range
+ ------- -----
+ volume 0 .. AUDIO_VOLUME_MAX (256)
+ pan AUDIO_PAN_LEFT (-256) .. AUDIO_PAN_RIGHT (256); centre plays at full volume on both sides and panning turns the other side down
+ pitch AUDIO_PITCH_MIN (64) .. AUDIO_PITCH_MAX (1024), AUDIO_PITCH_NORMAL (256) = recorded speed
+
audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoiceStop and audioVoiceIsPlaying take the context and the voice id; a voice id of another context, or of a voice that has ended, gives AUDIO_ERR_VOICE. audioStopAll stops every sample and tone of a context.
+
audioSetEndFn(ctx, fn, userData) asks for a call when a voice of ctx ends, with the reason: AUDIO_END_FINISHED (played out), AUDIO_END_STOPPED (stopped, or its sample or stream went away) or AUDIO_END_DROPPED (the voice limit made room for a louder sound). The call comes from audioPump or the shell's idle time, as the program that owns the context, never from an interrupt. The mixer runs up to one buffer (about 50 ms) ahead of what is heard, so a notification can come that much before the sound has finished playing.
+
Tones
+
audioTone(ctx, wave, hz, volume, ms) plays a square (AUDIO_WAVE_SQUARE), triangle (AUDIO_WAVE_TRIANGLE) or noise (AUDIO_WAVE_NOISE) tone of hz (AUDIO_TONE_MIN_HZ 20 to AUDIO_TONE_MAX_HZ 20000) for ms milliseconds, or until stopped when ms is 0. For noise, hz is how fast the noise changes: higher sounds brighter. audioVoiceSetFrequency changes a playing tone. When the computer has an FM chip (and DVX.INI [audio] fmTones is not false), a square or triangle tone plays on a free FM voice (a square as a carrier modulated at twice its pitch, which gives odd harmonics like a square wave; a triangle as a sine) at up to the chip's highest note (about 6,200 Hz); when every FM voice is taken it is mixed like any sound. Its voice id, volume, pan, pitch and end notification work the same either way. On a device with a DAC tones are mixed like any sound; on the PC speaker in its tones mode the loudest square or triangle tone playing is sounded by the timer chip.
+
Note Queues
+
AudioNotesT *tune = NULL;
+
+audioNotesOpen(audio, AUDIO_WAVE_SQUARE, AUDIO_VOLUME_MAX, &tune);
+audioNotesAdd(tune, 440 * AUDIO_MILLIHZ_PER_HZ, 250000); // A for 1/4 s
+audioNotesAdd(tune, 0, 50000); // a rest
+
A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.
audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. A stream that runs dry plays silence and counts underruns. audioStreamClose stops and frees it.
+
FM Synthesizer
+
int32_t v = audioFmAlloc(audio); // AUDIO_ERR_NO_VOICE: all taken
+
+audioFmWrite(audio, v, AUDIO_FM_CAR_LEVEL, 0);
+audioFmWrite(audio, v, AUDIO_FM_FNUM_LOW, fnum & 0xFF);
+audioFmWrite(audio, v, AUDIO_FM_KEY_BLOCK, AUDIO_FM_KEY_ON | (block << AUDIO_FM_BLOCK_SHIFT) | (fnum >> AUDIO_FM_FNUM_HI_SHIFT));
+audioFmFree(audio, v); // key off, back to everyone
+
An OPL2 (9 voices) or OPL3 (18 voices, each playing left, right or both) found by the opl driver is shared by every program: audioFmAlloc(ctx) takes a two-operator voice (the least recently given back first, so a released note's tail is cut last) and returns its id, AUDIO_ERR_NO_VOICE when all are taken, AUDIO_ERR_BUSY while a program has the whole chip, AUDIO_ERR_NO_SYNTH without a chip. audioFmWrite(ctx, voice, reg, value) sets a register of the voice's channel, numbered as for the first channel of an OPL2 (AUDIO_FM_MOD_CHAR 20h, AUDIO_FM_CAR_CHAR 23h, levels 40h/43h, attack 60h/63h, sustain 80h/83h, waveforms E0h/E3h, AUDIO_FM_FNUM_LOW A0h, AUDIO_FM_KEY_BLOCK B0h, AUDIO_FM_FEEDBACK C0h); the library adds the channel's offsets and bank. On an OPL3 a feedback value without an output bit (AUDIO_FM_OUT_LEFT, AUDIO_FM_OUT_RIGHT) plays on both sides. Frequencies: hz = fnum x AUDIO_FM_CHIP_HZ (49,716) x 2^block / 2^20. audioFmFree keys the voice off and gives it back; closing the context gives back all of its voices. audioFmGetInfo tells the chip, its voices and how many are free.
+
audioFmRawAcquire(ctx) gives one program the whole chip: every voice the others hold is keyed off and taken back (their tones go on in the mixer), new voices are refused, and audioFmRawWrite(ctx, reg, value) sets any register (bank x AUDIO_FM_BANK_REGS + index). A second program asking gets AUDIO_ERR_BUSY. audioFmRawRelease (or closing the context) silences the chip, sets it up again and gives its voices back to everyone.
+
audioFmAlloc, audioFmFree, audioFmWrite and audioFmRawWrite may also be called from a timer callback (platformTimerAdd); music.lib plays MIDI that way.
+
MIDI Port
+
With DVX.INI [audio] midi = mpu401 the mpu401 driver opens an MPU-401 (or compatible, such as a Sound Blaster 16's) in UART mode, for an external synthesizer such as a Roland MT-32 or SC-55. The port belongs to one program at a time: audioMidiAcquire(ctx) (AUDIO_ERR_BUSY when another has it, AUDIO_ERR_NO_SYNTH without a port), audioMidiPut(ctx, byte) sends a byte and returns AUDIO_ERR_BUSY when the port cannot take one yet (send it again later; it may be called from a timer callback), audioMidiRelease gives it back (closing the context does too). audioMidiGetInfo tells whether there is a port and whether it is in use. music.lib uses the port for MIDI music.
+
Volume and the Voice Limit
+
audioSetMasterVolume sets the volume of all sound (DVX.INI [audio] volume, in percent, sets it when the device starts); audioSetVolume sets the volume of everything one context plays. At most the voice limit (DVX.INI [audio] voices, 1 to AUDIO_VOICES_MAX, default 16) plays at once. Starting another sample or tone then stops the quietest one playing (it ends with AUDIO_END_DROPPED); when the new sound would itself be the quietest it is not started (AUDIO_ERR_NO_VOICE). Streams are never dropped.
+
Files
+
WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to 48000 Hz; WAVE_FORMAT_EXTENSIBLE with the PCM sub-format is accepted, other chunks are skipped, and a data chunk that claims more bytes than the file holds plays what is there. VOC (Creative Voice File, header checksum checked): sound blocks with 8-bit unsigned samples, continuation, silence, extended (rate and stereo for the next sound block) and new-format blocks with 8-bit or 16-bit PCM; markers and text are skipped, and a repeated section plays once. Compressed sound (ADPCM, 24-bit, floating point) gives AUDIO_ERR_UNSUPPORTED, a damaged file AUDIO_ERR_CORRUPT, a file that is neither AUDIO_ERR_FORMAT.
+
The Mixer
+
All sound is mixed in 16-bit integers at the rate the device achieved (the mixer never assumes the rate it asked for: a Sound Blaster Pro asked for 22,050 Hz plays at 22,222 Hz), and every source is resampled to it with linear interpolation. Gains combine volume, pan, context volume and master volume; the sum is clipped to 16 bits. The mixer runs from the sound card's own interrupt (the Sound Blaster, at the end of each half buffer of about 23 ms), or from the timer service for devices without one, so sound keeps playing however busy the programs are. It uses no floating point and touches only memory pinned for interrupts (sample and stream buffers are pinned when they are made).
+
Settings
+
DVX.INI section [audio]:
+
Key Meaning
+ --- -------
+ driver auto (default: find the hardware), or a driver name: sb, speaker, null
+ rate mixing rate asked for, 4000 to 48000 (default 22050)
+ bits 0 (the device's best), 8 or 16
+ channels 0 (the device's best), 1 or 2
+ voices voices mixed at once, 1 to 32 (default 16)
+ volume master volume in percent (default 100)
+ speakerPcm true: sampled sound on the PC speaker (see below); default false
+ fmTones false: tones always in the mixer, never on FM voices; default true
+ oplPort the FM chip's port, hex (default 388)
+ midi where MIDI music plays: auto (default: the FM chip, else music.lib's software synthesizer), mpu401, fm, software
+ mpuPort the MPU-401's port, hex (default 330); the port is only looked for with midi = mpu401
+ mpuMode what is on the MPU-401 (music.lib): gm (default), mt32 or raw
+ midiBank the OP2 instrument bank for FM MIDI (music.lib), a path from the DVX folder
+
Desktop Sounds
+
The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file (a path from the DVX folder), empty or missing for none; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). The default set, made by DVX's own sound generator at build time, is in AUDIO_SOUNDS_DIR (config/sounds).
+
Key Event
+ --- -----
+ default the default beep (dvxBeep; a message box without an icon)
+ startup DVX started
+ exit DVX is ending
+ error a message box with MB_ICONERROR
+ warning MB_ICONWARNING
+ question MB_ICONQUESTION
+ information MB_ICONINFO
+ windowOpen a window opened (not a dialog)
+ windowClose a window closed (not a dialog)
+
Drivers
+
Driver Device
+ ------ ------
+ sb Sound Blaster family by DSP version: 1.x (8-bit single-cycle), 1.5 / 2.0 (8-bit auto-init, high-speed above 23,000 Hz on 2.0), Pro and Pro 2 (8-bit stereo at 11,025 or 22,050 Hz), 16 (8- or 16-bit, mono or stereo, any rate from 5,000 to 44,100 Hz). The port, IRQ and DMA channels come from the BLASTER variable (e.g. SET BLASTER=A220 I5 D1 H5); without it an SB16 is found at 220h or 240h and asked for its IRQ and DMA. If no interrupt arrives from the card within half a second (a wrong IRQ), the next driver is tried.
+ speaker The PC speaker. By default tones only: the timer chip plays the loudest square or triangle tone as a square wave, with no interrupts and no CPU cost; sampled sound and noise are not heard. With speakerPcm = true it plays everything by pulse-width modulation: the timer interrupt runs at 22,096 Hz and each interrupt sets the next pulse (about 5.7 bits). That costs a lot of CPU time: 22,096 interrupts a second plus the mixing. A 486DX2-66 spends a large share of its time on it (estimated 10 to 15 percent for the interrupts alone on plain DOS, much more under a memory manager or Windows) and can lose interrupts, which slows the sound and the DVX clock down. Meant for Pentium-class machines.
+ null No sound device: plays nothing, but voices still run their course and end, so programs behave the same.
+ opl (synthesizer) The OPL2 or OPL3 FM chip of an AdLib or Sound Blaster at 388h (oplPort), found by the AdLib timer test; the status register tells an OPL2 from an OPL3. Register writes are timed by status reads (3.3 and 23 us on an OPL2) with interrupts off around each index/data pair. Closing zeroes every register.
+ mpu401 (synthesizer) An MPU-401 MIDI port in UART mode at mpuPort, only with midi = mpu401: reset (FFh, answered FEh; tried twice, since a port left in UART mode takes the first FFh as data), then UART mode (3Fh). Bytes are sent when the port says it is ready (a short wait, else the caller sends later); input the port has (active sensing) is read and dropped. Closing sends All Notes Off on every channel and resets the port.
+
audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen).
+
Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Sound Blaster, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: of each kind (FM chip, MIDI port) the first that finds its hardware is kept, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.
+
Writing a Driver
+
A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
+
A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM or AUDIO_SYNTH_MIDI, .open = ..., .close = ..., .write = ... or .put = ...). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready. Both run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver.
+
BASIC
+
src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends.
+
BASIC Runtime Library
Stack-based p-code virtual machine and value system for DVX BASIC. Embeddable: the host provides I/O and UI callbacks. No DVX GUI dependencies in the core runtime.
When the DVX folder cannot be written to (platformIsReadOnly: the loader found it read-only, as on a CD-ROM), a message says once that changed settings last until DVX exits and that the help is used as it is.
The program named by DVX.INI [shell] welcome (SHELL_INI_WELCOME), if the key is there. The shipped DVX.INI names the Welcome window, which removes the key when the user ticks "Don't show this again", so the key itself is the "first start" flag.
-
The program given with /RUN=path on the DVX command line (DVX /RUN=APPS\KPUNCH\SETUP\SETUP.APP is how INSTALL.BAT starts Setup).
+
The program given with /RUN=path on the DVX command line (for example DVX /RUN=APPS\KPUNCH\NOTEPAD\NOTEPAD.APP).
Both are started with shellRunCommand, so a missing program is reported to the user.
@@ -6637,6 +7109,316 @@ fileOpsJobFree(job);
BASIC
src/include/basic/fileops.bas declares BASIC wrappers: FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree (a whole folder, without prompting), FileOpsDriveList$ and FileOpsFreeKb. Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR and SETATTR statements.
+
+
Game Helpers Library
+
Frame pacing, held keys, program-owned timers and nearest-colour lookup for programs that run like games in a DVX window. It builds on the core's game input windows, held-key table, timer service and high-resolution clock (see Writing Games for DVX).
+
Header: game/game.h
+
Loaded as: bin/libs/kpunch/game/game.lib
+
Frame Pacing
+
GameFramePacerT pacer;
+
+gameFrameInit(&pacer, 70);
+
+while (running) {
+ uint32_t missed = gameFrameWait(&pacer); // 0 when on time
+
+ updateGame(missed + 1); // catch the game logic up
+ drawFrame();
+}
+
gameFrameInit(p, hz) starts pacing at hz frames a second (1 to GAME_FRAME_MAX_HZ, 1000; out of range is clamped) with frame 0 beginning now. gameFrameWait waits until the next frame begins and returns the number of frames that were missed. While it waits it gives the time to the other programs (tsYield), so call it from a program with its own main loop (hasMainLoop), never from a window callback. Frame n begins at start + n / hz on platformMicros, so the pace does not drift however long it runs. When the frame after the next one has already begun (the caller was busy), the frames in between are skipped, not caught up: gameFrameWait returns at once with how many, and GameFramePacerT.missed keeps the total. A frame that starts late but within its own time counts as on time.
+
gameFrameNext(p, &missed) does the same without the waiting: it moves on to the next frame (skipping and counting missed ones the same way) and returns the platformMicros time that frame begins, for a caller that waits its own way. The BASIC runtime uses it for the FRAMEWAIT statement, keeping the desktop running while it waits.
+
Held Keys
+
gameKeyIsDown(win, rawCode) is true while the key is held down and win has the keyboard focus. Keys are named by raw code (KEY_RAW_CODE, KEY_RAW_* in dvxTypes.h). The held-key table starts over whenever the focus moves, so a key pressed while another window had the focus is not seen until it repeats.
+
Timers
+
gameTimerAdd(ctx, hz, fn, userData) is platformTimerAdd for the calling program (ctx->currentAppId): the timer is removed when the program ends, however it ends. fn runs inside the timer interrupt; read the rules at the Timer Service in the platform reference before writing one. Remove it with platformTimerRemove.
+
Colours on a 256-Colour Desktop
+
On an 8-bit desktop every window shares the desktop palette (dvxGetDisplay(ctx)->palette; NULL at 15 bits and more). A game with its own colours draws the nearest desktop entries.
+
Function Description
+ -------- -----------
+ gamePalNearest(pal, r, g, b) The entry of the 256-entry palette pal (R, G, B bytes) closest to the colour: smallest squared RGB distance, lowest index on a tie. An exact search of all 256 entries.
+ gamePalCacheInit(c, pal) Record pal in the cache c and empty it. GamePalCacheT is about 37 KB: allocate it.
+ gamePalCacheLookup(c, r, g, b) The entry nearest to the centre of the colour's 15-bit cell (5 bits per channel), searched once per cell and then remembered, so every colour in a cell gets the same answer.
+ gamePalCacheSync(c, pal) Start over (and return true) when pal differs from the recorded palette; call it once a frame if the palette can change.
+
BASIC
+
src/include/basic/game.bas declares GameKeyDown(keyCode), True while the key is held and one of the calling program's windows has the focus, with GAMEKEY_* constants for common keys. The BASIC runtime gives the library the GUI context (gameBasSetContext) when a program starts. Frame pacing from BASIC comes with the BASIC game statements later.
+
+
+
Joystick Library
+
Game controllers for DVX programs behind one API, whatever the hardware: the devices the drivers found, axes calibrated to -32768..32767 with a dead zone round the centre, buttons, hats, read when the program wants (polled) or delivered as events. A program's handles close when it ends, however it ends. Calibration and the other settings live in DVX.INI [joystick]; the Control Panel's Game Controllers page sets them.
+
Header: joystick/joystick.h
+
Loaded as: bin/libs/kpunch/joystick/joystick.lib, drivers in bin/libs/kpunch/joystick/drivers/*.drv (joyport.drv, joykeys.drv), loaded only when a program first asks for devices.
+
Reading a Joystick
+
if (joyCount() > 0) {
+ int32_t stick = joyOpen(ctx, 0); // device 0, owned by the calling program
+ JoyStateT st;
+
+ if (joyRead(stick, &st) && st.connected) {
+ shipX += st.axes[0] / 4096; // -8..7
+ firing = (st.buttons & 1) != 0;
+ }
+}
+
joyCount() is the number of devices (the first call of any joystick function finds them); joyGetInfo(device, &info) describes one: name, id (the driver's name and unit number, "gameport0", which prefixes the device's DVX.INI keys), axisCount, buttonCount, hatCount, axisPresent (bit n set when axis n answers; a missing axis reads 0) and caps (JOY_CAP_CALIBRATE for analog axes, JOY_CAP_KEYBOARD for the keyboard joystick).
+
joyOpen(ctx, device) returns a handle owned by ctx->currentAppId, or -1. joyOpenShared(ctx, device) returns the calling program's shared handle on the device, opening it the first time (for code with no place to keep a handle; the BASIC functions use it). joyClose closes a handle; a program's handles are closed for it when it ends.
+
joyRead(handle, &st) fills a JoyStateT: axes[] (JOY_AXIS_MIN..JOY_AXIS_MAX, 0 inside the dead zone), hats[] (JOY_HAT_CENTERED or degrees clockwise from up), buttons (bit n = button n) and connected (false when the device stopped answering; everything then reads neutral). A button that went down and up again since the handle last looked reads as down once, so a quick tap between two reads is not lost. joyButtonPressed(handle, button) asks the same about one button. joyButtonWentDown(handle, button) only asks whether the button went down since the last time this function asked about it (a press counter of its own, apart from joyRead's).
+
The keyboard joystick (JOY_CAP_KEYBOARD) reads neutral for a handle whose program has no window with the focus, so keys typed into another program do not steer the game.
+
Events
+
joySetEventFn(handle, fn, userData) delivers the device's changes from now on: JoyEventAxisE when an axis moved by at least JOY_EVENT_AXIS_STEP or reached the centre or an end, JoyEventButtonDownE / JoyEventButtonUpE (a tap between two polls gives both), JoyEventHatE. fn runs in program context as the handle's program (ctx->currentAppId is its id), so DVX calls are fine. Events are delivered by joyPump, which the shell calls on every idle frame; a program with its own main loop that repaints every frame (so the shell is never idle) calls joyPump itself or reads with joyRead. joyPump is not reentrant: an event function that runs a modal dialog gets its remaining events after it returns. A NULL fn stops the events.
+
Polling and CPU Cost
+
While at least one handle is open, a timer-service callback marks a poll due [joystick] pollHz times a second (JOY_POLL_HZ_DEFAULT 60, JOY_POLL_HZ_MIN 10 to JOY_POLL_HZ_MAX 200) and runs the drivers' tick hooks (the game port latches button presses there). The poll itself runs in program context, the next time a program reads (joyRead, joyButtonPressed, joyGetRaw) or the shell is idle, so however often a program reads there are at most pollHz polls a second. With no handle open nothing is polled and the timer is removed.
+
A game port poll waits for the slowest connected axis to run out: about 0.6 ms with the stick centred, up to about 1.2 ms at an end. At 60 polls a second that is about 4 to 7 percent of the processor's time on any machine, whatever its speed, while a program has a joystick open; the timer interrupt's part (one port read) is negligible. The keyboard joystick costs nothing measurable.
+
Calibration and Settings
+
joyGetRaw(device, &raw) returns the last raw poll (driver units: microseconds for the game port). joyGetCalibration / joySetCalibration get and set an axis's JoyAxisCalT (min, center, max in raw units; min < center < max): min..center maps to -32768..0, center..max to 0..32767, clamped. A NULL calibration forgets it. An axis never calibrated ranges automatically: centred on the reading taken when the device was found, starting at center / 8 .. center * 2 and widening as the stick moves. joyGetDeadZone / joySetDeadZone get and set the device's dead zone in percent (0..JOY_DEADZONE_MAX, default JOY_DEADZONE_DEFAULT 8). joyAxisScale(cal, raw) and joyDeadZoneApply(value, percent) are the two steps as plain functions.
+
Settings changed through the library take effect at once and reach DVX.INI only through joySettingsStore(prefs) (the caller saves the file); joySettingsLoad(prefs) replaces them all, e.g. to undo. joySettingGet / joySettingSet read and write single values. The [joystick] keys:
+
Key Value
+ --- -----
+ pollHz Polls a second (10 to 200, default 60).
+ fallback auto (default): the keyboard joystick only when no other device is found; always; never.
+ gameport.mode twoSticks (default) or fourAxes: one joystick using all four axes and buttons (flight sticks with a throttle or rudder).
+ keyboard.up ... keyboard.right, keyboard.button1 ... keyboard.button4 Key names (joyKeyName: Up, Down, Left, Right, LCtrl, RCtrl, LShift, RShift, LAlt, RAlt, Space, Enter, Tab, Backspace, Insert, Delete, Home, End, PgUp, PgDn, Keypad0 to Keypad9, A to Z, 0 to 9). Defaults: the arrows, Left Ctrl, Space, Left Shift, Z.
+ <id>.axis<n> min,center,max: axis n of the device with that id, e.g. gameport0.axis0 = 40,560,1150.
+ <id>.deadZone The device's dead zone in percent.
+
joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise.
+
The Game Port Driver
+
joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched.
+
In the default mode the port makes up to two joysticks (A with axes 0-1 and buttons 0-1, B with axes 2-3 and buttons 2-3; either may be missing); in the four-axis mode one joystick with four axes and four buttons.
+
The Keyboard Joystick
+
joykeys.drv is a fallback driver: a joystick made of keys (two axes: full deflection while a key is held, the centre with both or neither; four buttons), read from the held-key table, so games work without joystick hardware. It is listed only when no other device is found, unless [joystick] fallback says always or never.
+
Writing a Driver
+
A driver is a driver module (see dvxDrv.h in the API reference): a file <name>.drv in bin/libs/kpunch/joystick/drivers, built by the joystick Makefile from each drivers/<name>/ folder, that exports one JoyDriverT named joyDriver, written with JOY_DRIVER(name, priority, .flags = ..., .probe = ..., .info = ..., .poll = ..., .tick = ..., .settingsChanged = ...). Nothing lists drivers and nothing loads them at startup: the first time a program asks for devices (the GUI and the timer service are running by then), and at every joyRescan, the library loads them all, probes them from the highest priority down (JOY_PRIORITY_PORT, ..., JOY_PRIORITY_KEYBOARD), probes the fallback drivers only when the fallback setting asks for them, and unloads every driver that found no device. The descriptor carries DVX_ABI_VERSION; a driver built for another version, one without probe, info or poll, or one whose name a higher priority driver already has is skipped and logged.
+
Member Meaning
+ ------ -------
+ hdr.name Lower case, no blanks ("gameport"); the prefix of the device ids and of the driver's own [joystick] keys.
+ hdr.priority Probing order (JOY_PRIORITY_*); devices are numbered in that order.
+ flags JOY_DRIVER_FALLBACK for a stand-in like the keyboard joystick.
+ probe() Find the units; return how many. Called right after the driver is loaded.
+ info(unit, out) Describe one unit (name, counts, axisPresent, caps); the library fills in id.
+ poll(raw, units) Read every unit now, in program context: raw[0..units-1]. Set axisValid for each axis measured (the others keep their last value), buttons held now, pressed for buttons that went down since the last poll, hats (JOY_HAT_CENTERED when not pushed) and connected. Axes are raw units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN..JOY_AXIS_MAX.
+ tick() NULL, or called INSIDE THE TIMER INTERRUPT at the poll rate while a handle is open. The timer service's rules apply; the driver pins the code and data it touches with platformLockRegion and releases them with platformUnlockRegion when its module is unloaded (a destructor), so rescans do not pile up locked memory.
+ settingsChanged() NULL, or re-read the driver's settings (joySettingGet) after they changed.
+
Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded again at every probe, so it must not rely on state kept from an earlier load; set its hardware up in probe (or a constructor).
+
BASIC
+
src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton and JoyHat. Joystick, axis, button and hat numbers start at 1; each function reads through the calling program's shared handle.
+
The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.
+
+
+
Writing Games for DVX
+
A game in DVX is a program with its own main loop (hasMainLoop = true in its SHELL_APP_DESCRIPTOR). DVX gives it its own task: appMain runs the game loop itself, draws into the game's window, and waits for each frame on the high-resolution clock while the other programs keep running. Nothing in DVX has to stop for a game: the desktop, other programs, the Task Manager (Ctrl+Esc) and help all stay available.
+
Starting From a Template
+
bin/sdk/newProject.sh game myGame a C game
+ bin/sdk/newProject.sh basicgame myGame a DVX BASIC game
+
The C template is a small but complete game, and every part of this guide is shown in it: a game input window, a 320x200 screen of 8-bit colour numbers drawn scaled into the window on any desktop colour depth, frame pacing, the arrow keys and a joystick, a sound effect and music carried inside the .app as resources, tones for the end of a round, the best score saved in the program's settings folder, its own help file (F1) and icon. The BASIC template does the same in a few dozen lines with FRAMEWAIT, GameKeyDown, STICK, STRIG and PLAY.
+
The Shape of a Game
+
int32_t appMain(DxeAppContextT *ctx) {
+ GameFramePacerT pacer;
+
+ createGameWindow(ctx); // gameInput = true, onClose sets quit
+ gameFrameInit(&pacer, 60);
+
+ while (!quit) {
+ uint32_t missed = gameFrameWait(&pacer); // yields until the frame is due
+
+ audioPump(); // sound end calls and streams
+ musicPump();
+ update(missed + 1); // move by the frames that passed
+ draw(); // into the game's own framebuffer
+ present(); // changed parts -> window, dvxInvalidateRect
+ }
+
+ cleanUp(); // or just return: DVX releases the rest
+ dvxDestroyWindow(ctx->shellCtx, win);
+ return 0;
+}
Set gameInput on the game's window and give it an onRawKey callback if it wants every key going down and up:
+
win->gameInput = true;
+win->onKey = gameKey; // typed keys: every key but three
+win->onRawKey = gameRawKey; // each key going down and up
+
While the window has the focus, DVX keeps only three keys, so the user can always switch away, open the Task Manager or close the game:
+
Key Always does
+ --- -----------
+ Alt+Tab Switch to the next window
+ Ctrl+Esc Open the Task Manager
+ Alt+F4 Close the window (its onClose)
+
Every other key, F1, F10, Alt+letter and the accelerators included, goes to onKey instead of opening help or menus. The game decides what F1 does (the template opens its help with shellOpenHelp); its menus still work with the mouse. onRawKey gets a RawKeyEventT for every key going down or up: the set-1 scancode with an extended flag for the grey keys, down or up, whether it is a typematic repeat, and the modifiers. Shift, Ctrl and Alt are keys of their own there, left and right apart.
+
Held Keys
+
For "is it held right now" use gameKeyIsDown(win, rawCode) with a raw key code (KEY_RAW_UP, KEY_RAW_LCTRL, or KEY_RAW_CODE(scan, extended) for any key); it is true only while win has the focus. platformKeyIsDown and platformKeyStateGet read the table itself. When the focus moves away, the game gets a release for every key still held and the table starts over, so no key stays stuck down. A key already held when the window gets the focus is seen once it repeats.
+
Typed Characters and the Mouse
+
onKey gets typed characters (with Shift and the keyboard layout applied) as it does in any window; keep them in the game's own queue if the game reads text. onMouse gets the pointer in content-area pixels: divide by the scale the game draws at, and clamp.
+
Joysticks
+
joyCount says how many joysticks there are; joyOpen(ctx, device) gives the program a handle (closed for it when it ends) and joyRead fills a JoyStateT: axes calibrated to -32768..32767 with a dead zone, buttons (a press between two reads is not lost), hats. Read it once a frame. Programs that wait for input can take events instead (joySetEventFn). With no joystick connected the keyboard joystick stands in (the arrow keys and four buttons, chosen in the Control Panel), answering only while the program's window has the focus; so a game that reads both the keys and the joystick should let the keys win rather than add the two up. Polling a game port joystick costs about 4 to 7 percent of the processor while a program has one open. See the Joystick Library.
+
+
+
Timing and Frame Pacing
+
platformMicros() is a monotonic microsecond clock from the PIT, the timer chip every PC has (with the CPU's time-stamp counter between timer interrupts when there is one). Use it for any game time: elapsed milliseconds are platformMicros() / 1000.
+
Frame Pacing
+
gameFrameInit(&pacer, hz) starts a frame clock (1 to GAME_FRAME_MAX_HZ frames a second); gameFrameWait(&pacer) once a frame yields to the other programs until the next frame is due and returns how many frames were missed. Frame n starts at start + n / hz, so the rate never drifts; frames that already ended while the game was busy are skipped, not caught up (pacer.missed counts them). Move by missed + 1 frames' worth (with a limit, so a long stall does not teleport the player) and the game runs at the same speed on a slow machine. pacer.frame is a frame counter.
+
There is no vertical retrace wait: a game window is composited with the rest of the desktop, and the frame rate is a virtual one. 60 or 70 frames a second both work; pick what the game was designed for.
+
The Timer Service
+
For work that must happen at a steady rate however busy the programs are (a music player, a sample-accurate sequencer), the timer service calls a function up to 1000 times a second from the timer interrupt: gameTimerAdd(ctx, hz, fn, userData), removed when the program ends (or platformTimerAdd). DVX speeds the timer interrupt up only as far as the fastest timer needs and keeps the DOS clock at its normal rate. The callback runs at interrupt time: no C library, no DVX calls, no floating point, and its code and data pinned with platformLockRegion. See Timer Service in the platform reference.
+
+
+
The Screen: Framebuffers and Palettes
+
A window's content buffer (win->contentBuf, win->contentPitch bytes per row, contentW x contentH pixels) is in the desktop's own pixel format: 8, 15, 16 or 32 bits per pixel, as dvxGetDisplay(ctx)->format says. A game that must look the same everywhere draws into a framebuffer of its own (for example 320x200 bytes, each a colour number into a 256-colour game palette) and converts only what changed into the content buffer each frame.
+
The Colour Lookup Table
+
Keep one desktop pixel value per game colour, and rebuild it when the game palette changes or the desktop's pixel format or palette does (the user can change the video mode while the game runs):
Desktop Pixel size Game colours
+ ------- ---------- ------------
+ 32-bit 4 bytes Exact.
+ 16-bit 2 bytes 5-6-5 bits of red, green, blue: very close.
+ 15-bit 2 bytes 5 bits each: very close.
+ 8-bit 1 byte The nearest of the desktop's 256 colours.
+
On an 8-bit desktop all windows share the one desktop palette: a 6x6x6 colour cube, a 16-step grey ramp and the chrome colours (see dvxPal.h). A game cannot change it; gamePalNearest finds the closest entry, and a GamePalCacheT remembers the answer per 15-bit colour when there are many colours to map (gamePalCacheSync starts over if the desktop palette changed). Gradients come out banded; design for it, or ask players to use a 15-bit or better desktop for the best look.
+
Palette animation (colour cycling, fades) works at every depth: change the game palette entries, look up just those entries again, and redraw the pixels that use them. Games whose hardware picks a palette per scan line keep one small table per palette and pick the table per row.
+
Presenting a Frame
+
+
Keep a list of the framebuffer rectangles (or rows) that changed this frame.
+
For each, expand the colour numbers through the table into contentBuf, repeating each pixel scale times across and each row scale times down, then call dvxInvalidateRect(ctx, win, x * scale, y * scale, w * scale, h * scale) so DVX puts it on the screen.
+
Give the window an onPaint that converts the whole framebuffer (without invalidating): DVX calls it when the window is first shown and after a video mode change, when contentBuf has been made again.
+
Clip to contentW and contentH, and do nothing while contentBuf is NULL.
+
+
Size the window so its content area is exactly the framebuffer times a whole scale that fits the screen (the template computes the scale from the display size and the chrome constants, then corrects with dvxResizeWindow after adding the menu bar). Copying only what changed matters on slower machines: a full 320x200 frame at 2x on a 16-bit desktop is 256 KB of pixels to convert and composite.
+
+
+
Sound, Music and MIDI
+
Games play sound on whatever the computer has: a Sound Blaster family card, an FM synthesizer chip, a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.
+
Sound Effects and Tones (audio.h)
+
+
audioOpen() gives the program an audio context (the first one opened starts the sound device).
+
audioSampleLoad / audioSampleLoadMem load a WAV or VOC file (from memory: a resource, say); audioSampleCreate makes a sample from raw PCM (8-bit unsigned or 16-bit signed, mono or stereo, any rate from 4000 to 48000 Hz).
+
audioPlay(sample, volume, pan, loop) plays it on a voice; the voice id sets volume, pan and pitch while it plays (audioVoiceSet...), audioVoiceStop stops it, and audioSetEndFn tells when it ends.
+
audioTone plays a square, triangle or noise tone of a frequency and length; audioNotesOpen gives a queue of notes and rests played back to back, each to the exact sample (jingles, chip-style tunes).
+
audioStreamOpen takes the game's own sound at any rate: written with audioStreamWrite, or asked for by a fill function.
+
audioSetVolume sets the program's volume; the master volume belongs to the user (Control Panel).
+
+
Music (music.h)
+
musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
+
The FM Chip and the MIDI Port
+
For music drivers of its own, a game can allocate two-operator FM voices shared with the other programs (audioFmAlloc, audioFmWrite with the AUDIO_FM_* register numbers), take the whole FM chip for itself (audioFmRawAcquire; refused with AUDIO_ERR_BUSY while another program has it), or send bytes to the MIDI port (audioMidiAcquire, audioMidiPut). Give them back as soon as the game does not need them.
+
Keeping Sound Fed
+
Sampled sound is mixed from the sound card's own interrupt, so it keeps playing however busy the game is. End notifications, pull streams, modules and the software synthesizer are made in the desktop's idle time; a game that redraws every frame should call audioPump() and musicPump() once a frame so they keep up. Modules use floating point: on a computer without a maths coprocessor they are slow.
+
See the Sound Library, the Music Library and the Joystick Library for every function.
+
+
+
Data Files and Saved Games
+
DOS has one current directory for everything, so a program never relies on it. appMain gets a DxeAppContextT with the paths it needs:
+
Field What it is Use it for
+ ----- ---------- ----------
+ appPath the program's .app file dvxResOpen: resources inside the .app
+ appDir the folder the .app is in read-only game data installed beside it
+ configDir the program's own settings folder saved games, high scores, options
+ helpFile the program's .hlp file shellOpenHelp (F1)
+
configDir is CONFIG plus the folder the .app is in: a game in APPS\ACME\MYGAME keeps its files in CONFIG\APPS\ACME\MYGAME.
+
+
Small data (sounds, music, a title picture) can travel inside the .app as resources: list them in NAME.res as binary (or icon, text) and read them with dvxResOpen(ctx->appPath) and dvxResRead. The template carries its sound effect and music this way.
+
Bigger data goes in files beside the .app (the DATA variable in the Makefile packages them there): build paths from appDir, never from the current directory.
+
Saved games go in configDir, which DVX keeps apart from the program files so a reinstall never overwrites them. Create it with shellEnsureConfigDir(ctx) before the first write and build names with shellConfigPath(ctx, "SAVE1.DAT", path, size). For settings and scores, prefsLoad and prefsSaveAs read and write INI files safely (a failed save leaves the old file intact).
+
Use 8.3 file names (8 characters, a dot, 3 characters).
+
platformDiskSpace tells how much room is free before a big save.
+
dvxLog writes a line to DVX.LOG, the place to look when something goes wrong.
+
+
+
+
Packaging and Ending
+
Packaging
+
make package lays out pkg/APPS/<VENDOR>/<NAME>/ with NAME.app, NAME.hlp and the files listed in DATA; zip the contents of pkg/ and the player unpacks it into the DVX directory (make install DVX_DIR=... does it for you). The .app carries its icon, name, author and description for Program Manager (NAME.res). Every game should have its own help (NAME.dhs, compiled to NAME.hlp), since F1 in a game input window goes to the game: open the help from the game's F1 and from Help > Contents. A game built on a static library of its own links it in with the LIBS variable (LIBS = port/libgame.a).
+
Ending
+
Close the game's window and return from appMain. Every sound, music, joystick handle and timer the program opened is released when it ends, however it ends, so a crash or End Task leaves nothing playing. A game (or any program) that calls exit() or abort() ends only itself, like End Task: its windows close, its timers are removed and DVX keeps running. Handlers registered with atexit are not run; clean up before calling exit(), or return from appMain. malloc and free work as usual; DVX counts each program's memory (the Task Manager shows it).
+
+
+
Games in DVX BASIC
+
newProject.sh basicgame NAME starts a BASIC game. A Timer fires once and runs the game loop; FRAMEWAIT rate waits for the next frame while the desktop and the program's forms keep running, and the loop ends when the form closes:
+
Sub tmrStart_Timer ()
+ tmrStart.Enabled = False
+ Do
+ MovePlayer ' GameKeyDown, STICK(0), STICK(1), STRIG(1)
+ DrawScreen ' a PictureBox: Clear, FillRect, FillCircle
+ FRAMEWAIT 30
+ Loop Until quitting ' set in the form's Unload event
+End Sub
+
GameKeyDown (game.bas) tells whether a key is held; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference.
+
+
+
Porting a Game Library to DVX
+
Portable game libraries for DOS-era machines usually keep everything machine-specific behind a small hardware layer: start and stop, show a frame, set the palette, read input, wait for the next frame, play sound, and find files. A DVX port replaces that layer. The rest of the library, including drawing code, sprite compilers that write x86 code into memory, and asset formats built for DOS, works unchanged, because a DVX program is 32-bit protected-mode x86 code in the same flat memory as a DOS program built with the same compiler.
+
The Model: a Window, a Task, a Virtual Frame Rate
+
The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. The game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame.
+
Mapping a Hardware Layer to DVX
+
Hardware layer DVX
+ -------------- ---
+ Start-up SHELL_APP_DESCRIPTOR (hasMainLoop), dvxCreateWindowCentered, gameInput
+ Shut-down dvxDestroyWindow, return from appMain
+ Error text, debug log dvxLog (DVX.LOG)
+ Present the back buffer colour table -> win->contentBuf, dvxInvalidateRect; onPaint
+ Palette packColor, gamePalNearest, GamePalCacheT
+ Palette per scan line one colour table per palette, chosen per row
+ Wait for vertical blank gameFrameInit, gameFrameWait
+ Frame counter, frame rate GameFramePacerT.frame, the rate given to gameFrameInit
+ Milliseconds platformMicros() / 1000
+ Held keys onRawKey, gameKeyIsDown, platformKeyStateGet
+ Typed characters onKey
+ Mouse onMouse
+ Joystick joyCount, joyOpen, joyRead
+ Periodic interrupt callback gameTimerAdd, platformLockRegion
+ Critical section platformIrqSave, platformIrqRestore
+ Audio start and stop audioOpen, audioClose
+ Sound effect slots audioSampleCreate, audioSampleLoadMem, audioPlay, audioVoiceSetPan, audioVoiceStop
+ Streamed sound audioStreamOpen, audioStreamWrite
+ Tracker modules musicLoadMem, musicPlay, musicStop, musicIsPlaying
+ Tone and noise voices audioTone, audioVoiceSetFrequency, audioVoiceSetVolume, audioNotesOpen, audioFmAlloc
+ Per-frame audio work audioPump, musicPump
+ Read-only data folder DxeAppContextT.appDir, dvxResOpen, dvxResRead
+ Save folder DxeAppContextT.configDir, shellEnsureConfigDir, shellConfigPath
+ Disk free platformDiskSpace
+ Memory malloc, free
+ Serial port the serial library (rs232Open)
+ exit() ends only the game
+
Notes on the Mapping
+
+
Start-up: make the window not resizable, with its content area the game's screen at a whole scale that fits the display; give it onClose and onPaint. audioOpen returning NULL (no sound device) is not an error.
+
Shut-down: close what the port opened and return. Anything left (sounds, music, joystick handles, timers) is released when the program ends.
+
Present: expand the rows or word bands that changed through the colour table into the content buffer, scaled, and invalidate them; onPaint does the whole screen. See The Screen: Framebuffers and Palettes.
+
Palette: scale 12-bit or 18-bit colour to 0..255 per channel before looking it up. Never touch the VGA palette registers: other programs share the screen.
+
Vertical blank: there is none in a window. The frame rate is a virtual one: use the rate the library reports (60 or 70 frames a second); truncate the frame counter to the library's width.
+
Keyboard: raw codes are set-1 scancodes with KEY_RAW_EXT for the keys sent behind 0xE0, so a DOS port's scancode table maps directly. Never hook the keyboard interrupt.
+
Mouse: divide content-area pixels by the scale and clamp.
+
Joystick: axes are -32768..32767 after calibration and dead zone; divide down to the library's range, or compare with a threshold for a digital stick. Buttons are bits. Never read the game port directly; calibration lives in the Control Panel.
+
Timer callbacks run inside the timer interrupt: no C library, no DVX calls, no floating point, code and data pinned. Never reprogram the PIT or hook the timer interrupt.
+
Sound effects: DVX's 8-bit samples are unsigned; flip the top bit of signed 8-bit data or convert it to 16-bit. Stream fill functions run in program time, not in an interrupt.
+
Files: the library's fixed "DATA" and save folders become run-time paths under appDir and configDir; use remove() to delete a save.
+
Serial: one owner per COM port, so the port uses DVX's serial library rather than programming the UART.
+
exit() ends only the game, like End Task; atexit handlers do not run.
+
+
Things to Decide
+
+
Shared library or static: built as a DVX library (.lib, loaded when DVX starts) the port is shared by every game, but its global state (the stage, the key table) means one game at a time. Linked into each game's .app (the LIBS variable) every game has its own copy, several can run at once, and nothing is loaded while no game runs.
+
Paths: libraries that open "DATA/..." or write saves to a fixed folder need a run-time root (appDir and configDir) instead of compile-time paths.
+
Speed: every frame costs a conversion and a composite. Present only what changed; a 1x scale on a 486 and larger scales on faster machines.
+
Tearing: there is no vertical retrace in a window. Games that depend on raster timing need rework.
+
The 256-colour desktop: nearest colours only; say so in the game's help if it needs 15-bit colour or better.
+
+
Checking a Port
+
The library's own drawing tests (hashes of the back buffer after each drawing call) should give the same answers as on DOS, since the drawing code is the same. Test in a 256-colour and a 16-bit desktop, with and without a sound card and a joystick, and with other programs running; End Task and exit() must leave DVX, the sound and the timer as they were.
+
SDK Quick Start
The DVX SDK holds everything needed to build DVX modules outside the DVX source tree: the public headers, the host tools, a shared set of build rules, and a template for each kind of module. A DVX build writes it to bin/sdk.
@@ -6660,7 +7442,10 @@ fileOpsJobFree(job);
app A C program (NAME.app)
widget A C widget (NAME.wgt) usable from C programs and DVX BASIC
library A C library of shared functions (NAME.lib) with BASIC declarations
- basic A DVX BASIC program (NAME.dbp, built to NAME.app)
+ basic A DVX BASIC program (NAME.dbp, built to NAME.app)
+ game A C game (NAME.app): a 320x200 screen in a window, frame pacing, keyboard and joystick, sound effects and music
+ basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG and PLAY
+
The two game templates are explained in Writing Games for DVX.
NAME is a letter followed by letters and digits, 8 characters at most, because it becomes a DOS file name. Files are named NAME with its first letter in lower case (camelCase, such as myApp); DVX shows it in upper case. The script refuses DOS device names (CON, PRN, AUX, NUL, COM1 and so on), the name of a widget DVX already has, and an existing directory. The project goes in DIR, or a new directory of that name in the current one.
The template's names become yours everywhere: file names, identifiers in each case style (tplName, TplName, tplname, TPLNAME), help topic ids and the ABI stamp. The Makefile refers to the SDK by a relative path, so a project and the SDK can move together.
make package lays out pkg/ exactly as the files go into the DVX directory; make install DVX_DIR=... copies it there. VENDOR (default local) names the vendor directory; use a short name of your own (8 characters at most).
Kind Directory Files
---- --------- -----
- Program APPS/<VENDOR>/<NAME>/ NAME.app, NAME.hlp
+ Program APPS/<VENDOR>/<NAME>/ NAME.app, NAME.hlp, the DATA files (if any)
Widget WIDGETS/<VENDOR>/<NAME>/ NAME.wgt, NAME.DHS, NAME.BHS, NAME.dep (if any)
Library LIBS/<VENDOR>/<NAME>/ NAME.lib, NAME.DHS, NAME.dep (if any)
Programs carry their compiled help. Widgets and libraries carry help sources, which DVX compiles into its system help when the set of installed modules changes.
.dep files are written with DOS line endings.
+
A program's data files (levels, graphics, sounds it loads at run time) go in the Makefile's DATA variable, for example DATA = levels sounds; the package puts them beside NAME.app, where the program finds them through appDir in its DxeAppContextT. Small ones can instead travel inside the .app as binary resources (see the .res file).
To ship a module, zip the contents of pkg/; the user unpacks it into the DVX directory. A library's header and .bas file go to the developers who use it, not into the DVX directory.
Removing a module is deleting its directory.
@@ -7180,6 +7967,73 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to
scrollPos [in/out] Current scroll offset; updated in place when the click hits the scrollbar.
Returns true if the click landed on the scrollbar (even if scrollPos was not changed, for example clicking the thumb), false if the click was in the item list area. When itemCount is less than or equal to DROPDOWN_MAX_VISIBLE, always returns false (there is no scrollbar).
+
+
Music Library
+
Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
+
Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank is bin/libs/kpunch/music/genmidi.op2.
musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state) and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
+
Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time.
+
Where Music Plays
+
Modules are rendered by libxmp-lite into a stream of the sound device (16-bit stereo at the device's rate, 400 ms of buffer, linear interpolation), so they play on any device that plays sampled sound.
+
MIDI goes to the best synthesizer there is, decided at the first musicPlay (or musicMidiOpen), with DVX.INI [audio] midi:
+
midi MIDI plays on
+ ---- -------------
+ auto the FM chip (OPL2 / OPL3) when there is one and no program has taken the whole chip; else the software synthesizer
+ mpu401 the MPU-401 port (mpuPort, default 330h) when it was found and no other music is using it; else as auto
+ fm as auto
+ software the software synthesizer
+
On the FM chip and the MPU-401 port the song is played by the timer service (500 times a second), so it keeps time however busy the programs are. On the FM chip every note takes a voice from audio.lib's shared pool; when none is free the note plays on the software synthesizer instead, which then runs beside the chip (its stream is always open for that). The software synthesizer runs in the stream's fill function (the shell's idle time), 64 frames between event checks, 150 ms ahead of what is heard; a program that keeps the desktop busy every frame should call audioPump once a frame, as for any stream.
+
General MIDI on FM
+
Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank names another OP2 file instead (a path from the DVX folder), for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. The bank is read once, at the first MIDI music that needs it.
+
What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.
+
The Software Synthesizer
+
When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.
+
The MPU-401 Port
+
With DVX.INI [audio] midi = mpu401, MIDI goes out of the MPU-401 port (audio.lib's mpu401 driver) to an external synthesizer, one music at a time (a second MIDI music meanwhile plays on the FM chip or the software synthesizer). Bytes are queued and sent by the timer as fast as the port takes them, with a 40 ms pause after every system exclusive message (first-generation MT-32s lose data without it). The music's volume scales every channel volume (CC7) sent. DVX.INI [audio] mpuMode says what is on the port:
+
mpuMode Treatment
+ ------- ---------
+ gm A General MIDI synthesizer (Roland SC-55 and the like): the music as it is, after a General MIDI System On message.
+ mt32 A Roland MT-32 (or CM-32L, LAPC-I) playing General MIDI music: the melodic channels move to the MT-32's eight parts (MIDI channels 2 to 9, in the order they are first used; a ninth channel shares the last part), percussion stays on channel 10, programs go through a General MIDI to MT-32 timbre table (our own nearest-sound choice), pan is mirrored (the MT-32 pans the other way), the bend range is set to 2 semitones by system exclusive messages at the start (and RPN 0 changes it the same way, since the MT-32 has no RPNs), and what it does not understand (other RPNs and NRPNs, bank select, reverb and chorus depth, aftertouch, system exclusive messages that are not Roland's) is left out.
+ raw Anything: the music exactly as it is, nothing added (for music written for an MT-32, with its own set-up messages).
+
Stopping, pausing or freeing a music sends sustain off and all notes off on every channel the synthesizer hears.
+
Live MIDI
+
MusicT *keys = NULL;
+
+musicMidiOpen(&keys); // plays at once
+musicMidiProgram(keys, 0, 73); // flute on channel 0
+musicMidiNoteOn(keys, 0, 60, 100); // middle C
+musicMidiNoteOff(keys, 0, 60);
+
musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.
+
Modules
+
MOD (ProTracker and compatibles), S3M, XM and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.
A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
+
Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML) or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
+
Several Programs at Once
+
Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music. When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.
+
BASIC
+
src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it.
+
The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount and musicBasBusy for the statements BEEP, SOUND, PLAY and the PLAY(n) function; each BASIC program has its own macro queue. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
+
Licences
+
libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
The GENMIDI instrument bank is from Freedoom 0.13.0, Copyright (C) 2001-2024 Contributors to the Freedoom project. All rights reserved. BSD 3-clause licence:
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Freedoom project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
Printing Library
Printer ports and print jobs, text laid out on pages, pictures for dot-matrix and laser printers, the printer settings and the common Print and Page Setup dialogs. Notepad, Image Viewer, Icon Editor, the Control Panel's Printer page and BASIC's LPRINT all print through it; any program can.
diff --git a/installToolchain.sh b/installToolchain.sh
index f69062c8..bed16338 100755
--- a/installToolchain.sh
+++ b/installToolchain.sh
@@ -35,10 +35,12 @@
# extracted once into app/ (no FUSE needed), and
# the matching 86Box ROM set (tag v6.0) in roms/
# toolchains/freedos FreeDOS 1.3 master boot record code, FAT16 boot
-# sector, KERNEL.SYS and COMMAND.COM (from the
+# sector, KERNEL.SYS, COMMAND.COM and for CD-ROMs
+# HIMEMX.EXE, UDVD2.SYS and SHSUCDX.COM (from the
# LiteUSB disk image) and CuteMouse 2.1b4
# CTMOUSE.EXE: the DOS the emulator test disk boots
-# (src/test/dos)
+# (src/test/dos; the CD-ROM pieces for the
+# installer test, # INSTALL: cd)
# toolchains/dosbox DOSBox Staging v0.83.0 (the quick emulator)
# toolchains/cppcheck cppcheck 2.21, built from its source release
# (src/test/lint/staticGate.sh)
@@ -47,6 +49,10 @@
# toolchains/pktdrv NE2000.COM packet driver (src/test/net/
# installPktDrv.sh, which pins it) for the DOS
# network tests
+# toolchains/emu8950 the music tests' references (src/test/music/
+# toolchains/soundfont installMusicRefs.sh, which pins them): the
+# emu8950 OPL2 emulator (host suite) and a General
+# MIDI sound font for 86Box's MIDI (DOS tests)
#
# Every download is checked against a pinned SHA-256 before it is unpacked,
# so a changed or corrupted archive is refused instead of installed.
@@ -128,7 +134,7 @@ if [ "$(uname -s)" != "Linux" ] || [ "$(uname -m)" != "x86_64" ]; then
exit 1
fi
-for tool in curl sha256sum tar bzip2 xz unzip make g++ mcopy xvfb-run; do
+for tool in curl sha256sum tar bzip2 xz unzip make g++ mcopy xvfb-run ar; do
if ! command -v "${tool}" >/dev/null 2>&1; then
echo "installToolchain: required tool '${tool}' is not installed" >&2
exit 1
@@ -224,8 +230,10 @@ else
echo "installToolchain: 86Box ROM set installed in ${BOX86_ROMS}"
fi
-# FreeDOS boot files and CuteMouse
-FREEDOS_PIN="${FREEDOS_SHA256} ${CTMOUSE_SHA256}"
+# FreeDOS boot files, the CD-ROM driver and CuteMouse. The pin names the
+# files taken from the image, so a change to the list installs again.
+FREEDOS_IMG_FILES="KERNEL.SYS COMMAND.COM FREEDOS/BIN/HIMEMX.EXE FREEDOS/BIN/UDVD2.SYS FREEDOS/BIN/SHSUCDX.COM"
+FREEDOS_PIN="${FREEDOS_SHA256} ${CTMOUSE_SHA256} ${FREEDOS_IMG_FILES// /,}"
if [ "${FORCE}" -eq 0 ] && pinned "${FREEDOS_DIR}" "${FREEDOS_PIN}"; then
echo "installToolchain: FreeDOS boot files already installed"
@@ -237,7 +245,9 @@ else
unzip -p "${CACHE}/${FREEDOS_FILE}" "${FREEDOS_DISK_IMG}" > "${FREEDOS_DIR}.new/disk.img"
dd if="${FREEDOS_DIR}.new/disk.img" of="${FREEDOS_DIR}.new/MBR.BIN" bs="${MBR_CODE_BYTES}" count=1 status=none
dd if="${FREEDOS_DIR}.new/disk.img" of="${FREEDOS_DIR}.new/BOOTSECT.BIN" bs=512 skip="${FREEDOS_PART_SECTOR}" count=1 status=none
- MTOOLS_SKIP_CHECK=1 mcopy -n -i "${FREEDOS_DIR}.new/disk.img@@$((FREEDOS_PART_SECTOR * 512))" ::KERNEL.SYS ::COMMAND.COM "${FREEDOS_DIR}.new/"
+ for f in ${FREEDOS_IMG_FILES}; do
+ MTOOLS_SKIP_CHECK=1 mcopy -n -i "${FREEDOS_DIR}.new/disk.img@@$((FREEDOS_PART_SECTOR * 512))" "::${f}" "${FREEDOS_DIR}.new/${f##*/}"
+ done
unzip -p "${CACHE}/${CTMOUSE_FILE}" BIN/CTMOUSE.EXE > "${FREEDOS_DIR}.new/CTMOUSE.EXE"
rm -f "${FREEDOS_DIR}.new/disk.img"
rm -rf "${FREEDOS_DIR}"
@@ -295,13 +305,20 @@ else
"${ROOT}/src/test/net/installPktDrv.sh"
fi
+# References for the music tests, pinned in their own script
+if [ "${FORCE}" -eq 1 ]; then
+ "${ROOT}/src/test/music/installMusicRefs.sh" --force
+else
+ "${ROOT}/src/test/music/installMusicRefs.sh"
+fi
+
# Verify what the build needs.
if [ "$("${DJGPP_GCC}" -dumpfullversion)" != "${DJGPP_VERSION}" ]; then
echo "installToolchain: ${DJGPP_GCC} is not GCC ${DJGPP_VERSION}" >&2
exit 1
fi
-for f in "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/dxe3gen" "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/exe2coff" "${CWSDSTUB}" "${BOX86_BIN}" "${BOX86_ROMS}/machines" "${FREEDOS_DIR}/MBR.BIN" "${FREEDOS_DIR}/BOOTSECT.BIN" "${FREEDOS_DIR}/KERNEL.SYS" "${FREEDOS_DIR}/COMMAND.COM" "${FREEDOS_DIR}/CTMOUSE.EXE" "${DOSBOX_BIN}" "${CPPCHECK_BIN}" "${TOOLCHAINS}/lrzsz/bin/lsz" "${TOOLCHAINS}/pktdrv/NE2000.COM"; do
+for f in "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/dxe3gen" "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/exe2coff" "${CWSDSTUB}" "${BOX86_BIN}" "${BOX86_ROMS}/machines" "${FREEDOS_DIR}/MBR.BIN" "${FREEDOS_DIR}/BOOTSECT.BIN" "${FREEDOS_DIR}/KERNEL.SYS" "${FREEDOS_DIR}/COMMAND.COM" "${FREEDOS_DIR}/HIMEMX.EXE" "${FREEDOS_DIR}/UDVD2.SYS" "${FREEDOS_DIR}/SHSUCDX.COM" "${FREEDOS_DIR}/CTMOUSE.EXE" "${DOSBOX_BIN}" "${CPPCHECK_BIN}" "${TOOLCHAINS}/lrzsz/bin/lsz" "${TOOLCHAINS}/pktdrv/NE2000.COM" "${TOOLCHAINS}/emu8950/emu8950.c" "${TOOLCHAINS}/soundfont/TimGM6mb.sf2"; do
if [ ! -e "${f}" ]; then
echo "installToolchain: expected file missing after install: ${f}" >&2
exit 1
diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile
index 3e55e9c4..9794f683 100644
--- a/src/apps/kpunch/Makefile
+++ b/src/apps/kpunch/Makefile
@@ -46,20 +46,9 @@ BASCOMP = ../../../bin/host/bascomp
# BASIC-only notepad, imgview, etc. replace the old C versions.
BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow
-# DOS batch files an app puts in the DVX folder itself (/dosroot/*.bat,
-# e.g. setup's install.bat), discovered like the apps, with DOS line endings.
-ROOT_BATS = $(wildcard */dosroot/*.bat)
-ROOT_BAT_OUT = $(addprefix $(BINROOT)/,$(notdir $(ROOT_BATS)))
-
-vpath %.bat $(sort $(dir $(ROOT_BATS)))
-
.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
-all: $(C_APPS) dvxbasic $(BASIC_APPS) $(ROOT_BAT_OUT)
-
-$(BINROOT)/%.bat: %.bat $(DVX_MAKEFILES)
- @mkdir -p $(BINROOT)
- sed 's/$$/\r/' $< > $@
+all: $(C_APPS) dvxbasic $(BASIC_APPS)
dvxbasic:
$(MAKE) -C dvxbasic
@@ -145,7 +134,7 @@ $(BINDIR)/kpunch/widshow/widshow.app: widshow/widshow.dbp widshow/widshow.frm wi
# the dynamic loader ignores). SAN/COV per common.mk.
HOSTOBJDIR = $(HOST_OBJROOT)/apps
HOSTCFLAGS = $(HOST_CFLAGS) $(APP_INCLUDES)
-HOSTLDFLAGS = -shared $(HOST_LDFLAGS)
+HOSTLDFLAGS = -shared $(HOST_LDFLAGS) $(HOST_APP_LDFLAGS)
HOST_SOS = $(foreach n,$(C_APPS),$(HOSTOBJDIR)/$(n).so)
appHostObjs = $(patsubst $(1)/%.c,$(HOSTOBJDIR)/$(1)/%.o,$(call appSrcs,$(1)))
@@ -185,7 +174,6 @@ $(BINDIR)/kpunch/widshow: ; mkdir -p $@
clean:
rm -f $(C_APP_OBJS) $(C_APP_OBJS:.o=.d)
rm -f $(foreach a,$(C_APPS),$(BINDIR)/kpunch/$(a)/$(a).app)
- rm -f $(ROOT_BAT_OUT)
rm -f $(addprefix $(BINDIR)/kpunch/dvxdemo/,$(DVXDEMO_BMPS))
rm -f $(BINDIR)/kpunch/iconed/iconed.app
rm -f $(BINDIR)/kpunch/notepad/notepad.app
diff --git a/src/apps/kpunch/README.md b/src/apps/kpunch/README.md
index e0b531bb..d8033865 100644
--- a/src/apps/kpunch/README.md
+++ b/src/apps/kpunch/README.md
@@ -393,9 +393,6 @@ The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
compiled and linked with `dxe3gen -U` into `.app`, then
resources are attached with `dvxres build` if `/.res`
exists. No list needs editing.
-* **DOS batch files** an app ships in the DVX folder itself
- (`/dosroot/*.bat`, e.g. `setup/dosroot/install.bat`) are
- discovered the same way and copied to `bin/` with DOS line endings.
* **BASIC apps** run `bascomp .dbp -o .app -release`,
which handles project-file parsing, compilation, resource
attachment, and linking into `basstub.app`.
diff --git a/src/apps/kpunch/cpanel/cpJoystick.c b/src/apps/kpunch/cpanel/cpJoystick.c
new file mode 100644
index 00000000..03954417
--- /dev/null
+++ b/src/apps/kpunch/cpanel/cpJoystick.c
@@ -0,0 +1,746 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// cpJoystick.c -- Control Panel's Game Controllers page (see cpJoystick.h).
+//
+// The joystick library is looked up by name when the page is built, so
+// the Control Panel still works where it is not installed (the page then
+// says so). The page holds a handle on the chosen controller: a timer
+// redraws the test view from it, and the calibration wizard reads its raw
+// values. Every change (calibration, dead zone, game port mode, keyboard
+// joystick) applies at once through the library; OK stores the settings
+// in DVX.INI, Cancel loads them back from the copy read when the Control
+// Panel opened.
+
+#include "cpJoystick.h"
+#include "joystick/joystick.h"
+#include "dvxDlg.h"
+#include "dvxPlat.h"
+#include "box/box.h"
+#include "button/button.h"
+#include "canvas/canvas.h"
+#include "dropdown/dropdown.h"
+#include "label/label.h"
+#include "slider/slider.h"
+#include "timer/timer.h"
+#include "stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define CPJ_LABEL_W 90
+#define CPJ_GAP 4
+#define CPJ_BTN_W 110
+#define CPJ_SMALL_BTN_W 70
+#define CPJ_TICK_MS 50
+#define CPJ_VIEW_W 320
+#define CPJ_VIEW_H 128
+#define CPJ_BOX 128 // the X / Y square
+#define CPJ_DOT_R 3
+#define CPJ_BAR_X 144 // axis bars, buttons and hat, right of the square
+#define CPJ_BAR_W 140
+#define CPJ_BAR_H 10
+#define CPJ_ROW_H 18
+#define CPJ_SHOWN_AXES 4
+#define CPJ_BTN_Y (CPJ_SHOWN_AXES * CPJ_ROW_H + CPJ_GAP)
+#define CPJ_BTN_BOX 18
+#define CPJ_SHOWN_BTNS 8
+#define CPJ_HAT_Y (CPJ_BTN_Y + CPJ_BTN_BOX + CPJ_GAP)
+#define CPJ_STATUS_Y (CPJ_HAT_Y + CPJ_ROW_H)
+#define CPJ_ZONE_LBL_W 40
+#define CPJ_TEXT_BUF 128
+#define CPJ_KEY_BUF 32
+#define CPJ_KEYS_DLG_W 240
+#define CPJ_WIZ_DLG_W 470
+#define CPJ_WIZ_CHARS 56 // the wizard's text lines keep this width as their text changes
+#define CPJ_SPAN_DIV 4 // each side of an axis's centre must reach centre / 4 away
+#define CPJ_AXIS_SPAN ((int64_t)JOY_AXIS_MAX - JOY_AXIS_MIN)
+
+// The joystick library's functions (NULL when it is not installed)
+typedef struct {
+ __typeof__(joyClose) *close;
+ __typeof__(joyCount) *count;
+ __typeof__(joyGetDeadZone) *getDeadZone;
+ __typeof__(joyGetInfo) *getInfo;
+ __typeof__(joyGetRaw) *getRaw;
+ __typeof__(joyKeyCount) *keyCount;
+ __typeof__(joyKeyName) *keyName;
+ __typeof__(joyOpen) *open;
+ __typeof__(joyRead) *read;
+ __typeof__(joyRescan) *rescan;
+ __typeof__(joySetCalibration) *setCal;
+ __typeof__(joySetDeadZone) *setDeadZone;
+ __typeof__(joySettingGet) *settingGet;
+ __typeof__(joySettingSet) *settingSet;
+ __typeof__(joySettingsLoad) *load;
+ __typeof__(joySettingsStore) *store;
+} JoyFnsT;
+
+typedef struct {
+ const char *name;
+ size_t offset;
+} JoyFnSymT;
+
+// The calibration wizard's steps
+typedef enum {
+ WizCenterE,
+ WizEdgesE,
+ WizButtonsE
+} WizStepE;
+
+typedef struct {
+ WindowT *dlg;
+ WidgetT *text;
+ WidgetT *values;
+ WidgetT *next;
+ JoyInfoT info;
+ JoyAxisCalT cal[JOY_MAX_AXES];
+ WizStepE step;
+} CalWizT;
+
+// ============================================================
+// Tables
+// ============================================================
+
+#define JOY_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(JoyFnsT, field) }
+
+static const JoyFnSymT sSyms[] = {
+ JOY_FN_SYM(close, joyClose),
+ JOY_FN_SYM(count, joyCount),
+ JOY_FN_SYM(getDeadZone, joyGetDeadZone),
+ JOY_FN_SYM(getInfo, joyGetInfo),
+ JOY_FN_SYM(getRaw, joyGetRaw),
+ JOY_FN_SYM(keyCount, joyKeyCount),
+ JOY_FN_SYM(keyName, joyKeyName),
+ JOY_FN_SYM(open, joyOpen),
+ JOY_FN_SYM(read, joyRead),
+ JOY_FN_SYM(rescan, joyRescan),
+ JOY_FN_SYM(setCal, joySetCalibration),
+ JOY_FN_SYM(setDeadZone, joySetDeadZone),
+ JOY_FN_SYM(settingGet, joySettingGet),
+ JOY_FN_SYM(settingSet, joySettingSet),
+ JOY_FN_SYM(load, joySettingsLoad),
+ JOY_FN_SYM(store, joySettingsStore)
+};
+
+// [joystick] gameport.mode and fallback: what the dropdowns show and store
+static const char *sModeLabels[] = { "Two joysticks", "One joystick with 4 axes" };
+static const char *sModeValues[] = { JOY_GAMEPORT_TWO, JOY_GAMEPORT_FOUR };
+
+static const char *sFallbackLabels[] = { "When no joystick is found", "Always", "Never" };
+static const char *sFallbackValues[] = { JOY_FALLBACK_AUTO, JOY_FALLBACK_ALWAYS, JOY_FALLBACK_NEVER };
+
+static const char *const sAxisNames[CPJ_SHOWN_AXES] = { "X", "Y", "3", "4" };
+
+static const char *const sSlotNames[JOY_KBD_SLOT_COUNT] = { JOY_KBD_SLOT_NAMES };
+static const char *const sSlotDefaults[JOY_KBD_SLOT_COUNT] = { JOY_KBD_SLOT_DEFAULTS };
+static const char *const sSlotLabels[JOY_KBD_SLOT_COUNT] = { "Up:", "Down:", "Left:", "Right:", "Button 1:", "Button 2:", "Button 3:", "Button 4:" };
+
+static const char *const sWizText[] = {
+ "Let the stick rest in the middle, then click Next.",
+ "Move the stick round all its edges, then click Next.",
+ "Press each button to test it, then click Finish."
+};
+
+// ============================================================
+// Module state
+// ============================================================
+
+static JoyFnsT sJoy;
+static AppContextT *sAc = NULL;
+static PrefsHandleT *sPrefs = NULL;
+static WidgetT *sDevDrop = NULL;
+static WidgetT *sView = NULL;
+static WidgetT *sCalBtn = NULL;
+static WidgetT *sResetBtn = NULL;
+static WidgetT *sZoneSldr = NULL;
+static WidgetT *sZoneLbl = NULL;
+static WidgetT *sModeDrop = NULL;
+static WidgetT *sKbdDrop = NULL;
+static WidgetT *sKeyDrops[JOY_KBD_SLOT_COUNT];
+static const char **sKeyItems = NULL; // stb_ds array of the key names
+static int32_t sDevice = -1;
+static int32_t sHandle = -1;
+static JoyStateT sShown;
+static bool sShownValid = false;
+static char sDevNames[JOY_MAX_DEVICES][JOY_NAME_MAX];
+static const char *sDevLabels[JOY_MAX_DEVICES];
+static CalWizT sWiz;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static WidgetT *addRow(WidgetT *parent, const char *label);
+static int32_t axisPixel(int32_t value, int32_t size);
+void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
+void cpJoystickFree(void);
+void cpJoystickRevert(void);
+void cpJoystickStore(PrefsHandleT *prefs);
+static void drawView(void);
+static int32_t findValue(const char *const *values, int32_t count, const char *value);
+static void onCalibrate(WidgetT *w);
+static void onDetect(WidgetT *w);
+static void onDevice(WidgetT *w);
+static void onFallback(WidgetT *w);
+static void onKeys(WidgetT *w);
+static bool onKeysOk(WindowT *dlg, void *userCtx);
+static void onMode(WidgetT *w);
+static void onReset(WidgetT *w);
+static void onTick(WidgetT *w);
+static void onWizCancel(WidgetT *w);
+static void onWizNext(WidgetT *w);
+static void onWizTick(WidgetT *w);
+static void onZone(WidgetT *w);
+static void refreshDevices(void);
+static bool resolveLibrary(void);
+static void selectDevice(int32_t device);
+static void showZone(void);
+static void slotKey(int32_t slot, char *out, int32_t outSize);
+static bool wizAxis(int32_t axis);
+
+
+// "Label:" and the row for its controls.
+static WidgetT *addRow(WidgetT *parent, const char *label) {
+ WidgetT *row = wgtHBox(parent);
+ WidgetT *lbl = wgtLabel(row, label);
+
+ row->spacing = wgtPixels(CPJ_GAP);
+ lbl->prefW = wgtPixels(CPJ_LABEL_W);
+ return row;
+}
+
+
+// An axis value as a pixel offset 0..size-1.
+static int32_t axisPixel(int32_t value, int32_t size) {
+ return (int32_t)(((int64_t)value - JOY_AXIS_MIN) * (size - 1) / CPJ_AXIS_SPAN);
+}
+
+
+void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
+ sAc = ac;
+ sPrefs = prefs;
+ sDevice = -1;
+ sHandle = -1;
+ sShownValid = false;
+
+ if (!resolveLibrary()) {
+ wgtLabel(page, "Game controller support is not installed.");
+ return;
+ }
+
+ WidgetT *row = addRow(page, "Controller:");
+
+ sDevDrop = wgtDropdown(row);
+ sDevDrop->weight = WGT_WEIGHT_FILL;
+ sDevDrop->onChange = onDevice;
+
+ WidgetT *detect = wgtButton(row, "Detect");
+
+ detect->onClick = onDetect;
+ detect->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+ sView = wgtCanvas(page, CPJ_VIEW_W, CPJ_VIEW_H);
+ row = wgtHBox(page);
+ row->spacing = wgtPixels(CPJ_GAP);
+ sCalBtn = wgtButton(row, "Calibrate...");
+ sCalBtn->onClick = onCalibrate;
+ sCalBtn->prefW = wgtPixels(CPJ_BTN_W);
+ sResetBtn = wgtButton(row, "Automatic");
+ sResetBtn->onClick = onReset;
+ sResetBtn->prefW = wgtPixels(CPJ_BTN_W);
+
+ wgtLabel(row, "Dead zone:");
+ sZoneSldr = wgtSlider(row, 0, JOY_DEADZONE_MAX);
+ sZoneSldr->weight = WGT_WEIGHT_FILL;
+ sZoneSldr->onChange = onZone;
+ sZoneLbl = wgtLabel(row, "");
+ sZoneLbl->prefW = wgtPixels(CPJ_ZONE_LBL_W);
+
+ row = addRow(page, "Game port:");
+ sModeDrop = wgtDropdown(row);
+ sModeDrop->onChange = onMode;
+ wgtDropdownSetItems(sModeDrop, sModeLabels, DVX_ARRAY_LEN(sModeLabels));
+ wgtDropdownSetSelected(sModeDrop, findValue(sModeValues, DVX_ARRAY_LEN(sModeValues), sJoy.settingGet(JOY_KEY_GAMEPORT_MODE, JOY_GAMEPORT_TWO)));
+
+ row = addRow(page, "Keyboard:");
+ sKbdDrop = wgtDropdown(row);
+ sKbdDrop->onChange = onFallback;
+ wgtDropdownSetItems(sKbdDrop, sFallbackLabels, DVX_ARRAY_LEN(sFallbackLabels));
+ wgtDropdownSetSelected(sKbdDrop, findValue(sFallbackValues, DVX_ARRAY_LEN(sFallbackValues), sJoy.settingGet(JOY_KEY_FALLBACK, JOY_FALLBACK_AUTO)));
+
+ WidgetT *keys = wgtButton(row, "Keys...");
+
+ keys->onClick = onKeys;
+ keys->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+
+ WidgetT *timer = wgtTimer(page, CPJ_TICK_MS, true);
+
+ timer->onChange = onTick;
+ refreshDevices();
+}
+
+
+void cpJoystickFree(void) {
+ if (sJoy.close) {
+ sJoy.close(sHandle);
+ }
+
+ sHandle = -1;
+ arrfree(sKeyItems);
+ sKeyItems = NULL;
+}
+
+
+void cpJoystickRevert(void) {
+ if (sJoy.load) {
+ sJoy.load(sPrefs);
+ }
+}
+
+
+void cpJoystickStore(PrefsHandleT *prefs) {
+ if (sJoy.store) {
+ sJoy.store(prefs);
+ }
+}
+
+
+// The test view: the X / Y position in the square, a bar per axis, the
+// buttons (lit while down), the hat, and whether it answers.
+static void drawView(void) {
+ const ColorSchemeT *cs = &sAc->colors;
+ JoyInfoT info;
+
+ wgtCanvasClear(sView, cs->windowFace);
+ wgtCanvasSetPenColor(sView, cs->contentFg);
+
+ if (sDevice < 0 || !sJoy.getInfo(sDevice, &info)) {
+ wgtCanvasDrawText(sView, 0, 0, "No game controllers found.");
+ return;
+ }
+
+ if (sShownValid && !sShown.connected) {
+ wgtCanvasDrawText(sView, CPJ_BAR_X, CPJ_STATUS_Y, "Not connected");
+ }
+
+ wgtCanvasSetPenColor(sView, cs->contentBg);
+ wgtCanvasFillRect(sView, 0, 0, CPJ_BOX, CPJ_BOX);
+ wgtCanvasSetPenColor(sView, cs->windowShadow);
+ wgtCanvasDrawRect(sView, 0, 0, CPJ_BOX, CPJ_BOX);
+ wgtCanvasDrawLine(sView, CPJ_BOX / 2, 0, CPJ_BOX / 2, CPJ_BOX - 1);
+ wgtCanvasDrawLine(sView, 0, CPJ_BOX / 2, CPJ_BOX - 1, CPJ_BOX / 2);
+ wgtCanvasSetPenColor(sView, cs->activeTitleBg);
+ wgtCanvasFillCircle(sView, axisPixel(sShown.axes[0], CPJ_BOX), axisPixel(sShown.axes[1], CPJ_BOX), CPJ_DOT_R);
+
+ int32_t barX = CPJ_BAR_X + sAc->font.charWidth * 2;
+
+ for (int32_t a = 0; a < info.axisCount && a < CPJ_SHOWN_AXES; a++) {
+ int32_t y = a * CPJ_ROW_H;
+
+ wgtCanvasSetPenColor(sView, cs->contentFg);
+ wgtCanvasDrawText(sView, CPJ_BAR_X, y, sAxisNames[a]);
+ wgtCanvasSetPenColor(sView, cs->contentBg);
+ wgtCanvasFillRect(sView, barX, y + CPJ_GAP, CPJ_BAR_W, CPJ_BAR_H);
+
+ if (info.axisPresent & (1U << a)) {
+ wgtCanvasSetPenColor(sView, cs->activeTitleBg);
+ wgtCanvasFillRect(sView, barX, y + CPJ_GAP, axisPixel(sShown.axes[a], CPJ_BAR_W) + 1, CPJ_BAR_H);
+ }
+
+ wgtCanvasSetPenColor(sView, cs->windowShadow);
+ wgtCanvasDrawRect(sView, barX, y + CPJ_GAP, CPJ_BAR_W, CPJ_BAR_H);
+ }
+
+ for (int32_t b = 0; b < info.buttonCount && b < CPJ_SHOWN_BTNS; b++) {
+ int32_t x = CPJ_BAR_X + b * (CPJ_BTN_BOX + CPJ_GAP);
+ bool down = (sShown.buttons & (1U << b)) != 0;
+ char digit[2];
+
+ digit[0] = (char)('1' + b);
+ digit[1] = '\0';
+ wgtCanvasSetPenColor(sView, down ? cs->activeTitleBg : cs->contentBg);
+ wgtCanvasFillRect(sView, x, CPJ_BTN_Y, CPJ_BTN_BOX, CPJ_BTN_BOX);
+ wgtCanvasSetPenColor(sView, cs->windowShadow);
+ wgtCanvasDrawRect(sView, x, CPJ_BTN_Y, CPJ_BTN_BOX, CPJ_BTN_BOX);
+ wgtCanvasSetPenColor(sView, down ? cs->activeTitleFg : cs->contentFg);
+ wgtCanvasDrawText(sView, x + (CPJ_BTN_BOX - sAc->font.charWidth) / 2, CPJ_BTN_Y + (CPJ_BTN_BOX - sAc->font.charHeight) / 2, digit);
+ }
+
+ if (info.hatCount > 0) {
+ char text[CPJ_TEXT_BUF];
+
+ if (sShown.hats[0] == JOY_HAT_CENTERED) {
+ snprintf(text, sizeof(text), "Hat: centred");
+ } else {
+ snprintf(text, sizeof(text), "Hat: %" PRId32 " degrees", sShown.hats[0]);
+ }
+
+ wgtCanvasSetPenColor(sView, cs->contentFg);
+ wgtCanvasDrawText(sView, CPJ_BAR_X, CPJ_HAT_Y, text);
+ }
+}
+
+
+// The index of value in values (case-insensitive), 0 when not there.
+static int32_t findValue(const char *const *values, int32_t count, const char *value) {
+ for (int32_t i = 0; i < count; i++) {
+ if (strcasecmp(values[i], value) == 0) {
+ return i;
+ }
+ }
+
+ return 0;
+}
+
+
+// The wizard reads raw values through the page's handle, which keeps the
+// polling going while the dialog is up.
+static void onCalibrate(WidgetT *w) {
+ (void)w;
+
+ if (sDevice < 0 || !sJoy.getInfo(sDevice, &sWiz.info)) {
+ return;
+ }
+
+ sWiz.dlg = dvxDialogCreate(sAc, "Calibrate", CPJ_WIZ_DLG_W);
+
+ if (!sWiz.dlg) {
+ return;
+ }
+
+ WidgetT *root = sWiz.dlg->widgetRoot;
+
+ sWiz.step = WizCenterE;
+ sWiz.text = wgtLabel(root, sWizText[WizCenterE]);
+ sWiz.text->minW = wgtChars(CPJ_WIZ_CHARS);
+ sWiz.values = wgtLabel(root, "");
+ sWiz.values->minW = wgtChars(CPJ_WIZ_CHARS);
+
+ WidgetT *row = wgtHBox(root);
+
+ row->align = AlignEndE;
+ row->spacing = wgtPixels(CPJ_GAP);
+ sWiz.next = wgtButton(row, "Next");
+ sWiz.next->onClick = onWizNext;
+ sWiz.next->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+
+ WidgetT *cancel = wgtButton(row, "Cancel");
+
+ cancel->onClick = onWizCancel;
+ cancel->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+
+ WidgetT *timer = wgtTimer(root, CPJ_TICK_MS, true);
+
+ timer->onChange = onWizTick;
+ onWizTick(NULL);
+ dvxDialogRun(sAc, sWiz.dlg);
+ sWiz.dlg = NULL;
+ sShownValid = false;
+}
+
+
+// Look for controllers again. The page's own handle is closed first;
+// another program's handle stops the search.
+static void onDetect(WidgetT *w) {
+ (void)w;
+ sJoy.close(sHandle);
+ sHandle = -1;
+
+ if (sJoy.rescan() < 0) {
+ dvxMessageBox(sAc, "Game Controllers", "A program is using a game controller. Close it, then click Detect again.", MB_OK | MB_ICONWARNING);
+ }
+
+ refreshDevices();
+}
+
+
+static void onDevice(WidgetT *w) {
+ selectDevice(wgtDropdownGetSelected(w));
+}
+
+
+static void onFallback(WidgetT *w) {
+ sJoy.settingSet(JOY_KEY_FALLBACK, sFallbackValues[wgtDropdownGetSelected(w)]);
+ onDetect(w);
+}
+
+
+// One dropdown of key names per keyboard joystick key.
+static void onKeys(WidgetT *w) {
+ char key[CPJ_KEY_BUF];
+
+ (void)w;
+
+ if (!sKeyItems) {
+ for (int32_t i = 0; i < sJoy.keyCount(); i++) {
+ arrput(sKeyItems, sJoy.keyName(i));
+ }
+ }
+
+ WindowT *dlg = dvxDialogCreate(sAc, "Keyboard Joystick", CPJ_KEYS_DLG_W);
+
+ if (!dlg) {
+ return;
+ }
+
+ for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) {
+ slotKey(i, key, sizeof(key));
+ sKeyDrops[i] = wgtDropdown(addRow(dlg->widgetRoot, sSlotLabels[i]));
+ wgtDropdownSetItems(sKeyDrops[i], sKeyItems, (int32_t)arrlen(sKeyItems));
+ wgtDropdownSetSelected(sKeyDrops[i], findValue(sKeyItems, (int32_t)arrlen(sKeyItems), sJoy.settingGet(key, sSlotDefaults[i])));
+ }
+
+ dvxDialogAddOkCancel(dlg, "OK", onKeysOk, NULL);
+ dvxDialogRun(sAc, dlg);
+}
+
+
+static bool onKeysOk(WindowT *dlg, void *userCtx) {
+ char key[CPJ_KEY_BUF];
+
+ (void)dlg;
+ (void)userCtx;
+
+ for (int32_t i = 0; i < JOY_KBD_SLOT_COUNT; i++) {
+ slotKey(i, key, sizeof(key));
+ sJoy.settingSet(key, sKeyItems[wgtDropdownGetSelected(sKeyDrops[i])]);
+ }
+
+ return true;
+}
+
+
+// The game port makes its joysticks anew in the other mode.
+static void onMode(WidgetT *w) {
+ sJoy.settingSet(JOY_KEY_GAMEPORT_MODE, sModeValues[wgtDropdownGetSelected(w)]);
+ onDetect(w);
+}
+
+
+// Forget the calibration: every axis ranges automatically again.
+static void onReset(WidgetT *w) {
+ (void)w;
+
+ for (int32_t a = 0; a < JOY_MAX_AXES; a++) {
+ sJoy.setCal(sDevice, a, NULL);
+ }
+}
+
+
+// Redraw the view when the state changed.
+static void onTick(WidgetT *w) {
+ JoyStateT st;
+
+ (void)w;
+
+ if (!sJoy.read(sHandle, &st) || (sShownValid && memcmp(&st, &sShown, sizeof(st)) == 0)) {
+ return;
+ }
+
+ sShown = st;
+ sShownValid = true;
+ drawView();
+}
+
+
+static void onWizCancel(WidgetT *w) {
+ (void)w;
+ dvxDialogEnd(sWiz.dlg, false);
+}
+
+
+// Centre: every axis's resting value. Edges: the stick must have gone
+// well out to both sides of the centre on every axis. Buttons: store.
+static void onWizNext(WidgetT *w) {
+ JoyRawT raw;
+
+ (void)w;
+
+ switch (sWiz.step) {
+ case WizCenterE:
+ sJoy.getRaw(sDevice, &raw);
+
+ for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
+ sWiz.cal[a].min = raw.axes[a];
+ sWiz.cal[a].center = raw.axes[a];
+ sWiz.cal[a].max = raw.axes[a];
+ }
+
+ sWiz.step = WizEdgesE;
+ break;
+
+ case WizEdgesE:
+ for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
+ int32_t span = sWiz.cal[a].center / CPJ_SPAN_DIV;
+
+ if (wizAxis(a) && (sWiz.cal[a].center - sWiz.cal[a].min <= span || sWiz.cal[a].max - sWiz.cal[a].center <= span)) {
+ wgtSetText(sWiz.text, "Move the stick further, all the way to each edge.");
+ return;
+ }
+ }
+
+ sWiz.step = WizButtonsE;
+ wgtSetText(sWiz.next, "Finish");
+ break;
+
+ case WizButtonsE:
+ for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
+ if (wizAxis(a)) {
+ sJoy.setCal(sDevice, a, &sWiz.cal[a]);
+ }
+ }
+
+ dvxDialogEnd(sWiz.dlg, true);
+ return;
+ }
+
+ wgtSetText(sWiz.text, sWizText[sWiz.step]);
+}
+
+
+// The raw values (and, while the edges are being found, their ranges) and
+// the buttons held.
+static void onWizTick(WidgetT *w) {
+ JoyRawT raw;
+ char text[CPJ_TEXT_BUF];
+ int32_t pos = 0;
+
+ (void)w;
+
+ if (!sJoy.getRaw(sDevice, &raw)) {
+ return;
+ }
+
+ for (int32_t a = 0; a < sWiz.info.axisCount; a++) {
+ if (!wizAxis(a)) {
+ continue;
+ }
+
+ if (sWiz.step == WizEdgesE) {
+ JoyAxisCalT *c = &sWiz.cal[a];
+
+ c->min = raw.axes[a] < c->min ? raw.axes[a] : c->min;
+ c->max = raw.axes[a] > c->max ? raw.axes[a] : c->max;
+ pos = dvxStrAppendf(text, sizeof(text), pos, "%" PRId32 ": %" PRId32 "-%" PRId32 " ", a + 1, c->min, c->max);
+ } else {
+ pos = dvxStrAppendf(text, sizeof(text), pos, "%" PRId32 ": %" PRId32 " ", a + 1, raw.axes[a]);
+ }
+ }
+
+ pos = dvxStrAppendf(text, sizeof(text), pos, " Buttons:");
+
+ for (int32_t b = 0; b < sWiz.info.buttonCount; b++) {
+ pos = dvxStrAppendf(text, sizeof(text), pos, " %c", (raw.buttons & (1U << b)) ? (char)('1' + b) : '-');
+ }
+
+ if (pos >= 0) {
+ wgtSetText(sWiz.values, text);
+ }
+}
+
+
+static void onZone(WidgetT *w) {
+ sJoy.setDeadZone(sDevice, wgtSliderGetValue(w));
+ showZone();
+}
+
+
+// The controllers found, in the dropdown; the first one chosen.
+static void refreshDevices(void) {
+ JoyInfoT info;
+ int32_t count = sJoy.count();
+
+ for (int32_t i = 0; i < count; i++) {
+ sJoy.getInfo(i, &info);
+ snprintf(sDevNames[i], sizeof(sDevNames[i]), "%s", info.name);
+ sDevLabels[i] = sDevNames[i];
+ }
+
+ wgtDropdownSetItems(sDevDrop, sDevLabels, count);
+ wgtDropdownSetSelected(sDevDrop, count > 0 ? 0 : -1);
+ selectDevice(count > 0 ? 0 : -1);
+}
+
+
+static bool resolveLibrary(void) {
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sSyms); i++) {
+ void *fn = dlsym(RTLD_DEFAULT, sSyms[i].name);
+
+ if (!fn) {
+ memset(&sJoy, 0, sizeof(sJoy));
+ return false;
+ }
+
+ memcpy((char *)&sJoy + sSyms[i].offset, &fn, sizeof(fn));
+ }
+
+ return true;
+}
+
+
+// Watch another controller: a handle on it, its dead zone, and the
+// calibration buttons only for one with analog axes.
+static void selectDevice(int32_t device) {
+ JoyInfoT info;
+ bool analog = device >= 0 && sJoy.getInfo(device, &info) && (info.caps & JOY_CAP_CALIBRATE);
+
+ sJoy.close(sHandle);
+ sDevice = device;
+ sHandle = device >= 0 ? sJoy.open(sAc, device) : -1;
+ sShownValid = false;
+ wgtSetEnabled(sCalBtn, analog);
+ wgtSetEnabled(sResetBtn, analog);
+ wgtSetEnabled(sZoneSldr, device >= 0);
+ wgtSliderSetValue(sZoneSldr, device >= 0 ? sJoy.getDeadZone(device) : 0);
+ showZone();
+ memset(&sShown, 0, sizeof(sShown));
+
+ for (int32_t i = 0; i < JOY_MAX_HATS; i++) {
+ sShown.hats[i] = JOY_HAT_CENTERED;
+ }
+
+ drawView();
+}
+
+
+static void showZone(void) {
+ char text[CPJ_KEY_BUF];
+
+ snprintf(text, sizeof(text), "%" PRId32 "%%", wgtSliderGetValue(sZoneSldr));
+ wgtSetText(sZoneLbl, text);
+}
+
+
+// The [joystick] key of a keyboard joystick slot.
+static void slotKey(int32_t slot, char *out, int32_t outSize) {
+ snprintf(out, (size_t)outSize, "%s%s", JOY_KBD_KEY_PREFIX, sSlotNames[slot]);
+}
+
+
+// Axis a of the controller being calibrated answers.
+static bool wizAxis(int32_t axis) {
+ return (sWiz.info.axisPresent & (1U << axis)) != 0;
+}
diff --git a/src/apps/kpunch/cpanel/cpJoystick.h b/src/apps/kpunch/cpanel/cpJoystick.h
new file mode 100644
index 00000000..95dceb1e
--- /dev/null
+++ b/src/apps/kpunch/cpanel/cpJoystick.h
@@ -0,0 +1,49 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// cpJoystick.h -- Control Panel's Game Controllers page: the joysticks
+// found, a live test view, calibration, the dead zone, the game port mode
+// and the keyboard joystick (DVX.INI [joystick], through the joystick
+// library).
+
+#ifndef CP_JOYSTICK_H
+#define CP_JOYSTICK_H
+
+#include "dvxApp.h"
+#include "dvxPrefs.h"
+#include "dvxWgt.h"
+
+// Build the page. prefs is the Control Panel's copy of DVX.INI as it was
+// when it opened (what Cancel goes back to); it stays open while the page
+// exists.
+void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
+
+// The Control Panel is closing: release the page's joystick handle.
+void cpJoystickFree(void);
+
+// Cancel: put the settings back as they were in prefs.
+void cpJoystickRevert(void);
+
+// OK: store the settings in prefs (the caller saves the file).
+void cpJoystickStore(PrefsHandleT *prefs);
+
+#endif // CP_JOYSTICK_H
diff --git a/src/apps/kpunch/cpanel/cpSounds.c b/src/apps/kpunch/cpanel/cpSounds.c
new file mode 100644
index 00000000..d440718e
--- /dev/null
+++ b/src/apps/kpunch/cpanel/cpSounds.c
@@ -0,0 +1,457 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// cpSounds.c -- Control Panel's Sounds page (see cpSounds.h).
+//
+// The sound library is looked up by name when the page is built, so the
+// Control Panel still works where it is not installed (the page then says
+// so). The sound files offered are those in the default sounds folder,
+// the ones DVX.INI names and any the user browses to; "(None)" is always
+// first. The volume applies at once (Cancel puts DVX.INI's back); the
+// sounds, the driver and the MIDI choice are written by OK, and the
+// driver and MIDI choice take effect when DVX next starts.
+
+#include "cpSounds.h"
+#include "audio/audio.h"
+#include "audio/audioDrv.h"
+#include "dvxDlg.h"
+#include "dvxPlat.h"
+#include "box/box.h"
+#include "button/button.h"
+#include "dropdown/dropdown.h"
+#include "label/label.h"
+#include "listBox/listBox.h"
+#include "slider/slider.h"
+#include "stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define CPS_LABEL_W 100
+#define CPS_GAP 4
+#define CPS_BTN_W 70
+#define CPS_LIST_H 148 // the event list: nine rows
+#define CPS_VOL_LBL_W 40
+#define CPS_TEXT_BUF 128
+#define CPS_DRIVERS_MAX 16
+#define CPS_DRV_ITEMS (CPS_DRIVERS_MAX + 2) // Automatic, the drivers, DVX.INI's own choice
+#define PERCENT_MAX 100
+
+// The sound library's functions (NULL when it is not installed)
+typedef struct {
+ __typeof__(audioDeskKey) *key;
+ __typeof__(audioDeskLabel) *label;
+ __typeof__(audioDeskPlayFile) *playFile;
+ __typeof__(audioDeskReload) *reload;
+ __typeof__(audioDriverNames) *driverNames;
+ __typeof__(audioErrorText) *errorText;
+ __typeof__(audioGetInfo) *getInfo;
+ __typeof__(audioSetMasterVolume) *setVolume;
+} SndFnsT;
+
+typedef struct {
+ const char *name;
+ size_t offset;
+} SndFnSymT;
+
+#define SND_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(SndFnsT, field) }
+
+static const SndFnSymT sSyms[] = {
+ SND_FN_SYM(key, audioDeskKey),
+ SND_FN_SYM(label, audioDeskLabel),
+ SND_FN_SYM(playFile, audioDeskPlayFile),
+ SND_FN_SYM(reload, audioDeskReload),
+ SND_FN_SYM(driverNames, audioDriverNames),
+ SND_FN_SYM(errorText, audioErrorText),
+ SND_FN_SYM(getInfo, audioGetInfo),
+ SND_FN_SYM(setVolume, audioSetMasterVolume)
+};
+
+// The sound files listed from the sounds folder
+static const char *sSoundExts[] = { ".wav", ".voc" };
+
+// [audio] midi: what the dropdown shows and stores
+static const char *sMidiLabels[] = { "Automatic", "MIDI port (MPU-401)", "FM synthesizer chip", "Built-in synthesizer" };
+static const char *sMidiValues[] = { AUDIO_MIDI_AUTO, AUDIO_MIDI_MPU401, AUDIO_MIDI_FM, AUDIO_MIDI_SOFTWARE };
+
+// ============================================================
+// Module state
+// ============================================================
+
+static SndFnsT sSnd;
+static AppContextT *sAc = NULL;
+static PrefsHandleT *sPrefs = NULL;
+static WidgetT *sEventList = NULL;
+static WidgetT *sFileDrop = NULL;
+static WidgetT *sVolSldr = NULL;
+static WidgetT *sVolLbl = NULL;
+static WidgetT *sDrvDrop = NULL;
+static WidgetT *sMidiDrop = NULL;
+static char **sFileValues = NULL; // stb_ds: paths as stored ("" for none), owned
+static const char **sFileLabels = NULL; // stb_ds: parallel labels (into sFileValues or literals)
+static char sDrvNames[CPS_DRV_ITEMS][AUDIO_NAME_MAX]; // as stored
+static const char *sDrvLabels[CPS_DRV_ITEMS];
+static int32_t sDrvCount = 0;
+static const char *sEventLabels[SoundCountE];
+static int32_t sChoice[SoundCountE];
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t addDriver(const char *name, const char *label);
+static int32_t addFile(const char *path);
+static WidgetT *addRow(WidgetT *parent, const char *label);
+static void addSoundsDir(void);
+static int compareNames(const void *a, const void *b);
+void cpSoundsApply(void);
+void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
+void cpSoundsFree(void);
+void cpSoundsRevert(void);
+void cpSoundsStore(PrefsHandleT *prefs);
+static int32_t iniVolume(void);
+static void onBrowse(WidgetT *w);
+static void onEvent(WidgetT *w);
+static void onFile(WidgetT *w);
+static void onTest(WidgetT *w);
+static void onVolume(WidgetT *w);
+static bool resolveLibrary(void);
+static void showVolume(void);
+
+
+// A driver item: the name stored and, when label is NULL, shown.
+static int32_t addDriver(const char *name, const char *label) {
+ snprintf(sDrvNames[sDrvCount], AUDIO_NAME_MAX, "%s", name);
+ sDrvLabels[sDrvCount] = label ? label : sDrvNames[sDrvCount];
+ return sDrvCount++;
+}
+
+
+// The item of a sound file, added when it is not listed yet ("" is
+// "(None)"). Files are told apart ignoring case: DOS names have none.
+static int32_t addFile(const char *path) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sFileValues); i++) {
+ if (strcasecmp(sFileValues[i], path) == 0) {
+ return i;
+ }
+ }
+
+ char *value = strdup(path);
+
+ if (!value) {
+ return 0;
+ }
+
+ size_t dirLen = strlen(AUDIO_SOUNDS_DIR);
+ const char *label = value;
+
+ if (!value[0]) {
+ label = "(None)";
+ } else if (strncasecmp(value, AUDIO_SOUNDS_DIR, dirLen) == 0 && value[dirLen] == '/') {
+ label = value + dirLen + 1;
+ }
+
+ arrput(sFileValues, value);
+ arrput(sFileLabels, label);
+ return (int32_t)arrlen(sFileValues) - 1;
+}
+
+
+// "Label:" and the row for its controls.
+static WidgetT *addRow(WidgetT *parent, const char *label) {
+ WidgetT *row = wgtHBox(parent);
+ WidgetT *lbl = wgtLabel(row, label);
+
+ row->spacing = wgtPixels(CPS_GAP);
+ lbl->prefW = wgtPixels(CPS_LABEL_W);
+ return row;
+}
+
+
+// The sound files of the default set, by name.
+static void addSoundsDir(void) {
+ char **names = dvxReadDir(AUDIO_SOUNDS_DIR);
+
+ if (!names) {
+ return;
+ }
+
+ qsort(names, arrlen(names), sizeof(names[0]), compareNames);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
+ for (int32_t e = 0; e < DVX_ARRAY_LEN(sSoundExts); e++) {
+ if (dvxHasExt(names[i], sSoundExts[e])) {
+ char path[DVX_MAX_PATH];
+
+ snprintf(path, sizeof(path), "%s/%s", AUDIO_SOUNDS_DIR, names[i]);
+ addFile(path);
+ }
+ }
+ }
+
+ dvxReadDirFree(names);
+}
+
+
+static int compareNames(const void *a, const void *b) {
+ return strcasecmp(*(const char * const *)a, *(const char * const *)b);
+}
+
+
+void cpSoundsApply(void) {
+ if (sSnd.reload) {
+ sSnd.reload();
+ }
+}
+
+
+void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
+ sAc = ac;
+ sPrefs = prefs;
+
+ if (!resolveLibrary()) {
+ wgtLabel(page, "Sound is not installed.");
+ return;
+ }
+
+ addFile("");
+ addSoundsDir();
+
+ for (int32_t i = 0; i < SoundCountE; i++) {
+ sEventLabels[i] = sSnd.label((DvxSoundE)i);
+ sChoice[i] = addFile(prefsGetString(prefs, AUDIO_SOUNDS_SECTION, sSnd.key((DvxSoundE)i), ""));
+ }
+
+ wgtLabel(page, "Events:");
+ sEventList = wgtListBox(page);
+ sEventList->prefH = wgtPixels(CPS_LIST_H);
+ sEventList->onChange = onEvent;
+ wgtListBoxSetItems(sEventList, sEventLabels, SoundCountE);
+
+ WidgetT *row = addRow(page, "Sound:");
+
+ sFileDrop = wgtDropdown(row);
+ sFileDrop->weight = WGT_WEIGHT_FILL;
+ sFileDrop->onChange = onFile;
+ wgtDropdownSetItems(sFileDrop, sFileLabels, (int32_t)arrlen(sFileLabels));
+
+ WidgetT *btn = wgtButton(row, "Browse...");
+
+ btn->onClick = onBrowse;
+ btn->prefW = wgtPixels(CPS_BTN_W);
+ btn = wgtButton(row, "Test");
+ btn->onClick = onTest;
+ btn->prefW = wgtPixels(CPS_BTN_W);
+
+ row = addRow(page, "Volume:");
+ sVolSldr = wgtSlider(row, 0, PERCENT_MAX);
+ sVolSldr->weight = WGT_WEIGHT_FILL;
+ sVolSldr->onChange = onVolume;
+ sVolLbl = wgtLabel(row, "");
+ sVolLbl->prefW = wgtPixels(CPS_VOL_LBL_W);
+ wgtSliderSetValue(sVolSldr, iniVolume());
+ showVolume();
+
+ // Driver: "Automatic", every installed driver, and DVX.INI's own
+ // choice even when that driver is missing (so OK keeps it).
+ char names[CPS_DRIVERS_MAX][AUDIO_NAME_MAX];
+ int32_t count = sSnd.driverNames(names, CPS_DRIVERS_MAX);
+ const char *want = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, AUDIO_DRIVER_AUTO);
+ int32_t drvSel = -1;
+
+ sDrvCount = 0;
+ addDriver(AUDIO_DRIVER_AUTO, "Automatic");
+
+ for (int32_t i = 0; i < count; i++) {
+ addDriver(names[i], NULL);
+ }
+
+ for (int32_t i = 0; i < sDrvCount; i++) {
+ drvSel = strcasecmp(sDrvNames[i], want) == 0 ? i : drvSel;
+ }
+
+ if (drvSel < 0) {
+ drvSel = addDriver(want, NULL);
+ }
+
+ sDrvDrop = wgtDropdown(addRow(page, "Sound driver:"));
+ wgtDropdownSetItems(sDrvDrop, sDrvLabels, sDrvCount);
+ wgtDropdownSetSelected(sDrvDrop, drvSel);
+
+ const char *midi = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO);
+ int32_t midiSel = 0;
+
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sMidiValues); i++) {
+ midiSel = strcasecmp(sMidiValues[i], midi) == 0 ? i : midiSel;
+ }
+
+ sMidiDrop = wgtDropdown(addRow(page, "MIDI music:"));
+ wgtDropdownSetItems(sMidiDrop, sMidiLabels, DVX_ARRAY_LEN(sMidiLabels));
+ wgtDropdownSetSelected(sMidiDrop, midiSel);
+
+ AudioInfoT info;
+ char text[CPS_TEXT_BUF];
+
+ sSnd.getInfo(&info);
+ snprintf(text, sizeof(text), "Sound device: %s", info.running ? info.description : "none running");
+ wgtLabel(page, text);
+ wgtLabel(page, "A new driver or MIDI choice takes effect when DVX next starts.");
+
+ wgtListBoxSetSelected(sEventList, 0);
+ onEvent(sEventList);
+}
+
+
+void cpSoundsFree(void) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sFileValues); i++) {
+ free(sFileValues[i]);
+ }
+
+ arrfree(sFileValues);
+ arrfree(sFileLabels);
+ sFileValues = NULL;
+ sFileLabels = NULL;
+}
+
+
+void cpSoundsRevert(void) {
+ if (sSnd.setVolume) {
+ sSnd.setVolume(iniVolume() * AUDIO_VOLUME_MAX / PERCENT_MAX);
+ }
+}
+
+
+void cpSoundsStore(PrefsHandleT *prefs) {
+ if (!sSnd.key) {
+ return;
+ }
+
+ for (int32_t i = 0; i < SoundCountE; i++) {
+ prefsSetString(prefs, AUDIO_SOUNDS_SECTION, sSnd.key((DvxSoundE)i), sFileValues[sChoice[i]]);
+ }
+
+ prefsSetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, wgtSliderGetValue(sVolSldr));
+ prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, sDrvNames[wgtDropdownGetSelected(sDrvDrop)]);
+ prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, sMidiValues[wgtDropdownGetSelected(sMidiDrop)]);
+}
+
+
+// The master volume in DVX.INI as the Control Panel opened (percent).
+static int32_t iniVolume(void) {
+ int32_t v = prefsGetInt(sPrefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, PERCENT_MAX);
+
+ return v < 0 ? 0 : (v > PERCENT_MAX ? PERCENT_MAX : v);
+}
+
+
+// Choose any sound file for the selected event.
+static void onBrowse(WidgetT *w) {
+ (void)w;
+
+ FileFilterT filters[] = {
+ { "Sounds (*.wav;*.voc)" },
+ { "All Files (*.*)" }
+ };
+ char path[DVX_MAX_PATH];
+
+ if (!dvxFileDialog(sAc, "Choose a Sound", FD_OPEN, AUDIO_SOUNDS_DIR, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
+ return;
+ }
+
+ int32_t item = addFile(path);
+
+ wgtDropdownSetItems(sFileDrop, sFileLabels, (int32_t)arrlen(sFileLabels));
+ wgtDropdownSetSelected(sFileDrop, item);
+ onFile(sFileDrop);
+}
+
+
+// Show the selected event's sound.
+static void onEvent(WidgetT *w) {
+ int32_t ev = wgtListBoxGetSelected(w);
+
+ if (ev >= 0 && ev < SoundCountE) {
+ wgtDropdownSetSelected(sFileDrop, sChoice[ev]);
+ }
+}
+
+
+static void onFile(WidgetT *w) {
+ int32_t ev = wgtListBoxGetSelected(sEventList);
+ int32_t item = wgtDropdownGetSelected(w);
+
+ if (ev >= 0 && ev < SoundCountE && item >= 0) {
+ sChoice[ev] = item;
+ }
+}
+
+
+// Play the sound chosen in the dropdown.
+static void onTest(WidgetT *w) {
+ (void)w;
+
+ int32_t item = wgtDropdownGetSelected(sFileDrop);
+
+ if (item <= 0) {
+ return;
+ }
+
+ int32_t rc = sSnd.playFile(sFileValues[item]);
+
+ if (rc < 0) {
+ dvxMessageBox(sAc, "Sounds", sSnd.errorText(rc), MB_OK | MB_ICONWARNING);
+ }
+}
+
+
+static void onVolume(WidgetT *w) {
+ sSnd.setVolume(wgtSliderGetValue(w) * AUDIO_VOLUME_MAX / PERCENT_MAX);
+ showVolume();
+}
+
+
+static bool resolveLibrary(void) {
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sSyms); i++) {
+ void *fn = dlsym(RTLD_DEFAULT, sSyms[i].name);
+
+ if (!fn) {
+ memset(&sSnd, 0, sizeof(sSnd));
+ return false;
+ }
+
+ memcpy((char *)&sSnd + sSyms[i].offset, &fn, sizeof(fn));
+ }
+
+ return true;
+}
+
+
+static void showVolume(void) {
+ char text[CPS_TEXT_BUF];
+
+ snprintf(text, sizeof(text), "%ld%%", (long)wgtSliderGetValue(sVolSldr));
+ wgtSetText(sVolLbl, text);
+}
diff --git a/src/apps/kpunch/cpanel/cpSounds.h b/src/apps/kpunch/cpanel/cpSounds.h
new file mode 100644
index 00000000..a0e5eadb
--- /dev/null
+++ b/src/apps/kpunch/cpanel/cpSounds.h
@@ -0,0 +1,49 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// cpSounds.h -- Control Panel's Sounds page: the sound of each desktop
+// event (DVX.INI [sounds]), the master volume, the sound driver and where
+// MIDI music plays (DVX.INI [audio]), through the sound library.
+
+#ifndef CP_SOUNDS_H
+#define CP_SOUNDS_H
+
+#include "dvxApp.h"
+#include "dvxPrefs.h"
+#include "dvxWgt.h"
+
+// DVX.INI has been saved: the desktop sounds follow the new choices.
+void cpSoundsApply(void);
+
+// Build the page from prefs (the Control Panel's copy of DVX.INI).
+void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs);
+
+// The Control Panel is closing: free the page's lists.
+void cpSoundsFree(void);
+
+// Cancel: the master volume goes back to what DVX.INI says.
+void cpSoundsRevert(void);
+
+// OK: store the settings in prefs (the caller saves the file).
+void cpSoundsStore(PrefsHandleT *prefs);
+
+#endif // CP_SOUNDS_H
diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c
index 4640309a..1b023d5e 100644
--- a/src/apps/kpunch/cpanel/cpanel.c
+++ b/src/apps/kpunch/cpanel/cpanel.c
@@ -30,6 +30,8 @@
// File Types -- which program opens each kind of document (cpTypes.c)
// Network -- TCP/IP settings and the network's state (cpNet.c)
// Printer -- printer model, port and page setup (cpPrinter.c)
+// Game Controllers -- joysticks: test view, calibration, keyboard joystick (cpJoystick.c)
+// Sounds -- the sound of each desktop event, volume, sound driver, MIDI music (cpSounds.c)
//
// Changes preview live. OK saves to DVX.INI, Cancel reverts to the
// state captured when the control panel was opened.
@@ -50,8 +52,10 @@
#include "dvxWm.h"
#include "dvxPlat.h"
#include "shellApp.h"
+#include "cpJoystick.h"
#include "cpNet.h"
#include "cpPrinter.h"
+#include "cpSounds.h"
#include "cpTypes.h"
#include "stb_ds_wrap.h"
@@ -310,6 +314,8 @@ void appShutdown(void) {
prefsClose(sPrefs);
sPrefs = NULL;
cpTypesFree();
+ cpJoystickFree();
+ cpSoundsFree();
}
@@ -703,6 +709,8 @@ static void onClearWallpaper(WidgetT *w) {
static void onClose(WindowT *win) {
restoreSnapshot();
+ cpJoystickRevert();
+ cpSoundsRevert();
prefsClose(sPrefs);
sPrefs = NULL;
dvxDestroyWindow(sAc, win);
@@ -758,7 +766,7 @@ static void onMenu(WindowT *win, int32_t menuId) {
break;
case CMD_HELP_ABOUT:
- dvxInfoBox(sAc, "About Control Panel", "Control Panel\n\nChanges the mouse, colors, desktop picture, screen mode, which programs open each type of file, the network settings and the printer.\n\nCopyright 2026 Scott Duensing");
+ dvxInfoBox(sAc, "About Control Panel", "Control Panel\n\nChanges the mouse, colors, desktop picture, screen mode, which programs open each type of file, the network settings, the printer, the game controllers and the sounds.\n\nCopyright 2026 Scott Duensing");
break;
default:
@@ -828,11 +836,14 @@ static void onOk(WidgetT *w) {
cpNetStore(sPrefs);
cpPrinterStore(sPrefs);
+ cpJoystickStore(sPrefs);
+ cpSoundsStore(sPrefs);
prefsSave(sPrefs);
prefsClose(sPrefs);
sPrefs = NULL;
cpTypesApply();
cpNetApply();
+ cpSoundsApply();
dvxDestroyWindow(sAc, sWin);
sWin = NULL;
}
@@ -1265,6 +1276,8 @@ int32_t appMain(DxeAppContextT *ctx) {
WidgetT *typesTab = wgtTabPage(tabs, "File Types");
WidgetT *netTab = wgtTabPage(tabs, "Network");
WidgetT *printerTab = wgtTabPage(tabs, "Printer");
+ WidgetT *joyTab = wgtTabPage(tabs, "Game Controllers");
+ WidgetT *soundsTab = wgtTabPage(tabs, "Sounds");
buildMouseTab(mouseTab);
buildColorsTab(colorsTab);
@@ -1273,6 +1286,8 @@ int32_t appMain(DxeAppContextT *ctx) {
cpTypesBuild(typesTab);
cpNetBuild(sAc, netTab, sPrefs);
cpPrinterBuild(sAc, printerTab, sPrefs);
+ cpJoystickBuild(sAc, joyTab, sPrefs);
+ cpSoundsBuild(sAc, soundsTab, sPrefs);
// OK / Cancel buttons at bottom
WidgetT *btnRow = wgtHBox(root);
diff --git a/src/apps/kpunch/cpanel/cpanel.dhs b/src/apps/kpunch/cpanel/cpanel.dhs
index c48f5d55..63b7d511 100644
--- a/src/apps/kpunch/cpanel/cpanel.dhs
+++ b/src/apps/kpunch/cpanel/cpanel.dhs
@@ -34,7 +34,7 @@
.h1 Control Panel
-The Control Panel changes how DVX looks and works: the mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of file, the network settings and the printer. Its window has seven tabs. Click a tab to see its settings.
+The Control Panel changes how DVX looks and works: the mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of file, the network settings, the printer, the game controllers and the sounds. Its window has nine tabs; when they do not all fit, the arrows beside them show the others. Click a tab to see its settings.
.link cpanel.mouse Mouse: pointer speed, double-click speed and the scroll wheel
.link cpanel.colors Colors: the colors of every part of the screen, and color themes
@@ -43,6 +43,8 @@ The Control Panel changes how DVX looks and works: the mouse, the screen colors,
.link cpanel.types File Types: which program opens each kind of document
.link cpanel.network Network: connecting DVX to a network, and its status
.link cpanel.printer Printer: your printer's model, where it is connected, and the page setup
+.link cpanel.joystick Game Controllers: joysticks, trying them out and calibrating them, and using the keyboard as a joystick
+.link cpanel.sounds Sounds: the sound DVX makes for each event, the volume, and the sound hardware
.h2 Trying Out Changes
@@ -280,3 +282,94 @@ The Printer tab tells DVX about your printer. Every program prints to the printe
Page Setup... opens the Page Setup dialog: the margins, lines per page, header and footer used when text is printed. Its changes are saved as soon as you choose OK in that dialog.
Like the other tabs, the model and port are saved when you click OK.
+
+.topic cpanel.joystick
+.title Game Controllers
+.toc 0 Game Controllers
+.index Game Controllers
+.index Joystick
+.index Game Port
+.index Calibrate
+.index Calibration
+.index Dead Zone
+.index Keyboard Joystick
+
+.h1 Game Controllers
+
+The Game Controllers tab shows the joysticks DVX found, lets you try them out and calibrates them so games read them correctly. When no joystick is connected, the keyboard can stand in for one.
+
+.table
+ Setting What it does
+ ------- ------------
+ Controller The joystick to look at. A joystick on the game port is listed as Game port joystick 1 or 2; the keyboard as Keyboard joystick.
+ Detect Looks for joysticks again, for example after you plugged one in. It only works while no program is using a joystick.
+ Dead zone How far the stick can move from the middle and still count as the middle, in percent. A worn stick that does not come back to the middle exactly needs a bigger dead zone.
+ Game port Two joysticks: the game port has room for two joysticks with two buttons each. One joystick with 4 axes: for flight sticks with a throttle or rudder, which use the whole port and all four buttons.
+ Keyboard When to offer the keyboard as a joystick: when no joystick is found (the usual choice), always, or never.
+ Keys... Chooses the keys the keyboard joystick uses: up, down, left, right and four buttons. At first they are the arrow keys, Left Ctrl, Space, Left Shift and Z.
+.endtable
+
+.h2 Trying Out a Joystick
+
+The picture under the controller list follows the joystick as you use it. The dot in the square shows where the stick points; the bars show each axis from one end to the other; the numbered boxes light up while their button is held down; a hat switch, if the joystick has one, shows its direction. "Not connected" means the joystick stopped answering: check its cable, then click Detect.
+
+The keyboard joystick only answers while the program using it is the active one, so the keys you type into other programs do not move it.
+
+.h2 Calibrating
+
+A joystick on the game port works at once: DVX takes where the stick rests when it is found as the middle and learns how far it goes as you move it. For the best results, calibrate it once:
+
+.list
+.item Click the Calibrate... button.
+.item Let go of the stick so it rests in the middle, then click Next.
+.item Move the stick all the way round its edges, reaching every corner, then click Next. If it did not go far enough, the Calibrate window asks you to move it further.
+.item Press each button to see that it works, then click Finish.
+.endlist
+
+Automatic forgets the calibration: DVX learns the stick's range by itself again.
+
+Every change works at once, so you can see it in the picture. Like the other tabs, OK saves the settings and Cancel puts them back as they were.
+
+.topic cpanel.sounds
+.title Sounds
+.toc 0 Sounds
+.index Sounds
+.index Sound Events
+.index Beep
+.index Volume
+.index Sound Driver
+.index MIDI Music
+
+.h1 Sounds
+
+The Sounds tab chooses the sound DVX makes for each event, such as starting, ending or showing a message, sets the overall volume, and says which sound hardware to use. Without sound hardware, everything works the same but silently.
+
+.table
+ Setting What it does
+ ------- ------------
+ Events The events DVX has a sound for. Click one to see and change its sound.
+ Sound The sound of the selected event: (None) for silence, one of the sounds that come with DVX, or a sound file you chose with Browse....
+ Browse... Chooses any sound file (WAV or VOC) for the selected event.
+ Test Plays the sound shown, so you can hear it.
+ Volume How loud all sounds and music are, in percent. It changes at once.
+ Sound driver The sound hardware to use. Automatic finds it by itself (the usual choice); sb is a Sound Blaster, speaker the PC speaker, null no sound at all.
+ MIDI music Where MIDI music plays: Automatic (the music chip on the sound card, or DVX's own synthesizer that sounds much the same), a MIDI port with an external synthesizer, the FM synthesizer chip, or the built-in synthesizer.
+.endtable
+
+.h2 The Events
+
+.table
+ Event When it sounds
+ ----- --------------
+ Default beep A message without a picture, and whenever a program asks for a beep.
+ Start DVX DVX has started.
+ Exit DVX DVX is ending.
+ Error A message about an error.
+ Warning A warning message.
+ Question A message that asks a question.
+ Information A message that tells you something.
+ Open window A window opens (not the small windows that ask something).
+ Close window A window closes.
+.endtable
+
+Like the other tabs, OK saves the settings and Cancel puts them back as they were. A new sound driver or MIDI choice takes effect the next time DVX starts.
diff --git a/src/apps/kpunch/dvxbasic/compiler/basKeywords.h b/src/apps/kpunch/dvxbasic/compiler/basKeywords.h
index 6a235fda..524c6d29 100644
--- a/src/apps/kpunch/dvxbasic/compiler/basKeywords.h
+++ b/src/apps/kpunch/dvxbasic/compiler/basKeywords.h
@@ -47,6 +47,7 @@ BAS_KEYWORD("APP", TOK_APP, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("APPEND", TOK_APPEND, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("AS", TOK_AS, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("BASE", TOK_BASE, BAS_KW_CLASS_OTHER)
+BAS_KEYWORD("BEEP", TOK_BEEP, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("BINARY", TOK_BINARY, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("BOOLEAN", TOK_BOOLEAN, BAS_KW_CLASS_TYPE)
BAS_KEYWORD("BYREF", TOK_BYREF, BAS_KW_CLASS_OTHER)
@@ -90,6 +91,7 @@ BAS_KEYWORD("FALSE", TOK_FALSE_KW, BAS_KW_CLASS_LITERAL)
BAS_KEYWORD("FILECOPY", TOK_FILECOPY, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("FILELEN", TOK_FILELEN, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("FOR", TOK_FOR, BAS_KW_CLASS_OTHER)
+BAS_KEYWORD("FRAMEWAIT", TOK_FRAMEWAIT, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("FUNCTION", TOK_FUNCTION, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("GET", TOK_GET, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("GETATTR", TOK_GETATTR, BAS_KW_CLASS_OTHER)
@@ -128,6 +130,7 @@ BAS_KEYWORD("OPTION", TOK_OPTION, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("OPTIONAL", TOK_OPTIONAL, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("OR", TOK_OR, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("OUTPUT", TOK_OUTPUT, BAS_KW_CLASS_OTHER)
+BAS_KEYWORD("PLAY", TOK_PLAY, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("PRESERVE", TOK_PRESERVE, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("PRINT", TOK_PRINT, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("PUT", TOK_PUT, BAS_KW_CLASS_OTHER)
@@ -151,6 +154,7 @@ BAS_KEYWORD("SHELL", TOK_SHELL, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("SHOW", TOK_SHOW, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("SINGLE", TOK_SINGLE, BAS_KW_CLASS_TYPE)
BAS_KEYWORD("SLEEP", TOK_SLEEP, BAS_KW_CLASS_OTHER)
+BAS_KEYWORD("SOUND", TOK_SOUND, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("STATIC", TOK_STATIC, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("STEP", TOK_STEP, BAS_KW_CLASS_OTHER)
BAS_KEYWORD("STRING", TOK_STRING_KW, BAS_KW_CLASS_TYPE)
diff --git a/src/apps/kpunch/dvxbasic/compiler/lexer.h b/src/apps/kpunch/dvxbasic/compiler/lexer.h
index b0bcb283..73b7b534 100644
--- a/src/apps/kpunch/dvxbasic/compiler/lexer.h
+++ b/src/apps/kpunch/dvxbasic/compiler/lexer.h
@@ -77,6 +77,7 @@ typedef enum {
TOK_APP,
TOK_AS,
TOK_BASE,
+ TOK_BEEP,
TOK_BOOLEAN,
TOK_BYREF,
TOK_BYVAL,
@@ -111,6 +112,7 @@ typedef enum {
TOK_EXIT,
TOK_FALSE_KW,
TOK_FOR,
+ TOK_FRAMEWAIT,
TOK_FUNCTION,
TOK_GET,
TOK_GOSUB,
@@ -141,6 +143,7 @@ typedef enum {
TOK_OPTION,
TOK_OR,
TOK_OUTPUT,
+ TOK_PLAY,
TOK_PRESERVE,
TOK_PRINT,
TOK_PUT,
@@ -161,6 +164,7 @@ typedef enum {
TOK_SHOW,
TOK_SINGLE,
TOK_SLEEP,
+ TOK_SOUND,
TOK_INIREAD,
TOK_INIWRITE,
TOK_STATIC,
diff --git a/src/apps/kpunch/dvxbasic/compiler/opcodes.h b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
index 5dfab79f..f2950367 100644
--- a/src/apps/kpunch/dvxbasic/compiler/opcodes.h
+++ b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
@@ -417,6 +417,18 @@ typedef enum {
#define OP_FILE_PUT_UDT 0xF6 // pop layoutStr, pop udt, pop recno, pop channel; write packed fields
#define OP_FILE_SEEK_GET 0xF7 // pop channel, push 1-based file position (SEEK function)
+// ============================================================
+// Sound, joysticks and frame pacing (BasDeviceCallbacksT)
+// ============================================================
+
+#define OP_BEEP 0xD1 // beep; waits in foreground music mode
+#define OP_SOUND 0xD2 // pop ticks, pop hz; play a tone (waits in foreground mode)
+#define OP_PLAY 0xD3 // pop macro string; play it (waits in foreground mode)
+#define OP_PLAY_COUNT 0xD4 // pop n (ignored), push notes waiting in the background (PLAY(n))
+#define OP_STICK 0xD5 // pop n, push joystick position (STICK(n))
+#define OP_STRIG 0xD6 // pop n, push joystick button state (STRIG(n))
+#define OP_FRAME_WAIT 0xD7 // pop frames per second; wait for the next frame
+
// ============================================================
// Halt
// ============================================================
@@ -559,6 +571,9 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
case OP_FS_GETATTR: case OP_FS_SETATTR:
case OP_CREATE_FORM: case OP_SET_EVENT: case OP_REMOVE_CTRL:
case OP_FILE_PUT_UDT: case OP_ERL: case OP_FILE_SEEK_GET:
+ case OP_BEEP: case OP_SOUND: case OP_PLAY:
+ case OP_PLAY_COUNT: case OP_STICK: case OP_STRIG:
+ case OP_FRAME_WAIT:
case OP_END: case OP_HALT:
return BAS_OPERAND_NONE;
diff --git a/src/apps/kpunch/dvxbasic/compiler/parser.c b/src/apps/kpunch/dvxbasic/compiler/parser.c
index 8b04d258..741eda58 100644
--- a/src/apps/kpunch/dvxbasic/compiler/parser.c
+++ b/src/apps/kpunch/dvxbasic/compiler/parser.c
@@ -103,6 +103,10 @@ static const BuiltinFuncT builtinFuncs[] = {
{"TIME$", OP_TIME_STR, 0, 0},
{"TIMER", OP_MATH_TIMER, 0, 0},
+ // Joysticks
+ {"STICK", OP_STICK, 1, 1},
+ {"STRIG", OP_STRIG, 1, 1},
+
{NULL, 0, 0, 0}
};
@@ -128,6 +132,12 @@ static const BuiltinFuncT builtinFuncs[] = {
// SLEEP with no argument waits this many seconds.
#define BAS_SLEEP_DEFAULT_SECS 1
+// Arguments of BEEP, SOUND hz, ticks, PLAY macro$ and FRAMEWAIT hz.
+#define BAS_BEEP_ARGS 0
+#define BAS_SOUND_ARGS 2
+#define BAS_PLAY_ARGS 1
+#define BAS_FRAMEWAIT_ARGS 1
+
// MsgBox flags when the argument is omitted (vbOKOnly).
#define BAS_MSGBOX_OK_ONLY 0x0000
@@ -374,6 +384,7 @@ static void parseShared(BasParserT *p);
static void parseShell(BasParserT *p);
static uint8_t parseShowModalFlag(BasParserT *p);
static void parseSleep(BasParserT *p);
+static void parseSoundStatement(BasParserT *p, int32_t argc, uint8_t op);
static void parseStatement(BasParserT *p);
static void parseStatic(BasParserT *p);
static void parseSwap(BasParserT *p);
@@ -4851,6 +4862,16 @@ static void parsePrimary(BasParserT *p) {
return;
}
+ // PLAY(n) -- notes waiting in the background music queue
+ if (tt == TOK_PLAY) {
+ advance(p);
+ expect(p, TOK_LPAREN);
+ parseExpression(p);
+ expect(p, TOK_RPAREN);
+ basEmit8(&p->cg, OP_PLAY_COUNT);
+ return;
+ }
+
// TIMER -- seconds since midnight (no args needed)
if (tt == TOK_TIMER) {
advance(p);
@@ -6189,6 +6210,23 @@ static void parseSleep(BasParserT *p) {
}
+// BEEP, SOUND hz, ticks, PLAY macro$ and FRAMEWAIT hz: the keyword, argc
+// comma-separated arguments, then op.
+static void parseSoundStatement(BasParserT *p, int32_t argc, uint8_t op) {
+ advance(p); // consume the keyword
+
+ for (int32_t i = 0; i < argc && !p->hasError; i++) {
+ if (i > 0) {
+ expect(p, TOK_COMMA);
+ }
+
+ parseExpression(p);
+ }
+
+ basEmit8(&p->cg, op);
+}
+
+
static void parseStatement(BasParserT *p) {
if (p->hasError) {
return;
@@ -6432,6 +6470,22 @@ static void parseStatement(BasParserT *p) {
parseSleep(p);
break;
+ case TOK_BEEP:
+ parseSoundStatement(p, BAS_BEEP_ARGS, OP_BEEP);
+ break;
+
+ case TOK_SOUND:
+ parseSoundStatement(p, BAS_SOUND_ARGS, OP_SOUND);
+ break;
+
+ case TOK_PLAY:
+ parseSoundStatement(p, BAS_PLAY_ARGS, OP_PLAY);
+ break;
+
+ case TOK_FRAMEWAIT:
+ parseSoundStatement(p, BAS_FRAMEWAIT_ARGS, OP_FRAME_WAIT);
+ break;
+
case TOK_SWAP:
parseSwap(p);
break;
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
index 5fb69a58..af6153c6 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
@@ -32,6 +32,9 @@
#include "../runtime/serialize.h"
#include "../../../../libs/kpunch/serial/rs232/rs232.h"
#include "../../../../libs/kpunch/serial/seclink/secLink.h"
+#include "../../../../libs/kpunch/game/game.h"
+#include "../../../../libs/kpunch/joystick/joystick.h"
+#include "../../../../libs/kpunch/music/music.h"
#include "../../../../libs/kpunch/print/print.h"
#include "dvxDlg.h"
#include "dvxRes.h"
@@ -176,6 +179,23 @@ typedef struct {
static PrintApiT sPrintApi;
+// The music, joystick and game libraries' functions behind BEEP, SOUND,
+// PLAY, STICK, STRIG and FRAMEWAIT (deviceBind; a group stays NULL when
+// its library is not loaded)
+typedef struct {
+ __typeof__(musicBasBeep) *beep;
+ __typeof__(musicBasBusy) *busy;
+ __typeof__(musicBasPlay) *play;
+ __typeof__(musicBasPlayCount) *playCount;
+ __typeof__(musicBasSound) *sound;
+ __typeof__(joyBasStick) *stick;
+ __typeof__(joyBasStrig) *strig;
+ __typeof__(gameFrameInit) *frameInit;
+ __typeof__(gameFrameNext) *frameNext;
+} DeviceApiT;
+
+static DeviceApiT sDeviceApi;
+
static bool sCommApiResolved = false;
#define COMM_BITS_PER_BYTE 8
@@ -496,6 +516,16 @@ static const char *ctrlDisplayName(const BasControlT *ctrl);
static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt);
static void destroyFormNow(BasFormRtT *rt, BasFormT *form);
static void detachFormForUnload(BasFormRtT *rt, BasFormT *form);
+static void deviceBeep(void *ctx);
+static void deviceBind(BasFormRtT *rt);
+static bool deviceFrameBusy(void *ctx);
+static void deviceFrameStart(void *ctx, int32_t hz);
+static bool devicePlay(void *ctx, const char *macro);
+static int32_t devicePlayCount(void *ctx);
+static void deviceSound(void *ctx, double hz, double ticks);
+static bool deviceSoundBusy(void *ctx);
+static int32_t deviceStick(void *ctx, int32_t n);
+static int32_t deviceStrig(void *ctx, int32_t n);
int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx);
const char *DlgFileOpen(const char *title, const char *filter);
const char *DlgFileSave(const char *title, const char *filter);
@@ -530,6 +560,7 @@ static void frmLoadOnMenuBegin(void *userData, const char *name, int32_t level);
static void frmLoadOnMenuEnd(void *userData);
static void frmLoadOnMenuProp(void *userData, const char *key, const char *value);
static void frmLoadQueueContextMenu(BasFrmLoadCtxT *ctx, BasControlT *ctrl, const char *menuName);
+static void gameBind(BasFormRtT *rt);
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled);
static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName);
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled);
@@ -810,6 +841,8 @@ void basFormRtBindVm(BasFormRtT *rt) {
basVmSetUiCallbacks(rt->vm, &ui);
printerBind(rt);
+ gameBind(rt);
+ deviceBind(rt);
}
@@ -3577,6 +3610,110 @@ static void detachFormForUnload(BasFormRtT *rt, BasFormT *form) {
}
+static void deviceBeep(void *ctx) {
+ (void)ctx;
+ sDeviceApi.beep();
+}
+
+
+// BEEP, SOUND and PLAY go to the music library, STICK and STRIG to the
+// joystick library and FRAMEWAIT to the game library, each only when
+// that library is loaded (the VM treats missing callbacks as described in
+// vm.h).
+static void deviceBind(BasFormRtT *rt) {
+ BasDeviceCallbacksT cb;
+
+ sDeviceApi.beep = (__typeof__(sDeviceApi.beep))dlsym(NULL, DVX_SYM("musicBasBeep"));
+ sDeviceApi.busy = (__typeof__(sDeviceApi.busy))dlsym(NULL, DVX_SYM("musicBasBusy"));
+ sDeviceApi.play = (__typeof__(sDeviceApi.play))dlsym(NULL, DVX_SYM("musicBasPlay"));
+ sDeviceApi.playCount = (__typeof__(sDeviceApi.playCount))dlsym(NULL, DVX_SYM("musicBasPlayCount"));
+ sDeviceApi.sound = (__typeof__(sDeviceApi.sound))dlsym(NULL, DVX_SYM("musicBasSound"));
+ sDeviceApi.stick = (__typeof__(sDeviceApi.stick))dlsym(NULL, DVX_SYM("joyBasStick"));
+ sDeviceApi.strig = (__typeof__(sDeviceApi.strig))dlsym(NULL, DVX_SYM("joyBasStrig"));
+ sDeviceApi.frameInit = (__typeof__(sDeviceApi.frameInit))dlsym(NULL, DVX_SYM("gameFrameInit"));
+ sDeviceApi.frameNext = (__typeof__(sDeviceApi.frameNext))dlsym(NULL, DVX_SYM("gameFrameNext"));
+ memset(&cb, 0, sizeof(cb));
+
+ if (sDeviceApi.beep && sDeviceApi.busy && sDeviceApi.play && sDeviceApi.playCount && sDeviceApi.sound) {
+ cb.beep = deviceBeep;
+ cb.sound = deviceSound;
+ cb.play = devicePlay;
+ cb.playCount = devicePlayCount;
+ cb.soundBusy = deviceSoundBusy;
+ }
+
+ if (sDeviceApi.stick && sDeviceApi.strig) {
+ cb.stick = deviceStick;
+ cb.strig = deviceStrig;
+ }
+
+ if (sDeviceApi.frameInit && sDeviceApi.frameNext) {
+ cb.frameStart = deviceFrameStart;
+ cb.frameBusy = deviceFrameBusy;
+ }
+
+ cb.ctx = rt;
+ basVmSetDeviceCallbacks(rt->vm, &cb);
+}
+
+
+static bool deviceFrameBusy(void *ctx) {
+ const BasFormRtT *rt = (const BasFormRtT *)ctx;
+
+ return platformMicros() < rt->frameDueUs;
+}
+
+
+// The frame clock starts over when the program asks for another rate.
+static void deviceFrameStart(void *ctx, int32_t hz) {
+ BasFormRtT *rt = (BasFormRtT *)ctx;
+ uint32_t missed = 0;
+
+ if (rt->frameHz != hz) {
+ sDeviceApi.frameInit(&rt->framePacer, (uint32_t)hz);
+ rt->frameHz = hz;
+ }
+
+ rt->frameDueUs = sDeviceApi.frameNext(&rt->framePacer, &missed);
+}
+
+
+static bool devicePlay(void *ctx, const char *macro) {
+ (void)ctx;
+ return sDeviceApi.play(macro) != MUSIC_ERR_MACRO;
+}
+
+
+static int32_t devicePlayCount(void *ctx) {
+ (void)ctx;
+ return sDeviceApi.playCount();
+}
+
+
+static void deviceSound(void *ctx, double hz, double ticks) {
+ (void)ctx;
+ sDeviceApi.sound(hz, ticks);
+}
+
+
+static bool deviceSoundBusy(void *ctx) {
+ (void)ctx;
+ return sDeviceApi.busy();
+}
+
+
+static int32_t deviceStick(void *ctx, int32_t n) {
+ (void)ctx;
+ return sDeviceApi.stick(n);
+}
+
+
+static int32_t deviceStrig(void *ctx, int32_t n) {
+ (void)ctx;
+ return sDeviceApi.strig(n);
+}
+
+
int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx) {
if (!sFormRt || !items) {
return -1;
@@ -4492,6 +4629,17 @@ static void frmLoadQueueContextMenu(BasFrmLoadCtxT *ctx, BasControlT *ctrl, cons
}
+// The game library's BASIC functions (game.bas) get the GUI context, when
+// the library is loaded.
+static void gameBind(BasFormRtT *rt) {
+ __typeof__(gameBasSetContext) *setContext = (__typeof__(setContext))dlsym(NULL, DVX_SYM("gameBasSetContext"));
+
+ if (setContext) {
+ setContext(rt->ctx);
+ }
+}
+
+
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) {
const BasPropDescT *pd = basFormRtFindCommonProp(propName);
WidgetT *w = ctrl->widget;
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
index ae34482e..0e79f6b0 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
@@ -34,6 +34,7 @@
#include "../runtime/vm.h"
#include "../runtime/values.h"
+#include "../../../../libs/kpunch/game/game.h"
#include "dvxApp.h"
#include "dvxWgt.h"
#include "frmParser.h"
@@ -351,6 +352,11 @@ typedef struct {
// is never trusted.
void *refCacheKey;
BasControlT *refCacheHit;
+ // FRAMEWAIT's frame clock: the rate it runs at (0: not started) and
+ // when the frame the program waits for begins.
+ GameFramePacerT framePacer;
+ int32_t frameHz;
+ uint64_t frameDueUs;
} BasFormRtT;
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c
index e6b80c06..0d683345 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideMain.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c
@@ -783,6 +783,14 @@ static const HelpMapEntryT sHelpMap[] = {
{"Sleep", "lang.misc"},
{"Unload", "lang.misc"},
+ // Sound, joysticks and frames
+ {"Beep", "lang.sound"},
+ {"FrameWait", "lang.sound"},
+ {"Play", "lang.sound"},
+ {"Sound", "lang.sound"},
+ {"Stick", "lang.sound"},
+ {"Strig", "lang.sound"},
+
// File I/O
{"Close", "lang.fileio"},
{"Eof", "lang.func.fileio"},
diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs
index 49462adf..1b38047e 100644
--- a/src/apps/kpunch/dvxbasic/ideguide.dhs
+++ b/src/apps/kpunch/dvxbasic/ideguide.dhs
@@ -1058,6 +1058,19 @@ DVX BASIC now uses the QBasic scope rules. A SUB or FUNCTION sees a variable dec
.item SHARED naming a variable that does not exist at module level creates it there; in a form's code it becomes a variable of that form, not a program-wide one.
.endlist
+.h2 Sound, Joysticks and Frames
+
+.list
+.item New statements from QBasic: BEEP, SOUND frequency, duration (in clock ticks, 18.2 a second) and PLAY with the whole music macro language (notes, octaves, lengths, dots, pauses, tempo, MN/ML/MS, and MF/MB for foreground or background music), plus the PLAY(n) function (notes still waiting in the background). They play on whatever sound hardware the computer has, or silently when there is none.
+.item PLAY's new I command plays the notes that follow on a General MIDI instrument (I1 to I128) through the music synthesizer; I0 goes back to tones.
+.item New functions from QBasic: STICK(n) (joystick position, 1 to 200) and STRIG(n) (joystick buttons), on any joystick DVX knows, or the keyboard when there is none.
+.item New statement FRAMEWAIT rate: waits for the next frame of a steady clock, keeping the desktop running, so a game loop runs at the same speed on every computer.
+.item BEEP, SOUND, PLAY and FRAMEWAIT are now keywords, and STICK and STRIG built-in functions: a variable, SUB or FUNCTION with one of these names must be renamed.
+.item Message boxes (MsgBox) play the desktop sound chosen for their kind on the Control Panel's Sounds tab.
+.endlist
+
+.link lang.sound See: Sound, Joysticks and Frames
+
.h2 Runtime
.list
diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
index 01aa83a9..623f2f01 100644
--- a/src/apps/kpunch/dvxbasic/langref.dhs
+++ b/src/apps/kpunch/dvxbasic/langref.dhs
@@ -244,6 +244,7 @@ REM This is a comment
.link lang.flow Flow Control (EXIT, CALL, GOTO, GOSUB, ON)
.link lang.io Input/Output (PRINT, INPUT, DATA/READ)
.link lang.misc Miscellaneous Statements (ON ERROR, SHELL, SLEEP, END)
+.link lang.sound Sound, Joysticks and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)
.topic lang.declarations
.title Declaration Statements
@@ -1179,6 +1180,127 @@ Terminates program execution immediately. Any form that is loaded is unloaded be
END
.endcode
+.topic lang.sound
+.title Sound, Joysticks and Frames
+.toc 2 Sound, Joysticks and Frames
+.index BEEP
+.index SOUND
+.index PLAY
+.index PLAY function
+.index Music macro language
+.index MB
+.index MF
+.index STICK
+.index STRIG
+.index FRAMEWAIT
+.index Frame rate
+
+.h1 Sound, Joysticks and Frames
+
+These statements work as they do in QBasic, on whatever sound hardware the computer has: a sound card, the PC speaker, or none at all, in which case the program runs the same but silently. Several programs can make sound at once. For sound files, tones of any shape and music files, see the sound.bas and music.bas include libraries; for more joystick detail, joystick.bas.
+
+.h2 BEEP
+
+Sounds a short beep (800 hertz for a quarter of a second).
+
+.code
+BEEP
+.endcode
+
+.h2 SOUND
+
+Plays a tone of frequency hertz (37 to 32767) for duration clock ticks; there are 18.2 ticks in a second, and fractions are allowed. A duration of 0 stops every sound the program has queued. Frequencies too high to hear (above 20000) are silent. Anything out of range is error 5 (Illegal function call).
+
+.code
+SOUND frequency, duration
+SOUND 440, 18.2 ' A for one second
+SOUND 440, 0 ' stop
+.endcode
+
+.h2 PLAY
+
+Plays a tune written in the music macro language. Letters may be upper or lower case and spaces are ignored. The settings (octave, length, tempo, style, instrument, foreground or background) stay from one PLAY to the next.
+
+.code
+PLAY commands$
+PLAY "T160 O3 L8 E D C D E E E4"
+.endcode
+
+.table
+ Command Meaning
+ ------- -------
+ A to G Play the note in the current octave. Follow it with # or + for sharp, - for flat, then optionally a length (1 to 64, as for L) and dots.
+ N n Play note n (1 to 84, seven octaves from the lowest C); N0 is a rest.
+ O n Octave 0 to 6 (default 4). Octave 3 starts at middle C.
+ > and < Go up or down one octave.
+ L n Length of the notes that follow: 1 a whole note, 2 a half, 4 a quarter (the default) ... 64.
+ P n Pause (rest) of length n (1 to 64).
+ . A dot after a note or pause makes it half as long again; each further dot adds half again.
+ T n Tempo in quarter notes a minute (32 to 255, default 120).
+ MN Music normal: each note sounds for 7/8 of its length (the default).
+ ML Music legato: notes sound for their whole length.
+ MS Music staccato: notes sound for 3/4 of their length.
+ MF Music foreground: PLAY, SOUND and BEEP wait until the sound has finished (the default).
+ MB Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting.
+ I n DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default).
+.endtable
+
+A string with a mistake in it plays nothing and raises error 5 (Illegal function call). QBasic's X command (play a string variable) and the "=" form of numbers are not supported; build the string with + instead, such as PLAY "O" + STR$(n). Without I, the notes are square-wave tones like the PC speaker's; with it, they play on the same synthesizer as MIDI music.
+
+.code
+PLAY "MB T120 L4 CDEF GAB>C" ' a scale in the background
+PLAY "I74 O4 L8 GAB>C" ' on the flute
+.endcode
+
+.h2 PLAY Function
+
+PLAY(n) returns how many notes are still waiting to be played in the background (n is ignored). A game can use it to keep its music going:
+
+.code
+If PLAY(0) < 4 Then PLAY "MB " + tune$
+.endcode
+
+.h2 STICK and STRIG
+
+STICK(n) returns where a joystick points, from 1 (all the way left or up) to 200 (all the way right or down), about 100 in the middle, or 0 when there is no such joystick. STICK(0) reads joystick A's X and remembers A's Y and joystick B's X and Y, which STICK(1), STICK(2) and STICK(3) then return; so call STICK(0) first.
+
+STRIG(n) returns True (-1) or False (0) for the buttons:
+
+.table
+ n Returns True when
+ - -----------------
+ 0 joystick A's first button was pressed since the last STRIG(0)
+ 1 joystick A's first button is down
+ 2 joystick B's first button was pressed since the last STRIG(2)
+ 3 joystick B's first button is down
+ 4 joystick A's second button was pressed since the last STRIG(4)
+ 5 joystick A's second button is down
+ 6 joystick B's second button was pressed since the last STRIG(6)
+ 7 joystick B's second button is down
+.endtable
+
+Joystick A is the first joystick; joystick B the second one, or the second stick of a joystick with four axes. When no joystick is connected the keyboard stands in for joystick A (see the Control Panel's Game Controllers tab). STRIG ON, STRIG OFF and ON STRIG are not supported; test STRIG in a Timer or a loop instead.
+
+.code
+x = STICK(0)
+y = STICK(1)
+If STRIG(0) Then FireShot
+.endcode
+
+.h2 FRAMEWAIT
+
+Waits until the next frame of a steady clock of rate frames a second begins, keeping the desktop and the program's forms running meanwhile. A game loop that calls it once a frame runs at the same speed on every computer. Frames the program was too busy for are skipped, not caught up. rate must be at least 1; rates above 1000 count as 1000. Changing the rate starts the clock over.
+
+.code
+FRAMEWAIT rate
+
+Do
+ MoveEverything
+ DrawEverything
+ FRAMEWAIT 30
+Loop Until quitting
+.endcode
+
.topic lang.fileio
.title File I/O
.toc 1 File I/O
@@ -1930,7 +2052,7 @@ MSGBOX message$ [, flags [, title$]]
result = MSGBOX(message$ [, flags [, title$]])
.endcode
-flags is the sum of a button-style constant and an icon constant. See Predefined Constants for the complete list.
+flags is the sum of a button-style constant and an icon constant. See Predefined Constants for the complete list. The message box plays the desktop sound chosen for its icon on the Control Panel's Sounds tab (the default beep without an icon).
.code
MsgBox "Operation complete"
@@ -2372,6 +2494,49 @@ Bit values used with GETATTR and SETATTR.
.index TcpRecv$
.index UdpSend
.index HostLookup$
+.index game.bas
+.index GameKeyDown
+.index joystick.bas
+.index JoyCount
+.index JoyX
+.index JoyY
+.index JoyAxis
+.index JoyButton
+.index JoyHat
+.index Joystick
+.index sound.bas
+.index PlayWave
+.index PlayWaveLoop
+.index PlayTone
+.index PlayNoise
+.index StopWave
+.index StopAllSounds
+.index SoundPlaying
+.index SoundVolume
+.index SoundInfo$
+.index SoundError$
+.index Sound
+.index music.bas
+.index MusicLoad
+.index MusicPlay
+.index MusicStop
+.index MusicPause
+.index MusicResume
+.index MusicPlaying
+.index MusicVolume
+.index MusicPosition
+.index MusicLength
+.index MusicSeek
+.index MusicFree
+.index MusicError$
+.index MusicMidiOpen
+.index MusicNoteOn
+.index MusicNoteOff
+.index MusicProgram
+.index MusicControl
+.index MusicPitchBend
+.index Music
+.index MIDI
.h1 Include Libraries
@@ -2387,6 +2552,10 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
help.bas Compile help source files and open the DVX help viewer
net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, ...), UDP messages and name lookups
print.bas Printing: text with the page setup and pictures, with or without the Print dialog; the Page Setup dialog
+ game.bas Games: whether a key is being held down right now (GameKeyDown)
+ joystick.bas Joysticks: how many there are, their names, where each stick points and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...)
+ sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...)
+ music.bas Music: play MOD, S3M, XM, IT and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, ...)
.endtable
.h2 Using an Include Library
@@ -2438,6 +2607,97 @@ End Sub
For printer output a line at a time, see LPRINT and OPEN "LPT1:".
+.h2 Held Keys for Games
+
+With game.bas in the project, GameKeyDown(keyCode) tells whether a key is being held down right now, so a game can move something for as long as a key is held instead of waiting for key presses. It returns True only while one of the program's own windows is active. Name the key with one of the GAMEKEY_ constants (the arrow keys, Shift, Ctrl, Alt, Space, Enter, Esc and a few letters) or with its PC keyboard scan code (add &H80 for the grey keys between the typing keys and the number pad). A Timer polls it:
+
+.code
+Sub Timer1_Timer ()
+ If GameKeyDown(GAMEKEY_LEFT) Then shipX = shipX - 2
+ If GameKeyDown(GAMEKEY_RIGHT) Then shipX = shipX + 2
+End Sub
+.endcode
+
+.h2 Joysticks
+
+With joystick.bas in the project, a program reads joysticks and game pads. JoyCount() says how many there are; they are numbered from 1, and so are their axes, buttons and hats. JoyX(joy) and JoyY(joy) say where the stick points, from -32768 (all the way left or up) to 32767 (all the way right or down), 0 in the middle; JoyAxis(joy, axis) reads any axis (3 and 4 are a throttle or rudder). JoyButton(joy, button) is True while the button is held down, and also once after a quick press that happened since the program last asked about that button, so a Timer never misses a shot. JoyHat(joy, hat) gives the direction a hat switch is pushed in degrees (0 is up, 90 right) or JOY_HAT_CENTERED. JoyName$(joy), JoyAxes(joy) and JoyButtons(joy) describe the joystick.
+
+.code
+Sub Timer1_Timer ()
+ If JoyCount() = 0 Then Exit Sub
+
+ shipX = shipX + JoyX(1) \ 8192
+ If JoyButton(1, 1) Then FireShot
+End Sub
+.endcode
+
+When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.
+
+.link lang.sound See: STICK and STRIG
+
+.h2 Sound
+
+With sound.bas in the project, a program plays sound files and tones through whatever sound hardware the computer has: a Sound Blaster, the PC speaker (tones only, unless sampled sound on the speaker is switched on) or nothing at all, in which case the program works the same but is silent. Several programs can play at once, and everything a program plays stops when it ends.
+
+PlayWave(fileName) plays a WAV or VOC file once and PlayWaveLoop(fileName) over and over; PlayTone(frequency, milliseconds, volume) plays a tone of frequency hertz (20 to 20000) and PlayNoise(frequency, milliseconds, volume) noise, which sounds brighter the higher the frequency; a length of 0 plays until stopped. Each returns a voice number, or SOUND_FAILED (0) when the sound could not be played, and then SoundError$() says why. StopWave(voice) stops one sound, StopAllSounds() all of the program's sounds, and SoundPlaying(voice) is True while a sound is still playing. SoundVolume(volume) sets the volume of everything the program plays. Volumes are percent (0 to 100). SoundInfo$() describes the sound device, like "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo".
+
+.code
+Dim Shared boom As Long
+
+Sub cmdFire_Click ()
+ boom = PlayWave("BOOM.WAV")
+
+ If boom = SOUND_FAILED Then
+ lblStatus.Caption = SoundError$()
+ End If
+End Sub
+.endcode
+
+WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND and PLAY work too, without sound.bas.
+
+.link lang.sound See: BEEP, SOUND and PLAY
+
+.h2 Music
+
+With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound, and MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none. Several programs can play music at once, and everything a program plays stops when it ends.
+
+MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.
+
+.code
+Dim Shared tune As Long
+
+Sub Form_Load ()
+ tune = MusicLoad("THEME.MOD")
+
+ If tune = MUSIC_FAILED Then
+ lblStatus.Caption = MusicError$()
+ Else
+ MusicPlay tune, True
+ End If
+End Sub
+.endcode
+
+A program can also play notes itself, like a keyboard. MusicMidiOpen() returns a live music that is already playing. MusicNoteOn(music, channel, note, velocity) starts a note and MusicNoteOff(music, channel, note) ends it; MusicProgram(music, channel, program) chooses a channel's instrument, MusicControl(music, channel, controller, value) sets a controller (7 volume, 10 pan, 11 expression, 64 the sustain pedal, 123 all notes off) and MusicPitchBend(music, channel, bend) bends the channel's notes (-8192 to 8191, two semitones either way at the ends). Channels are 1 to 16, and channel 10 (MUSIC_DRUMS) plays drums, one per note. Notes, velocities, instruments and controller values are 0 to 127; middle C is note 60, and instruments are numbered as in General MIDI (0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute). The same calls work on a MIDI file that is playing.
+
+.code
+Dim Shared keys As Long
+
+Sub Form_Load ()
+ keys = MusicMidiOpen()
+ MusicProgram keys, 1, 73
+End Sub
+
+Sub cmdC_Click ()
+ MusicNoteOn keys, 1, 60, 100
+ tmrNoteOff.Enabled = True
+End Sub
+
+Sub tmrNoteOff_Timer ()
+ MusicNoteOff keys, 1, 60
+ tmrNoteOff.Enabled = False
+End Sub
+.endcode
+
.h2 Network Connections
With net.bas in the project, a program talks to other computers over the network set up in the Control Panel. TcpConnect returns a handle (0 when it failed; NetError$ says why), TcpSend sends text, and TcpRecv$ returns whatever has arrived without waiting, so a Timer keeps the program responsive:
diff --git a/src/apps/kpunch/dvxbasic/runtime/serialize.c b/src/apps/kpunch/dvxbasic/runtime/serialize.c
index c3a08a8c..7f9c5290 100644
--- a/src/apps/kpunch/dvxbasic/runtime/serialize.c
+++ b/src/apps/kpunch/dvxbasic/runtime/serialize.c
@@ -75,7 +75,7 @@
// so both sides pull from the same definition.
#define BAS_MOD_MAGIC "DBXM"
#define BAS_MOD_MAGIC_LEN 4
-#define BAS_MOD_VERSION 2 // 2: per-frame GOSUB stack (OP_GOSUB), OP_STR_APPEND
+#define BAS_MOD_VERSION 3 // 2: per-frame GOSUB stack (OP_GOSUB), OP_STR_APPEND; 3: BEEP / SOUND / PLAY / STICK / STRIG / FRAMEWAIT opcodes (runtime and stub rebuilt together)
// Fixed header sizes on the wire. BAS_MOD_HEADER_SIZE is magic(4) +
// version(2) + 6 int32 header fields(24) = 30. BAS_DBG_HEADER_SIZE is
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c
index acacab59..3e877f5d 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.c
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.c
@@ -112,6 +112,10 @@
// SLEEP: pause between event pumps while waiting (microseconds).
#define BAS_SLEEP_POLL_USEC 10000
+// BEEP / SOUND / PLAY / FRAMEWAIT waiting without an event pump: pause
+// between checks (microseconds).
+#define BAS_WAIT_POLL_USEC 1000
+
// RND: rand() scaled by RAND_MAX + 1 so the result stays below 1.0, and
// the multiplier that spreads a negative RND(n) argument's fraction into
// the reseed value (RND(-1.5) and RND(-1) must seed differently).
@@ -180,6 +184,7 @@ static void dropValues(BasVmT *vm, int32_t count);
static const char *errorText(int32_t errNum);
static BasVmResultE execArith(BasVmT *vm, uint8_t op);
static BasVmResultE execCompare(BasVmT *vm, uint8_t op);
+static BasVmResultE execDevice(BasVmT *vm, uint8_t op);
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op);
static BasVmResultE execFsOp(BasVmT *vm, uint8_t op);
static BasVmResultE execLogical(BasVmT *vm, uint8_t op);
@@ -261,6 +266,7 @@ static BasVmResultE uiTarget(BasVmT *vm, bool hasCallback, BasValueT target);
static int32_t usingFieldLen(const char *fmt, int32_t fmtLen, int32_t pos);
static BasStringT *usingFormat(BasVmT *vm, const BasStringT *fmt, BasValueT val, bool last);
static bool validArrayDims(BasVmT *vm, int32_t dims);
+static bool waitWhile(BasVmT *vm, bool (*busy)(void *ctx));
static double wallSeconds(void);
static BasValueT zeroValue(uint8_t type);
@@ -671,6 +677,11 @@ void basVmSetCurrentFormVars(BasVmT *vm, BasValueT *vars, int32_t count) {
}
+void basVmSetDeviceCallbacks(BasVmT *vm, const BasDeviceCallbacksT *device) {
+ vm->device = *device;
+}
+
+
void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx) {
vm->doEventsFn = fn;
vm->doEventsCtx = ctx;
@@ -1958,6 +1969,15 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
case OP_FS_SETATTR:
return execFsOp(vm, op);
+ case OP_BEEP:
+ case OP_SOUND:
+ case OP_PLAY:
+ case OP_PLAY_COUNT:
+ case OP_STICK:
+ case OP_STRIG:
+ case OP_FRAME_WAIT:
+ return execDevice(vm, op);
+
// ============================================================
// DoEvents
// ============================================================
@@ -3919,6 +3939,130 @@ static BasVmResultE execCompare(BasVmT *vm, uint8_t op) {
}
+// BEEP, SOUND, PLAY, PLAY(n), STICK, STRIG and FRAMEWAIT through the
+// host's device callbacks (BasDeviceCallbacksT), with QBasic's argument
+// ranges. The statements then wait, keeping the host's events running,
+// for as long as the host says.
+static BasVmResultE execDevice(BasVmT *vm, uint8_t op) {
+ const BasDeviceCallbacksT *d = &vm->device;
+ BasValueT a;
+ BasValueT b;
+
+ switch (op) {
+ case OP_BEEP:
+ if (d->beep) {
+ d->beep(d->ctx);
+ }
+
+ break;
+
+ case OP_SOUND: {
+ if (!popArgs(vm, 2, &b, &a)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ double hz = basValToNumber(a);
+ double ticks = basValToNumber(b);
+
+ basValRelease(&a);
+ basValRelease(&b);
+
+ if (hz < BAS_SOUND_HZ_MIN || hz > BAS_SOUND_HZ_MAX || ticks < 0.0 || ticks > BAS_SOUND_TICKS_MAX) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
+ }
+
+ if (d->sound) {
+ d->sound(d->ctx, hz, ticks);
+ }
+
+ break;
+ }
+
+ case OP_PLAY: {
+ if (!popStringArg(vm, &a)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ bool ok = !d->play || d->play(d->ctx, a.strVal->data);
+
+ basValRelease(&a);
+
+ if (!ok) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call (a mistake in the PLAY string)");
+ return BAS_VM_ERROR;
+ }
+
+ break;
+ }
+
+ case OP_FRAME_WAIT: {
+ if (!pop(vm, &a)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ int32_t hz = basValToInt32(a);
+
+ basValRelease(&a);
+
+ if (hz < 1) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
+ }
+
+ if (!d->frameStart || !d->frameBusy) {
+ runtimeError(vm, BAS_ERR_DEVICE_UNAVAILABLE, errorText(BAS_ERR_DEVICE_UNAVAILABLE));
+ return BAS_VM_ERROR;
+ }
+
+ d->frameStart(d->ctx, hz);
+
+ if (!waitWhile(vm, d->frameBusy)) {
+ vm->running = false;
+ return BAS_VM_HALTED;
+ }
+
+ return BAS_VM_OK;
+ }
+
+ default: {
+ // PLAY(n), STICK(n), STRIG(n): one argument, one result
+ if (!pop(vm, &a)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ int32_t n = basValToInt32(a);
+ int32_t result = 0;
+
+ basValRelease(&a);
+
+ if ((op == OP_STICK && (n < 0 || n >= BAS_STICK_COUNT)) || (op == OP_STRIG && (n < 0 || n >= BAS_STRIG_COUNT))) {
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, "Illegal function call");
+ return BAS_VM_ERROR;
+ }
+
+ if (op == OP_PLAY_COUNT && d->playCount) {
+ result = d->playCount(d->ctx);
+ } else if (op == OP_STICK && d->stick) {
+ result = d->stick(d->ctx, n);
+ } else if (op == OP_STRIG && d->strig) {
+ result = d->strig(d->ctx, n);
+ }
+
+ return push(vm, basValLong(result)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
+ }
+ }
+
+ // BEEP, SOUND and PLAY wait while the music is in the foreground.
+ if (d->soundBusy && !waitWhile(vm, d->soundBusy)) {
+ vm->running = false;
+ return BAS_VM_HALTED;
+ }
+
+ return BAS_VM_OK;
+}
+
+
static BasVmResultE execFileOp(BasVmT *vm, uint8_t op) {
switch (op) {
case OP_FILE_OPEN: {
@@ -7621,6 +7765,22 @@ static bool validArrayDims(BasVmT *vm, int32_t dims) {
}
+// Keep the host's events running until busy says the wait is over (or
+// just check again after a pause without an event pump). False when the
+// host asks the program to stop.
+static bool waitWhile(BasVmT *vm, bool (*busy)(void *ctx)) {
+ while (busy(vm->device.ctx)) {
+ if (!vm->doEventsFn) {
+ usleep(BAS_WAIT_POLL_USEC);
+ } else if (!vm->doEventsFn(vm->doEventsCtx)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
// Wall-clock time in seconds (sub-second resolution), for SLEEP deadlines.
static double wallSeconds(void) {
struct timeval tv;
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h
index cbac38d7..ab5d7700 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.h
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.h
@@ -238,6 +238,42 @@ typedef struct {
void *ctx;
} BasPrinterCallbacksT;
+// ============================================================
+// Sound, joystick and frame callbacks (host-provided)
+// ============================================================
+//
+// BEEP, SOUND, PLAY and PLAY(n), STICK and STRIG, and FRAMEWAIT reach the
+// host's sound, joystick and game libraries through these. play returns
+// false for a macro string with a mistake (Illegal function call). After
+// BEEP, SOUND and PLAY the VM keeps the host's events running while
+// soundBusy says the statement must still wait (foreground music); after
+// frameStart(hz) (the host's frame clock moves on to its next frame at hz
+// frames a second) likewise while frameBusy says that frame has not
+// begun. Without beep / sound / play the statements do nothing and
+// PLAY(n) is 0, without stick / strig they read 0, and FRAMEWAIT without
+// frameStart raises "Device unavailable".
+
+// QBasic's ranges: SOUND frequency (Hz) and duration (clock ticks, 18.2
+// a second), STICK(0..3), STRIG(0..7)
+#define BAS_SOUND_HZ_MIN 37
+#define BAS_SOUND_HZ_MAX 32767
+#define BAS_SOUND_TICKS_MAX 65535
+#define BAS_STICK_COUNT 4
+#define BAS_STRIG_COUNT 8
+
+typedef struct {
+ void (*beep)(void *ctx);
+ void (*sound)(void *ctx, double hz, double ticks);
+ bool (*play)(void *ctx, const char *macro);
+ int32_t (*playCount)(void *ctx);
+ bool (*soundBusy)(void *ctx);
+ int32_t (*stick)(void *ctx, int32_t n);
+ int32_t (*strig)(void *ctx, int32_t n);
+ void (*frameStart)(void *ctx, int32_t hz);
+ bool (*frameBusy)(void *ctx);
+ void *ctx;
+} BasDeviceCallbacksT;
+
// ============================================================
// External library callbacks (host-provided)
// ============================================================
@@ -518,6 +554,7 @@ typedef struct {
BasUiCallbacksT ui;
BasSqlCallbacksT sql;
BasPrinterCallbacksT printer;
+ BasDeviceCallbacksT device;
// External library callbacks (set by host)
BasExternCallbacksT ext;
@@ -581,6 +618,10 @@ void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
// Set the printer callbacks (LPRINT, OPEN "LPTn:").
void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer);
+// Set the sound, joystick and frame callbacks (BEEP, SOUND, PLAY, STICK,
+// STRIG, FRAMEWAIT).
+void basVmSetDeviceCallbacks(BasVmT *vm, const BasDeviceCallbacksT *device);
+
// Set UI callbacks (for form/control system).
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c
index b520e2f8..5314f21c 100644
--- a/src/apps/kpunch/dvxbasic/test_compiler.c
+++ b/src/apps/kpunch/dvxbasic/test_compiler.c
@@ -4466,6 +4466,21 @@ int main(void) {
"True \nTrue \nTrue \nFalse \nTrue \nFalse \n"
);
+ // Sound, joystick and frame statements: with no host they are silent
+ // and read 0; argument counts are checked when compiling.
+ runProgram("Sound statements run silently without a host",
+ "BEEP\n"
+ "SOUND 440, 18.2\n"
+ "Play \"T120 L8 CDE\"\n"
+ "PRINT PLAY(0); STICK(0); Strig(1)\n",
+ "0 0 0 \n"
+ );
+ compileOnly("FRAMEWAIT in a game loop", "DO\n FRAMEWAIT 60\n IF STRIG(1) THEN EXIT DO\nLOOP\n");
+ expectCompileError("SOUND needs a duration", "SOUND 440\n", "Expected ','");
+ expectCompileError("PLAY as a function needs parentheses", "x = PLAY\n", "Expected '('");
+ expectCompileError("STICK takes one argument", "PRINT STICK(1, 2)\n", "Built-in 'STICK' expects 1-1 arguments");
+ expectCompileError("PLAY is a keyword", "SUB Play\nEND SUB\n", "");
+
testKeywordApi();
testTokenNames();
testLexerLimits();
diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c
index 90786ac9..35d08cc8 100644
--- a/src/apps/kpunch/dvxbasic/test_suite.c
+++ b/src/apps/kpunch/dvxbasic/test_suite.c
@@ -59,7 +59,6 @@
#define TOO_MANY_GLOBALS_SRC_MAX ((BAS_VM_MAX_GLOBALS + 1) * 32)
-
// ============================================================
// Recording UI stubs
// ============================================================
@@ -218,6 +217,16 @@ typedef struct {
static void breakpointRecordStub(void *ctx, int32_t line);
static BasModuleT *compileSource(const char *name, const char *source);
+static void deviceBeepStub(void *ctx);
+static bool deviceDoEventsStub(void *ctx);
+static bool deviceFrameBusyStub(void *ctx);
+static void deviceFrameStartStub(void *ctx, int32_t hz);
+static int32_t devicePlayCountStub(void *ctx);
+static bool devicePlayStub(void *ctx, const char *macro);
+static bool deviceSoundBusyStub(void *ctx);
+static void deviceSoundStub(void *ctx, double hz, double ticks);
+static int32_t deviceStickStub(void *ctx, int32_t n);
+static int32_t deviceStrigStub(void *ctx, int32_t n);
static bool doEventsAbortStub(void *ctx);
static bool doEventsCountdownStub(void *ctx);
static bool doEventsResumeStub(void *ctx);
@@ -246,6 +255,7 @@ static void testCommandArgs(const char *name);
static void testCompileError(const char *name, const char *source, const char *needle);
static void testDebuggerApi(void);
static void testDebugInfoRoundTrip(const char *name);
+static void testDeviceStatements(void);
static void testDispatchEq(const char *name, const TestDispatchT *d, const char *expected);
static void testEq(const char *name, const char *source, const char *expected);
static void testExternCalls(void);
@@ -309,6 +319,30 @@ static const BasUiCallbacksT sUiStubs = {
// Installed by runAndCapture when non-NULL (see TEST_UI_EQ).
static const BasUiCallbacksT *sRunUi = NULL;
+// Installed by runAndCapture when non-NULL, with deviceDoEventsStub (the
+// sound, joystick and frame statements).
+static const BasDeviceCallbacksT *sRunDevice = NULL;
+
+// What the device stubs saw and how long they keep a statement waiting
+static char sDeviceLog[256];
+static int32_t sDeviceSoundWaits = 0;
+static int32_t sDeviceFrameWaits = 0;
+static int32_t sDeviceEvents = 0;
+static int32_t sDeviceEventsLeft = 0;
+
+static const BasDeviceCallbacksT sDeviceStubs = {
+ .beep = deviceBeepStub,
+ .sound = deviceSoundStub,
+ .play = devicePlayStub,
+ .playCount = devicePlayCountStub,
+ .soundBusy = deviceSoundBusyStub,
+ .stick = deviceStickStub,
+ .strig = deviceStrigStub,
+ .frameStart = deviceFrameStartStub,
+ .frameBusy = deviceFrameBusyStub,
+ .ctx = NULL
+};
+
// ============================================================
// Test driver
@@ -360,6 +394,70 @@ static BasModuleT *compileSource(const char *name, const char *source) {
}
+static void deviceBeepStub(void *ctx) {
+ (void)ctx;
+ snprintf(sDeviceLog + strlen(sDeviceLog), sizeof(sDeviceLog) - strlen(sDeviceLog), "beep;");
+}
+
+
+// Counts the events a waiting statement pumps; false (stop the program)
+// once sDeviceEventsLeft runs out when it was set.
+static bool deviceDoEventsStub(void *ctx) {
+ (void)ctx;
+ sDeviceEvents++;
+ return sDeviceEventsLeft == 0 || --sDeviceEventsLeft > 0;
+}
+
+
+static bool deviceFrameBusyStub(void *ctx) {
+ (void)ctx;
+ return sDeviceFrameWaits-- > 0;
+}
+
+
+static void deviceFrameStartStub(void *ctx, int32_t hz) {
+ (void)ctx;
+ snprintf(sDeviceLog + strlen(sDeviceLog), sizeof(sDeviceLog) - strlen(sDeviceLog), "frame %ld;", (long)hz);
+}
+
+
+static int32_t devicePlayCountStub(void *ctx) {
+ (void)ctx;
+ return 7;
+}
+
+
+static bool devicePlayStub(void *ctx, const char *macro) {
+ (void)ctx;
+ snprintf(sDeviceLog + strlen(sDeviceLog), sizeof(sDeviceLog) - strlen(sDeviceLog), "play %s;", macro);
+ return strchr(macro, 'X') == NULL;
+}
+
+
+static bool deviceSoundBusyStub(void *ctx) {
+ (void)ctx;
+ return sDeviceSoundWaits-- > 0;
+}
+
+
+static void deviceSoundStub(void *ctx, double hz, double ticks) {
+ (void)ctx;
+ snprintf(sDeviceLog + strlen(sDeviceLog), sizeof(sDeviceLog) - strlen(sDeviceLog), "sound %g %g;", hz, ticks);
+}
+
+
+static int32_t deviceStickStub(void *ctx, int32_t n) {
+ (void)ctx;
+ return n * 10 + 1;
+}
+
+
+static int32_t deviceStrigStub(void *ctx, int32_t n) {
+ (void)ctx;
+ return n % 2 ? -1 : 0;
+}
+
+
// DoEvents stub that tells the VM to stop (host shutting down).
static bool doEventsAbortStub(void *ctx) {
(void)ctx;
@@ -723,6 +821,11 @@ static int32_t runAndCapture(const char *source, char *out, int32_t outSize, cha
basVmSetUiCallbacks(vm, sRunUi);
}
+ if (sRunDevice) {
+ basVmSetDeviceCallbacks(vm, sRunDevice);
+ basVmSetDoEventsCallback(vm, deviceDoEventsStub, NULL);
+ }
+
BasVmResultE result = basVmRun(vm);
sUiCap = NULL;
@@ -1173,9 +1276,6 @@ static void testCompileError(const char *name, const char *source, const char *n
}
-// Debug-info side channel: serialize the debugVars table, wipe it on a
-// second compile of the same source, deserialize, and compare. Also
-// drives the truncated-stream rejection paths.
static void testDebuggerApi(void) {
const char *source =
"DIM x AS INTEGER\n"
@@ -1470,6 +1570,53 @@ static void testDebugInfoRoundTrip(const char *name) {
}
+// Debug-info side channel: serialize the debugVars table, wipe it on a
+// second compile of the same source, deserialize, and compare. Also
+// drives the truncated-stream rejection paths.
+// BEEP, SOUND, PLAY, PLAY(n), STICK, STRIG and FRAMEWAIT reach the device
+// callbacks with QBasic's ranges checked, and wait (pumping events) while
+// the host says so.
+static void testDeviceStatements(void) {
+ char out[CAPTURE_MAX];
+ char err[256];
+
+ sRunDevice = &sDeviceStubs;
+ sDeviceLog[0] = '\0';
+ TEST_EQ("device-calls", "BEEP\nSOUND 440, 18.2\nPLAY \"T90 CDE\"\nFRAMEWAIT 60\nPRINT PLAY(0); STICK(0); STICK(3); STRIG(2); STRIG(3)\n", "7 1 31 0 -1 \n");
+ expectTrue("device-calls-log", strcmp(sDeviceLog, "beep;sound 440 18.2;play T90 CDE;frame 60;") == 0, sDeviceLog);
+
+ sDeviceSoundWaits = 3;
+ sDeviceFrameWaits = 2;
+ sDeviceEvents = 0;
+ TEST_EQ("device-waits", "PLAY \"C\"\nFRAMEWAIT 30\nPRINT 1\n", "1 \n");
+ expectTrue("device-waits-pump", sDeviceEvents == 5, "wait loops pump events");
+
+ TEST_RUNTIME_ERROR("device-sound-low", "SOUND 36, 1\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("device-sound-high", "SOUND 32768, 1\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("device-sound-ticks", "SOUND 440, 65536\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("device-sound-neg", "SOUND 440, -1\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("device-stick-range", "PRINT STICK(4)\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("device-strig-range", "PRINT STRIG(-1)\n", "Illegal function call");
+ TEST_RUNTIME_ERROR("device-play-bad", "PLAY \"CXD\"\n", "PLAY string");
+ TEST_RUNTIME_ERROR("device-frame-zero", "FRAMEWAIT 0\n", "Illegal function call");
+ TEST_EQ("device-play-trapped", "ON ERROR GOTO h\nPLAY \"X\"\nEND\nh:\nPRINT ERR\nRESUME NEXT\n", "5 \n");
+
+ // Closing the program while it waits stops it there.
+ sDeviceSoundWaits = 100;
+ sDeviceEventsLeft = 2;
+ memset(out, 0, sizeof(out));
+ runAndCapture("BEEP\nPRINT \"after\"\n", out, sizeof(out), err, sizeof(err));
+ expectTrue("device-wait-stopped", strstr(out, "after") == NULL, out);
+ sDeviceSoundWaits = 0;
+ sDeviceEventsLeft = 0;
+
+ // Without a host: silent, 0, and FRAMEWAIT unavailable.
+ sRunDevice = NULL;
+ TEST_EQ("device-none", "BEEP\nSOUND 440, 1\nPLAY \"CX\"\nPRINT PLAY(0); STICK(0); STRIG(1)\n", "0 0 0 \n");
+ TEST_RUNTIME_ERROR("device-none-frame", "FRAMEWAIT 60\n", "Device unavailable");
+}
+
+
// Breakpoints, step into/over/out, run to cursor, and the runSubLoop
// pause/resume/abort protocol used by the IDE debugger.
@@ -6832,6 +6979,7 @@ int main(void) {
// SLEEP keeps pumping the host's events and stops when told to.
testSleepPumpsEvents("sleep-pumps-events");
+ testDeviceStatements();
testStrAppend();
testObfuscateCaption("obfuscate-caption-kept");
diff --git a/src/apps/kpunch/progman/userHelp.dhs b/src/apps/kpunch/progman/userHelp.dhs
index e78c1d2d..8cdb0d88 100644
--- a/src/apps/kpunch/progman/userHelp.dhs
+++ b/src/apps/kpunch/progman/userHelp.dhs
@@ -637,13 +637,17 @@ Closing the Program Manager window leaves DVX, after asking you first.
.index Mouse Settings
.index Video Mode
.index Screen Resolution
+.index Sounds
+.index Volume
.h1 Changing Settings
-The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network and the printer are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.
+The mouse, the screen colors, the desktop picture, the screen resolution, which program opens each kind of document, the network, the printer, joysticks and the sounds are changed with the settings program, which has an icon in the Installed Programs group of the Program Manager. Start it like any other program. Its own help, opened with F1 or its Help menu, explains every setting.
Settings you keep are saved, and DVX uses them again the next time it starts.
+DVX plays a short sound when it starts and when it ends, and when a message appears (a different one for errors, warnings, questions and information). Each sound can be changed or turned off, and the volume set, in the settings program.
+
.note tip
If a new screen resolution leaves the screen unreadable, just wait: DVX goes back to the old one after 15 seconds unless you confirm the change.
.endnote
@@ -775,6 +779,7 @@ Programs can have their own shortcut keys. Look for them at the right edge of ea
.toc 0 Troubleshooting
.index Troubleshooting
.index Problems
+.index No Sound
.index Mouse Does Not Move
.index Screen Problems
.index Memory
@@ -785,6 +790,10 @@ Programs can have their own shortcut keys. Look for them at the right edge of ea
.index Safe Video Mode
.index Shift Key at Start
.index Installing DVX
+.index Setup
+.index Uninstalling DVX
+.index Upgrading DVX
+.index Read-only Disk
.index Empty Floppy Drive
.h1 Troubleshooting
@@ -807,13 +816,23 @@ If DVX starts but the screen cannot be read (the monitor cannot show the saved r
You can pick another resolution at any time with the settings program (see Changing Settings).
+.h2 There is no sound
+
+DVX finds the sound hardware by itself. A Sound Blaster needs its BLASTER setting in AUTOEXEC.BAT, for example SET BLASTER=A220 I5 D1 H5 (the card's manual gives the numbers). Without a sound card DVX uses the PC speaker, which plays tones and beeps but not recorded sounds. The settings program shows the sound hardware in use, lets you pick it yourself and sets the volume; if a sound is set to none there, nothing is heard for it. Without any sound hardware DVX and its programs work the same, silently.
+
.h2 An empty floppy or CD-ROM drive
When a program tries to read a drive that has no disk in it, or to write to a write-protected disk, the program simply reports that it could not read or write the file. DVX never stops at the DOS "Abort, Retry, Fail" question. Put the disk in and try again.
-.h2 Running DVX from a CD-ROM or floppy disk
+.h2 Installing, upgrading and removing DVX
-DVX can copy itself to your hard disk. Change to the drive and folder that hold DVX.EXE and type INSTALL. DVX starts and opens a window that asks for the folder to copy DVX to, and can make DVX start when the computer starts. Press F1 in that window for help with each step.
+DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. Setup shows the licence, asks for the folder to install DVX in, checks that there is room and unpacks DVX there, asking for each floppy disk in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.
+
+To upgrade or repair DVX, run Setup again and choose the same folder: answer Yes to keep your settings. To remove DVX, run SETUP in the DVX folder (for example C:\DVX\SETUP) and choose Uninstall. Files you added to the folder yourself are not removed, and your settings are only removed if you ask.
+
+.h2 Running DVX from a CD-ROM or a write-protected disk
+
+DVX also runs straight from a disk it cannot write to. It then tells you once that changed settings last only until you exit DVX, and the help files are used as they are. To keep your settings, install DVX on your hard disk with Setup.
.h2 A program is already running
diff --git a/src/apps/kpunch/setup/dosroot/install.bat b/src/apps/kpunch/setup/dosroot/install.bat
deleted file mode 100644
index 0714c36f..00000000
--- a/src/apps/kpunch/setup/dosroot/install.bat
+++ /dev/null
@@ -1,12 +0,0 @@
-@ECHO OFF
-REM Copies DVX to your hard disk: starts DVX from this disk together with
-REM its Setup program. Run it from the folder that holds DVX.EXE.
-IF NOT EXIST DVX.EXE GOTO WHERE
-DVX.EXE /RUN=APPS\KPUNCH\SETUP\SETUP.APP
-GOTO END
-:WHERE
-ECHO Change to the drive and folder that hold DVX.EXE first, for example:
-ECHO D:
-ECHO CD \
-ECHO INSTALL
-:END
diff --git a/src/apps/kpunch/setup/icon32.bmp b/src/apps/kpunch/setup/icon32.bmp
deleted file mode 100644
index 7028ebee..00000000
--- a/src/apps/kpunch/setup/icon32.bmp
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:553aaaaeb3bf8e5dfbcab5cdd75627d908e47036b20480e08351335f86195388
-size 3126
diff --git a/src/apps/kpunch/setup/setup.c b/src/apps/kpunch/setup/setup.c
deleted file mode 100644
index eb0396a3..00000000
--- a/src/apps/kpunch/setup/setup.c
+++ /dev/null
@@ -1,456 +0,0 @@
-// The MIT License (MIT)
-//
-// Copyright (C) 2026 Scott Duensing
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to
-// deal in the Software without restriction, including without limitation the
-// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-// sell copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-// IN THE SOFTWARE.
-
-// setup.c -- Setup: copies DVX to a folder on the hard disk
-//
-// Runs inside DVX, started from wherever DVX is (INSTALL.BAT on a CD-ROM or
-// floppy runs DVX /RUN=APPS\KPUNCH\SETUP\SETUP.APP). The window asks for
-// the target folder and whether DVX should start with the computer.
-// Install checks the folder and the free space, copies with progress and
-// Cancel, stores the display mode in the new DVX.INI and, when asked and
-// confirmed, adds DVX to AUTOEXEC.BAT (after backing it up). The work
-// itself is in setupCore.c.
-//
-// Main-loop app: the Install button (a callback on the shell's task) only
-// sets a flag; the copy runs on the app's own task in time slices, so the
-// desktop stays live, Cancel is seen between slices, and the questions
-// are dialogs shown from that task.
-
-#include "dvxApp.h"
-#include "dvxDlg.h"
-#include "dvxPlat.h"
-#include "dvxWgt.h"
-#include "dvxWm.h"
-#include "box/box.h"
-#include "button/button.h"
-#include "checkbox/checkbox.h"
-#include "label/label.h"
-#include "progressBar/progress.h"
-#include "textInput/textInpt.h"
-#include "setupCore.h"
-#include "shellApp.h"
-#include "taskSwch.h"
-
-#include
-#include
-#include
-#include
-#include
-#include "dvxMem.h"
-
-// ============================================================
-// Constants
-// ============================================================
-
-#define SETUP_TITLE "DVX Setup"
-#define SETUP_WIN_W 480
-#define SETUP_WIN_H 280
-#define SETUP_SPACING 8
-#define SETUP_SLICE_MS 100
-#define SETUP_DEST_FOLDER "DVX"
-#define SETUP_MSG_MAX (DVX_MAX_PATH * 2 + SETUP_ERR_MAX)
-
-// Menu command IDs
-#define CMD_HELP_CONTENTS 100
-#define CMD_HELP_ABOUT 101
-
-typedef struct {
- DxeAppContextT *ctx;
- WindowT *win;
- WidgetT *dest; // target folder
- WidgetT *autoexec; // "start with the computer" (NULL without a boot drive)
- WidgetT *status;
- WidgetT *bar;
- WidgetT *installBtn;
- char srcDir[DVX_MAX_PATH];
- bool quit;
- bool busy; // an install is running
- bool startRequested; // Install clicked; the app task starts it
- bool cancelRequested;
-} SetupStateT;
-
-// ============================================================
-// Module state
-// ============================================================
-
-static SetupStateT sState;
-
-// ============================================================
-// Prototypes
-// ============================================================
-
-void appShutdown(void);
-static void installAutoexec(const char *destDir);
-static bool installCheck(const char *destDir, bool *upgrade);
-static FileOpsStepE installCopy(const char *destDir);
-static void installRun(void);
-static void onCancel(WidgetT *w);
-static void onClose(WindowT *win);
-static void onInstall(WidgetT *w);
-static void onMenu(WindowT *win, int32_t menuId);
-static void setBusy(bool busy);
-static void showProgress(FileOpsJobT *job);
-int32_t appMain(DxeAppContextT *ctx);
-
-// ============================================================
-// App descriptor
-// ============================================================
-
-SHELL_APP_DESCRIPTOR(
- .name = "Setup",
- .hasMainLoop = true,
- .multiInstance = false,
- .stackSize = SHELL_STACK_DEFAULT,
- .priority = TS_PRIORITY_NORMAL
-);
-
-
-// Force kill or shell shutdown: stop the copy and leave the main loop.
-void appShutdown(void) {
- sState.cancelRequested = true;
- sState.quit = true;
-}
-
-
-// Show the exact line, ask, then back up AUTOEXEC.BAT and add it.
-static void installAutoexec(const char *destDir) {
- AppContextT *ac = sState.ctx->shellCtx;
- char path[DVX_MAX_PATH];
- char line[DVX_MAX_PATH];
- char bak[DVX_MAX_PATH];
- char msg[SETUP_MSG_MAX];
- char err[SETUP_ERR_MAX];
-
- if (!setupAutoexecPath(platformBootDrive(), path, sizeof(path)) || !setupAutoexecLine(destDir, line, sizeof(line)) || !platformPathSibling(bak, sizeof(bak), path, SETUP_AUTOEXEC_BAK)) {
- return;
- }
-
- if (setupAutoexecHasLine(path, line)) {
- dvxLog("Setup: %s already starts DVX", path);
- return;
- }
-
- snprintf(msg, sizeof(msg), "Setup will add this line to the end of %s, so DVX starts when the computer starts:\n\n%s\n\nThe file as it is now is saved as %s first. Add the line?", path, line, bak);
-
- if (dvxMessageBox(ac, SETUP_TITLE, msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
- return;
- }
-
- if (!setupAutoexecAdd(path, line, err, sizeof(err))) {
- dvxErrorBox(ac, SETUP_TITLE, err);
- return;
- }
-
- dvxLog("Setup: added '%s' to %s", line, path);
-}
-
-
-// The folder, the free space, and on a folder that already has DVX the
-// user's agreement to update it.
-static bool installCheck(const char *destDir, bool *upgrade) {
- AppContextT *ac = sState.ctx->shellCtx;
- char msg[SETUP_MSG_MAX];
- char err[SETUP_ERR_MAX];
- char needText[FILEOPS_SIZE_TEXT_MAX];
- char freeText[FILEOPS_SIZE_TEXT_MAX];
- uint64_t needed = 0;
- uint64_t total = 0;
- uint64_t avail = 0;
-
- if (!setupCheckDest(sState.srcDir, destDir, err, sizeof(err))) {
- dvxErrorBox(ac, SETUP_TITLE, err);
- return false;
- }
-
- if (!setupSpaceNeeded(sState.srcDir, &needed)) {
- dvxLog("Setup: some of %s could not be read while adding up its size", sState.srcDir);
- }
-
- if (platformDiskSpace(destDir, &total, &avail) && avail < needed) {
- fileOpsFormatSize(needed, needText, sizeof(needText));
- fileOpsFormatSize(avail, freeText, sizeof(freeText));
- snprintf(msg, sizeof(msg), "There is not enough free space for DVX on that disk.\n\nDVX needs %s bytes; %s bytes are free.", needText, freeText);
- dvxErrorBox(ac, SETUP_TITLE, msg);
- return false;
- }
-
- *upgrade = setupIsInstalled(destDir);
-
- if (*upgrade) {
- snprintf(msg, sizeof(msg), "DVX is already installed in %s.\n\nSetup will replace its program files and keep your settings. Continue?", destDir);
-
- if (dvxMessageBox(ac, SETUP_TITLE, msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
- return false;
- }
- }
-
- if (platformMkdirRecursive(destDir) != 0) {
- snprintf(msg, sizeof(msg), "Setup could not create the folder %s.", destDir);
- dvxErrorBox(ac, SETUP_TITLE, msg);
- return false;
- }
-
- return true;
-}
-
-
-// Run the copy job in time slices. "Exists" questions follow
-// setupOverwriteAnswer; an error offers Retry or Cancel.
-static FileOpsStepE installCopy(const char *destDir) {
- AppContextT *ac = sState.ctx->shellCtx;
- FileOpsJobT *job = setupJobCreate(sState.srcDir, destDir);
- PlatformTicksT slice = PLATFORM_MS_TO_TICKS(SETUP_SLICE_MS);
- PlatformTicksT sliceStart;
- FileOpsStepE step = FileOpsStepCancelledE;
-
- if (!job) {
- dvxErrorBox(ac, SETUP_TITLE, "Setup could not read the DVX files, or there is not enough memory.");
- return step;
- }
-
- sliceStart = platformClock();
-
- for (step = fileOpsJobStep(job); step != FileOpsStepDoneE && step != FileOpsStepCancelledE; step = fileOpsJobStep(job)) {
- if (sState.cancelRequested) {
- fileOpsJobCancel(job);
- } else if (step == FileOpsStepAskE) {
- fileOpsJobAnswer(job, setupOverwriteAnswer(sState.srcDir, job));
- } else if (step == FileOpsStepErrorE) {
- showProgress(job);
-
- bool retry = dvxMessageBox(ac, SETUP_TITLE, fileOpsJobMessage(job), MB_RETRYCANCEL | MB_ICONERROR) == ID_RETRY;
-
- fileOpsJobAnswer(job, retry ? FileOpsAnswerRetryE : FileOpsAnswerCancelE);
- sliceStart = platformClock();
- } else if (platformClock() - sliceStart >= slice) {
- showProgress(job);
- tsYield();
- sliceStart = platformClock();
- }
- }
-
- showProgress(job);
- fileOpsJobFree(job);
- return step;
-}
-
-
-// Install clicked: check, copy, settings, AUTOEXEC.BAT, report.
-static void installRun(void) {
- AppContextT *ac = sState.ctx->shellCtx;
- char destDir[DVX_MAX_PATH];
- char msg[SETUP_MSG_MAX];
- bool upgrade = false;
-
- dvxCopyTrimmed(destDir, sizeof(destDir), wgtGetText(sState.dest));
-
- if (!installCheck(destDir, &upgrade)) {
- return;
- }
-
- setBusy(true);
- dvxLog("Setup: copying %s to %s", sState.srcDir, destDir);
-
- FileOpsStepE result = installCopy(destDir);
-
- // Closing the window cancels without a message: Setup is going away.
- if (result != FileOpsStepDoneE) {
- setBusy(false);
- wgtSetText(sState.status, "Setup was cancelled.");
- snprintf(msg, sizeof(msg), "Setup was cancelled. The files copied so far are still in %s.", destDir);
-
- if (!sState.quit) {
- dvxInfoBox(ac, SETUP_TITLE, msg);
- }
-
- return;
- }
-
- // A new install starts in the mode Setup is running in: it works here.
- if (!upgrade && !setupWriteIni(destDir, ac->display.width, ac->display.height, ac->display.format.bitsPerPixel)) {
- snprintf(msg, sizeof(msg), "The files were copied, but the settings file in %s could not be written.", destDir);
- dvxErrorBox(ac, SETUP_TITLE, msg);
- }
-
- if (sState.autoexec && wgtCheckboxIsChecked(sState.autoexec)) {
- installAutoexec(destDir);
- }
-
- setBusy(false);
- wgtSetText(sState.status, "DVX is installed.");
- dvxLog("Setup: finished");
- snprintf(msg, sizeof(msg), "DVX is installed in %s.\n\nTo start it from the DOS prompt, type %s" DVX_PATH_SEP "%s", destDir, destDir, SETUP_EXE_NAME);
- dvxInfoBox(ac, SETUP_TITLE, msg);
-}
-
-
-// Cancel stops a running copy; otherwise it closes Setup.
-static void onCancel(WidgetT *w) {
- (void)w;
-
- if (sState.busy) {
- sState.cancelRequested = true;
- return;
- }
-
- sState.quit = true;
-}
-
-
-// Closing the window during a copy cancels it first.
-static void onClose(WindowT *win) {
- (void)win;
- sState.cancelRequested = true;
- sState.quit = true;
-}
-
-
-static void onInstall(WidgetT *w) {
- (void)w;
-
- if (!sState.busy) {
- sState.startRequested = true;
- }
-}
-
-
-static void onMenu(WindowT *win, int32_t menuId) {
- (void)win;
-
- switch (menuId) {
- case CMD_HELP_CONTENTS:
- shellOpenHelp(sState.ctx->shellCtx, sState.ctx->helpFile);
- break;
-
- case CMD_HELP_ABOUT:
- dvxInfoBox(sState.ctx->shellCtx, "About Setup", "Setup\n\nCopies DVX to a folder on your hard disk.\n\nCopyright 2026 Scott Duensing");
- break;
-
- default:
- break;
- }
-}
-
-
-static void setBusy(bool busy) {
- sState.busy = busy;
- sState.cancelRequested = false;
- wgtSetEnabled(sState.installBtn, !busy);
- wgtSetEnabled(sState.dest, !busy);
-
- if (sState.autoexec) {
- wgtSetEnabled(sState.autoexec, !busy);
- }
-}
-
-
-static void showProgress(FileOpsJobT *job) {
- FileOpsProgressT p;
- char line[SETUP_MSG_MAX];
-
- fileOpsJobProgress(job, &p);
- snprintf(line, sizeof(line), "Copying %s", p.current);
- wgtSetText(sState.status, line);
- wgtProgressBarSetValue(sState.bar, p.percent);
-}
-
-
-int32_t appMain(DxeAppContextT *ctx) {
- AppContextT *ac = ctx->shellCtx;
- char boot = platformBootDrive();
- char text[SETUP_MSG_MAX];
-
- memset(&sState, 0, sizeof(sState));
- sState.ctx = ctx;
-
- if (!getcwd(sState.srcDir, sizeof(sState.srcDir))) {
- dvxErrorBox(ac, SETUP_TITLE, "Setup cannot tell which folder DVX is running from.");
- return -1;
- }
-
- sState.win = dvxCreateWindowCentered(ac, SETUP_TITLE, SETUP_WIN_W, SETUP_WIN_H, false);
-
- if (!sState.win) {
- return -1;
- }
-
- sState.win->onClose = onClose;
- sState.win->onMenu = onMenu;
-
- MenuBarT *menuBar = wmAddMenuBar(sState.win);
- MenuT *helpMenu = menuBar ? wmAddMenu(menuBar, "&Help") : NULL;
-
- if (helpMenu) {
- wmAddMenuItem(helpMenu, "&Contents\tF1", CMD_HELP_CONTENTS);
- wmAddMenuSeparator(helpMenu);
- wmAddMenuItem(helpMenu, "&About Setup...", CMD_HELP_ABOUT);
- }
-
- WidgetT *root = wgtInitWindow(ac, sState.win);
-
- root->spacing = wgtPixels(SETUP_SPACING);
- wgtLabel(root, "Setup copies DVX to a folder on your hard disk.");
- snprintf(text, sizeof(text), "Copy from: %s", sState.srcDir);
- wgtLabel(root, text);
- wgtLabel(root, "Install &to:");
- sState.dest = wgtTextInput(root, DVX_MAX_PATH - 1);
-
- text[0] = '\0';
-
- if (boot) {
- snprintf(text, sizeof(text), "%c:" DVX_PATH_SEP SETUP_DEST_FOLDER, boot);
- sState.autoexec = wgtCheckbox(root, "&Start DVX when the computer starts (AUTOEXEC.BAT)");
- }
-
- wgtSetText(sState.dest, text);
- sState.status = wgtLabel(root, "");
- sState.bar = wgtProgressBar(root);
-
- WidgetT *btnRow = wgtHBox(root);
-
- btnRow->align = AlignEndE;
- btnRow->spacing = wgtPixels(SETUP_SPACING);
-
- sState.installBtn = wgtButton(btnRow, "&Install");
-
- WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
-
- sState.installBtn->onClick = onInstall;
- cancelBtn->onClick = onCancel;
-
- sState.status->weight = WGT_WEIGHT_FILL;
- dvxFitWindow(ac, sState.win);
- wgtSetFocused(sState.dest);
-
- while (!sState.quit) {
- if (sState.startRequested) {
- sState.startRequested = false;
- installRun();
- }
-
- tsYield();
- }
-
- dvxDestroyWindow(ac, sState.win);
- sState.win = NULL;
- return 0;
-}
diff --git a/src/apps/kpunch/setup/setup.dhs b/src/apps/kpunch/setup/setup.dhs
deleted file mode 100644
index 790ad693..00000000
--- a/src/apps/kpunch/setup/setup.dhs
+++ /dev/null
@@ -1,102 +0,0 @@
-# The MIT License (MIT)
-#
-# Copyright (C) 2026 Scott Duensing
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to
-# deal in the Software without restriction, including without limitation the
-# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-# sell copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-# IN THE SOFTWARE.
-
-
-# Setup help. Compiled to setup.hlp next to setup.app; F1 and
-# Help > Contents in the Setup window open it.
-
-.topic setup.overview
-.title Setup
-.toc 0 Setup
-.default
-.index Setup
-.index Install
-.index Installing DVX
-.index INSTALL.BAT
-.index Copy DVX to the Hard Disk
-
-.h1 Setup
-
-Setup copies DVX to a folder on your hard disk, so you can start it without the CD-ROM or floppy disk it came on. It can also make DVX start by itself when the computer starts.
-
-.h2 Starting Setup from DOS
-
-If DVX is on a CD-ROM or floppy disk, change to the drive and folder that hold DVX.EXE and type INSTALL. For example, with DVX on a CD-ROM in drive D:
-
-.code
-D:
-CD \
-INSTALL
-.endcode
-
-DVX starts from that disk and opens Setup straight away. Setup copies the DVX that is running, from the folder it is running from.
-
-You can also start Setup from inside DVX at any time, from the Installed Programs group of Program Manager, to copy DVX to another folder.
-
-.h2 Installing
-
-.list
-.item Install to: type the folder to copy DVX to, starting with the drive, for example C:\DVX. Setup creates the folder if it does not exist yet.
-.item Start DVX when the computer starts: tick this to have DVX start every time the computer starts (see below).
-.item Click Install.
-.endlist
-
-Setup first checks that there is enough free space on that disk. It then copies every DVX file and folder, showing the file being copied and a bar that fills up as it goes. Click Cancel to stop; the files copied so far stay in the folder.
-
-When it is done, Setup tells you how to start the new copy: type the folder and DVX, for example C:\DVX\DVX, at the DOS prompt.
-
-.h2 Settings
-
-A new copy of DVX gets the same settings as the DVX you are running, with one change: it starts in the screen mode Setup is running in, because that mode is known to work on this computer.
-
-If the folder already holds DVX, Setup asks before it goes on. It then replaces the program files but keeps your settings: everything in the CONFIG folder there (screen, mouse and colors, your program groups, file types) stays as it was.
-
-.h2 Starting DVX with the Computer
-
-When "Start DVX when the computer starts" is ticked, Setup offers to add one line to the end of AUTOEXEC.BAT, the file DOS runs every time the computer starts. Before changing anything it shows you the exact line, for example:
-
-.code
-C:\DVX\DVX.EXE
-.endcode
-
-and asks whether to add it. If you answer Yes, Setup first saves the file as it is now under the name AUTOEXEC.DVX, in the same folder, so you can always go back: copy AUTOEXEC.DVX over AUTOEXEC.BAT. If the line is already there, nothing is changed.
-
-Your mouse driver must be started before DVX. If AUTOEXEC.BAT does not start it yet, add it above the DVX line with any text editor.
-
-The box is not shown when the computer does not report which drive it started from.
-
-.h2 Problems
-
-.list
-.item "There is not enough free space": choose a folder on another disk, or make room on this one, and click Install again.
-.item "Choose a folder that is not this one": Setup cannot copy DVX into the folder it is running from, or into a folder inside it.
-.item A file cannot be copied: Setup says which one and why. Click Retry to try again (for example after putting the disk back in), or Cancel to stop.
-.endlist
-
-.h2 Menus
-
-.table
- Menu Command What it does
- ---- ------- ------------
- Help Contents Open this help.
- Help About Setup Show the program name and copyright.
-.endtable
diff --git a/src/apps/kpunch/setup/setup.res b/src/apps/kpunch/setup/setup.res
deleted file mode 100644
index 52b7030b..00000000
--- a/src/apps/kpunch/setup/setup.res
+++ /dev/null
@@ -1,7 +0,0 @@
-# setup.res -- Resource manifest for Setup
-icon32 icon icon32.bmp
-name text "Setup"
-author text "Scott Duensing"
-copyright text "Copyright 2026 Scott Duensing"
-publisher text "Kangaroo Punch Studios"
-description text "Copies DVX to a folder on your hard disk"
diff --git a/src/apps/kpunch/setup/setupCore.c b/src/apps/kpunch/setup/setupCore.c
deleted file mode 100644
index f801127e..00000000
--- a/src/apps/kpunch/setup/setupCore.c
+++ /dev/null
@@ -1,278 +0,0 @@
-// The MIT License (MIT)
-//
-// Copyright (C) 2026 Scott Duensing
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to
-// deal in the Software without restriction, including without limitation the
-// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-// sell copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-// IN THE SOFTWARE.
-
-// setupCore.c -- Setup's work without its window (see setupCore.h)
-
-#include "setupCore.h"
-#include "dvxPlat.h"
-#include "dvxPrefs.h"
-#include "stb_ds_wrap.h"
-
-#include
-#include
-#include
-#include
-#include
-#include "dvxMem.h"
-
-// Line ending written into AUTOEXEC.BAT
-#define SETUP_EOL "\r\n"
-
-// ============================================================
-// Prototypes
-// ============================================================
-
-bool setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize);
-bool setupAutoexecHasLine(const char *path, const char *line);
-bool setupAutoexecLine(const char *destDir, char *out, int32_t outSize);
-bool setupAutoexecPath(char bootDrive, char *out, int32_t outSize);
-bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
-bool setupIsInstalled(const char *destDir);
-FileOpsJobT *setupJobCreate(const char *srcDir, const char *destDir);
-FileOpsAnswerE setupOverwriteAnswer(const char *srcDir, const FileOpsJobT *job);
-static bool setupSkip(const char *name);
-bool setupSpaceNeeded(const char *srcDir, uint64_t *bytes);
-bool setupWriteIni(const char *destDir, int32_t width, int32_t height, int32_t bpp);
-static bool writeAll(FILE *f, const char *data, int32_t len);
-
-
-// The copy goes to a scratch file first (platformReplaceFile swaps it in),
-// so a full disk never leaves a cut-off AUTOEXEC.BAT.
-bool setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize) {
- char bak[DVX_MAX_PATH];
- char tmp[DVX_MAX_PATH];
- char swap[DVX_MAX_PATH];
- int32_t len = 0;
-
- if (!platformPathSibling(bak, sizeof(bak), path, SETUP_AUTOEXEC_BAK) || !platformPathSibling(tmp, sizeof(tmp), path, PLATFORM_TMP_EXT) || !platformPathSibling(swap, sizeof(swap), path, PLATFORM_BAK_EXT)) {
- snprintf(err, errSize, "The path %s is too long.", path);
- return false;
- }
-
- char *old = platformReadFile(path, &len);
-
- if (old && !platformCopyFile(path, bak)) {
- snprintf(err, errSize, "Could not make the backup copy %s, so %s was not changed.", bak, path);
- free(old);
- return false;
- }
-
- FILE *f = fopen(tmp, "wb");
- bool ok = f != NULL;
-
- if (ok && old) {
- ok = writeAll(f, old, len);
-
- // A last line without its line break would run into the new one.
- if (ok && len > 0 && old[len - 1] != '\n') {
- ok = writeAll(f, SETUP_EOL, (int32_t)strlen(SETUP_EOL));
- }
- }
-
- ok = ok && writeAll(f, line, (int32_t)strlen(line)) && writeAll(f, SETUP_EOL, (int32_t)strlen(SETUP_EOL));
-
- if (f && fclose(f) != 0) {
- ok = false;
- }
-
- free(old);
-
- if (!ok) {
- remove(tmp);
- snprintf(err, errSize, "Could not write %s (is the disk full or write-protected?).", path);
- return false;
- }
-
- if (!platformReplaceFile(tmp, path, swap)) {
- snprintf(err, errSize, "Could not replace %s.", path);
- return false;
- }
-
- return true;
-}
-
-
-bool setupAutoexecHasLine(const char *path, const char *line) {
- char *text = platformReadFile(path, NULL);
- bool has = false;
-
- if (!text) {
- return false;
- }
-
- for (char *l = text; l && !has; ) {
- char *next = strchr(l, '\n');
-
- if (next) {
- *next++ = '\0';
- }
-
- dvxTrimRight(l);
- has = strcasecmp(dvxSkipWs(l), line) == 0;
- l = next;
- }
-
- free(text);
- return has;
-}
-
-
-bool setupAutoexecLine(const char *destDir, char *out, int32_t outSize) {
- return fileOpsPathJoin(out, outSize, destDir, SETUP_EXE_NAME);
-}
-
-
-bool setupAutoexecPath(char bootDrive, char *out, int32_t outSize) {
- if (!bootDrive) {
- return false;
- }
-
- int32_t n = snprintf(out, outSize, "%c:" DVX_PATH_SEP SETUP_AUTOEXEC_NAME, bootDrive);
-
- return n > 0 && n < outSize;
-}
-
-
-bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize) {
- if (!destDir[0]) {
- snprintf(err, errSize, "Type the folder to copy DVX to.");
- return false;
- }
-
- if (!platformPathIsAbsolute(destDir)) {
- snprintf(err, errSize, "Type the whole path of the folder, starting with the drive, for example C:\\DVX.");
- return false;
- }
-
- if (fileOpsPathIsInside(destDir, srcDir)) {
- snprintf(err, errSize, "DVX is running from %s. Choose a folder that is not this one and not inside it.", srcDir);
- return false;
- }
-
- return true;
-}
-
-
-bool setupIsInstalled(const char *destDir) {
- char path[DVX_MAX_PATH];
- struct stat st;
-
- return fileOpsPathJoin(path, sizeof(path), destDir, DVX_INI_PATH) && stat(path, &st) == 0 && S_ISREG(st.st_mode);
-}
-
-
-FileOpsJobT *setupJobCreate(const char *srcDir, const char *destDir) {
- char **names = dvxReadDir(srcDir);
-
- if (!names) {
- return NULL;
- }
-
- FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, destDir);
-
- for (int32_t i = 0; job && i < (int32_t)arrlen(names); i++) {
- char path[DVX_MAX_PATH];
-
- if (!setupSkip(names[i]) && fileOpsPathJoin(path, sizeof(path), srcDir, names[i])) {
- fileOpsJobAddSource(job, path);
- }
- }
-
- if (job) {
- fileOpsJobClearAttr(job, FILEOPS_ATTR_READONLY);
- }
-
- dvxReadDirFree(names);
- return job;
-}
-
-
-FileOpsAnswerE setupOverwriteAnswer(const char *srcDir, const FileOpsJobT *job) {
- char configDir[DVX_MAX_PATH];
- FileOpsProgressT p;
-
- fileOpsJobProgress(job, &p);
-
- if (fileOpsPathJoin(configDir, sizeof(configDir), srcDir, DVX_CONFIG_DIR) && fileOpsPathIsInside(p.current, configDir)) {
- return FileOpsAnswerNoE;
- }
-
- return FileOpsAnswerYesE;
-}
-
-
-// The log is being written while Setup runs and belongs to this copy only.
-static bool setupSkip(const char *name) {
- return strcasecmp(name, DVX_LOG_FILE) == 0;
-}
-
-
-bool setupSpaceNeeded(const char *srcDir, uint64_t *bytes) {
- char **names = dvxReadDir(srcDir);
- bool ok = names != NULL;
-
- *bytes = 0;
-
- for (int32_t i = 0; ok && i < (int32_t)arrlen(names); i++) {
- char path[DVX_MAX_PATH];
- uint64_t itemBytes = 0;
- int32_t files = 0;
- int32_t folders = 0;
-
- if (setupSkip(names[i])) {
- continue;
- }
-
- ok = fileOpsPathJoin(path, sizeof(path), srcDir, names[i]) && fileOpsTreeSize(path, &itemBytes, &files, &folders);
- *bytes += itemBytes;
- }
-
- dvxReadDirFree(names);
- return ok;
-}
-
-
-bool setupWriteIni(const char *destDir, int32_t width, int32_t height, int32_t bpp) {
- char path[DVX_MAX_PATH];
-
- if (!fileOpsPathJoin(path, sizeof(path), destDir, DVX_INI_PATH)) {
- return false;
- }
-
- PrefsHandleT *ini = prefsLoad(path);
-
- if (!ini) {
- return false;
- }
-
- prefsSetVideoMode(ini, width, height, bpp);
-
- bool ok = prefsSave(ini);
-
- prefsClose(ini);
- return ok;
-}
-
-
-static bool writeAll(FILE *f, const char *data, int32_t len) {
- return (int32_t)fwrite(data, 1, (size_t)len, f) == len;
-}
diff --git a/src/apps/kpunch/setup/setupCore.h b/src/apps/kpunch/setup/setupCore.h
deleted file mode 100644
index bbaad7a6..00000000
--- a/src/apps/kpunch/setup/setupCore.h
+++ /dev/null
@@ -1,96 +0,0 @@
-// The MIT License (MIT)
-//
-// Copyright (C) 2026 Scott Duensing
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to
-// deal in the Software without restriction, including without limitation the
-// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-// sell copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
-// IN THE SOFTWARE.
-
-// setupCore.h -- Setup's work without its window: checking the target
-// folder, the copy job, DVX.INI and AUTOEXEC.BAT (host tested)
-//
-// Setup copies everything in the folder DVX runs from (the working
-// directory; a CD-ROM, a floppy or any folder) except the log into a
-// target folder, as a fileops copy job that clears the read-only bit a
-// CD-ROM puts on every file. On a folder that already holds DVX (its
-// CONFIG\DVX.INI exists) the files under CONFIG are kept, so the user's
-// settings, groups and file types survive; everything else is replaced.
-// A new install gets the copied DVX.INI with the display mode Setup ran in
-// (known to work on this computer).
-
-#ifndef SETUP_CORE_H
-#define SETUP_CORE_H
-
-#include "fileops/fileOps.h"
-
-#include
-#include
-
-// The program AUTOEXEC.BAT starts, in the target folder
-#define SETUP_EXE_NAME "DVX.EXE"
-
-// AUTOEXEC.BAT in the root of the boot drive, and the copy of it Setup
-// keeps before changing it (AUTOEXEC.DVX)
-#define SETUP_AUTOEXEC_NAME "AUTOEXEC.BAT"
-#define SETUP_AUTOEXEC_BAK ".DVX"
-
-// Longest message the functions below return
-#define SETUP_ERR_MAX 320
-
-// Add line (with a CR LF) to the end of the batch file at path, after
-// copying it to its SETUP_AUTOEXEC_BAK sibling. A missing file is
-// created (nothing to back up). The change is written to a scratch file
-// and swapped in, so a failure leaves the file as it was. False with a
-// reason in err.
-bool setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize);
-
-// True when the batch file at path already has line (blanks around it and
-// case ignored).
-bool setupAutoexecHasLine(const char *path, const char *line);
-
-// The AUTOEXEC.BAT line that starts DVX from destDir: destDir\DVX.EXE (the
-// loader changes to its own folder first). False when it does not fit.
-bool setupAutoexecLine(const char *destDir, char *out, int32_t outSize);
-
-// Path of AUTOEXEC.BAT on the boot drive ("C:\AUTOEXEC.BAT"). False when
-// there is no boot drive (0).
-bool setupAutoexecPath(char bootDrive, char *out, int32_t outSize);
-
-// Check a target folder: given, a full path, and neither the source
-// folder nor inside it. False with a reason in err.
-bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
-
-// True when destDir already holds a DVX with settings (CONFIG\DVX.INI).
-bool setupIsInstalled(const char *destDir);
-
-// The copy job: every item of srcDir except the log, into destDir,
-// clearing read-only. NULL when out of memory or srcDir cannot be read.
-FileOpsJobT *setupJobCreate(const char *srcDir, const char *destDir);
-
-// Answer to the job's "file exists" question: keep (No) what is under
-// srcDir's CONFIG folder, replace (Yes) everything else.
-FileOpsAnswerE setupOverwriteAnswer(const char *srcDir, const FileOpsJobT *job);
-
-// Bytes the copy needs (the items setupJobCreate copies). False when
-// something could not be read (bytes then covers what was read).
-bool setupSpaceNeeded(const char *srcDir, uint64_t *bytes);
-
-// Store the display mode in destDir's CONFIG\DVX.INI. False when it
-// cannot be saved.
-bool setupWriteIni(const char *destDir, int32_t width, int32_t height, int32_t bpp);
-
-#endif // SETUP_CORE_H
diff --git a/src/common.mk b/src/common.mk
index 55780425..e263d80a 100644
--- a/src/common.mk
+++ b/src/common.mk
@@ -48,6 +48,9 @@
# HOST_CFLAGS native test-harness objects (libdvx.a, widget .so,
# suites): sanitizers per SAN, gcov per COV
# HOST_LDFLAGS link flags matching HOST_CFLAGS
+# HOST_APP_LDFLAGS extra link flags of every host app shared object:
+# exit() and abort() become platformAppExit /
+# platformAppAbort, as the DOS export table maps them
# HOST_TOOL_CFLAGS native release tools in bin/host (dvxres, dvxhlpc,
# bascomp, ...)
#
@@ -103,4 +106,17 @@ endif
HOST_OBJROOT = $(DVX_ROOT)/obj/$(HOST_SUB)
HOST_CFLAGS = -O1 -g -fPIC $(WARNFLAGS) -D_GNU_SOURCE -DDVX_HOST_TEST $(HOST_INSTFLAGS) $(DEPFLAGS)
HOST_LDFLAGS = $(HOST_INSTFLAGS)
+HOST_APP_LDFLAGS = -Wl,--wrap=exit -Wl,--wrap=abort
HOST_TOOL_CFLAGS = -O2 $(WARNFLAGS) $(DEPFLAGS)
+
+# ---- Driver modules (dvxDrv.h) ----
+# A library with drivers in drivers// sets OBJDIR and DRVDIR and
+# writes $(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))):
+# each directory is built from all its sources into $(DRVDIR)/.drv.
+DVX_DRIVERS = $(notdir $(patsubst %/,%,$(wildcard drivers/*/)))
+
+define DVX_DRIVER_RULE
+$(DRVDIR)/$1.drv: $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/$1/*.c)) | $(DRVDIR)
+ $$(DXE3GEN) -o $(DRVDIR)/$1.dxe -U $$^
+ mv $(DRVDIR)/$1.dxe $$@
+endef
diff --git a/src/include/basic/game.bas b/src/include/basic/game.bas
new file mode 100644
index 00000000..eadcd370
--- /dev/null
+++ b/src/include/basic/game.bas
@@ -0,0 +1,61 @@
+' The MIT License (MIT)
+'
+' Copyright (C) 2026 Scott Duensing
+'
+' Permission is hereby granted, free of charge, to any person obtaining a copy
+' of this software and associated documentation files (the "Software"), to
+' deal in the Software without restriction, including without limitation the
+' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+' sell copies of the Software, and to permit persons to whom the Software is
+' furnished to do so, subject to the following conditions:
+'
+' The above copyright notice and this permission notice shall be included in
+' all copies or substantial portions of the Software.
+'
+' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+' IN THE SOFTWARE.
+
+' game.bas -- Game Helpers Library for DVX BASIC
+'
+' Tells whether a key is being held down right now, for games that move
+' something for as long as a key is held. Keys are named by their raw key
+' code: one of the constants below, or for any other key its PC keyboard
+' scan code (add &H80 for the grey keys between the typing keys and the
+' number pad). Keys only read as held while one of your program's windows
+' is active.
+'
+' Usage: Add this file to your project.
+
+' Raw key codes
+CONST GAMEKEY_ESC = &H01
+CONST GAMEKEY_ENTER = &H1C
+CONST GAMEKEY_SPACE = &H39
+CONST GAMEKEY_TAB = &H0F
+CONST GAMEKEY_LSHIFT = &H2A
+CONST GAMEKEY_RSHIFT = &H36
+CONST GAMEKEY_LCTRL = &H1D
+CONST GAMEKEY_RCTRL = &H9D
+CONST GAMEKEY_LALT = &H38
+CONST GAMEKEY_RALT = &HB8
+CONST GAMEKEY_UP = &HC8
+CONST GAMEKEY_DOWN = &HD0
+CONST GAMEKEY_LEFT = &HCB
+CONST GAMEKEY_RIGHT = &HCD
+CONST GAMEKEY_A = &H1E
+CONST GAMEKEY_D = &H20
+CONST GAMEKEY_S = &H1F
+CONST GAMEKEY_W = &H11
+CONST GAMEKEY_Z = &H2C
+CONST GAMEKEY_X = &H2D
+CONST GAMEKEY_F1 = &H3B
+
+DECLARE LIBRARY "game"
+ ' True while the key is held down and one of this program's windows
+ ' is active, else False.
+ DECLARE FUNCTION GameKeyDown(BYVAL keyCode AS LONG) AS LONG
+END DECLARE
diff --git a/src/include/basic/joystick.bas b/src/include/basic/joystick.bas
new file mode 100644
index 00000000..7c31943c
--- /dev/null
+++ b/src/include/basic/joystick.bas
@@ -0,0 +1,70 @@
+' The MIT License (MIT)
+'
+' Copyright (C) 2026 Scott Duensing
+'
+' Permission is hereby granted, free of charge, to any person obtaining a copy
+' of this software and associated documentation files (the "Software"), to
+' deal in the Software without restriction, including without limitation the
+' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+' sell copies of the Software, and to permit persons to whom the Software is
+' furnished to do so, subject to the following conditions:
+'
+' The above copyright notice and this permission notice shall be included in
+' all copies or substantial portions of the Software.
+'
+' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+' IN THE SOFTWARE.
+
+' joystick.bas -- Joystick Library for DVX BASIC
+'
+' Reads joysticks and game pads. Joysticks are numbered from 1 to
+' JoyCount(); their axes, buttons and hats are numbered from 1 too. An
+' axis reads from -32768 (all the way left or up) to 32767 (all the way
+' right or down), 0 in the middle. When no joystick is connected the
+' keyboard can stand in for one (the arrow keys and Left Ctrl, Space,
+' Left Shift and Z as buttons; the Control Panel's Game Controllers page
+' changes the keys); it only answers while one of your program's windows
+' is active. Calibrate joysticks in the Control Panel.
+'
+' Usage: Add this file to your project.
+
+' A hat (point of view switch) that is not pushed
+CONST JOY_HAT_CENTERED = -1
+
+' The ends of an axis
+CONST JOY_AXIS_MIN = -32768
+CONST JOY_AXIS_MAX = 32767
+
+DECLARE LIBRARY "joystick"
+ ' How many joysticks there are (0 when there are none).
+ DECLARE FUNCTION JoyCount() AS LONG
+
+ ' The joystick's name, e.g. "Game port joystick 1" ("" when there is
+ ' no such joystick).
+ DECLARE FUNCTION JoyName$(BYVAL joy AS LONG) AS STRING
+
+ ' How many axes / buttons the joystick has.
+ DECLARE FUNCTION JoyAxes(BYVAL joy AS LONG) AS LONG
+ DECLARE FUNCTION JoyButtons(BYVAL joy AS LONG) AS LONG
+
+ ' Left/right and up/down position, -32768 to 32767.
+ DECLARE FUNCTION JoyX(BYVAL joy AS LONG) AS LONG
+ DECLARE FUNCTION JoyY(BYVAL joy AS LONG) AS LONG
+
+ ' Any axis (1 = left/right, 2 = up/down, 3 and 4 for throttles and
+ ' rudders), -32768 to 32767.
+ DECLARE FUNCTION JoyAxis(BYVAL joy AS LONG, BYVAL axis AS LONG) AS LONG
+
+ ' True while the button is held down, and once after a quick press
+ ' that happened since the last time you asked about that button.
+ DECLARE FUNCTION JoyButton(BYVAL joy AS LONG, BYVAL button AS LONG) AS LONG
+
+ ' The direction a hat is pushed in degrees (0 = up, 90 = right), or
+ ' JOY_HAT_CENTERED.
+ DECLARE FUNCTION JoyHat(BYVAL joy AS LONG, BYVAL hat AS LONG) AS LONG
+END DECLARE
diff --git a/src/include/basic/music.bas b/src/include/basic/music.bas
new file mode 100644
index 00000000..bb18a974
--- /dev/null
+++ b/src/include/basic/music.bas
@@ -0,0 +1,105 @@
+' The MIT License (MIT)
+'
+' Copyright (C) 2026 Scott Duensing
+'
+' Permission is hereby granted, free of charge, to any person obtaining a copy
+' of this software and associated documentation files (the "Software"), to
+' deal in the Software without restriction, including without limitation the
+' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+' sell copies of the Software, and to permit persons to whom the Software is
+' furnished to do so, subject to the following conditions:
+'
+' The above copyright notice and this permission notice shall be included in
+' all copies or substantial portions of the Software.
+'
+' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+' IN THE SOFTWARE.
+
+' music.bas -- Music Library for DVX BASIC
+'
+' Plays music through whatever sound hardware the computer has. Two
+' kinds of music files:
+'
+' Tracker modules (MOD, S3M, XM and IT files) play wherever the computer
+' can play sampled sound.
+' MIDI files (.MID, type 0 and 1) play on the computer's music
+' synthesizer, or on a built-in synthesizer that sounds like it when
+' there is none.
+'
+' You can also play notes yourself, like a keyboard: open a live music
+' with MusicMidiOpen and send it notes. MIDI channels are 1 to 16 (10 is
+' the drums), notes, velocities (how hard a key is struck), instruments
+' and controller values 0 to 127 (middle C is note 60).
+'
+' MusicLoad and MusicMidiOpen return a music number for the other
+' functions, or 0 when the music could not be loaded (MusicError$ says
+' why). Several programs can play music at the same time; everything
+' your program plays stops when it ends. Volumes are percent (0 to 100),
+' times milliseconds.
+'
+' Usage: Add this file to your project.
+
+' Music number returned when a music could not be loaded
+CONST MUSIC_FAILED = 0
+
+' The drum channel
+CONST MUSIC_DRUMS = 10
+
+DECLARE LIBRARY "music"
+ ' Load a module or MIDI file. Nothing plays until MusicPlay.
+ DECLARE FUNCTION MusicLoad(BYVAL fileName AS STRING) AS LONG
+
+ ' Play from where the music is (the start after loading or stopping);
+ ' with loopIt True it starts over at the end until stopped.
+ DECLARE SUB MusicPlay(BYVAL music AS LONG, BYVAL loopIt AS LONG)
+
+ ' Stop and go back to the start.
+ DECLARE SUB MusicStop(BYVAL music AS LONG)
+
+ ' Pause, and go on from the same place.
+ DECLARE SUB MusicPause(BYVAL music AS LONG)
+ DECLARE SUB MusicResume(BYVAL music AS LONG)
+
+ ' True while the music is playing (not stopped, finished or paused).
+ DECLARE FUNCTION MusicPlaying(BYVAL music AS LONG) AS LONG
+
+ ' Set the volume of this music (percent).
+ DECLARE SUB MusicVolume(BYVAL music AS LONG, BYVAL volume AS LONG)
+
+ ' Where the music is, and how long it is (milliseconds).
+ DECLARE FUNCTION MusicPosition(BYVAL music AS LONG) AS LONG
+ DECLARE FUNCTION MusicLength(BYVAL music AS LONG) AS LONG
+
+ ' Go to a place in the music (milliseconds).
+ DECLARE SUB MusicSeek(BYVAL music AS LONG, BYVAL milliseconds AS LONG)
+
+ ' Stop the music and forget it.
+ DECLARE SUB MusicFree(BYVAL music AS LONG)
+
+ ' Why the last music function did not work.
+ DECLARE FUNCTION MusicError$() AS STRING
+
+ ' A live music for playing notes yourself (it is playing already).
+ DECLARE FUNCTION MusicMidiOpen() AS LONG
+
+ ' Start and stop a note on a channel (1 to 16).
+ DECLARE SUB MusicNoteOn(BYVAL music AS LONG, BYVAL channel AS LONG, BYVAL note AS LONG, BYVAL velocity AS LONG)
+ DECLARE SUB MusicNoteOff(BYVAL music AS LONG, BYVAL channel AS LONG, BYVAL note AS LONG)
+
+ ' Choose a channel's instrument (0 to 127, General MIDI numbering:
+ ' 0 piano, 24 guitar, 40 violin, 56 trumpet, 73 flute...).
+ DECLARE SUB MusicProgram(BYVAL music AS LONG, BYVAL channel AS LONG, BYVAL program AS LONG)
+
+ ' Set a controller of a channel: 7 volume, 10 pan, 11 expression,
+ ' 64 sustain pedal (64 or more: down), 123 all notes off.
+ DECLARE SUB MusicControl(BYVAL music AS LONG, BYVAL channel AS LONG, BYVAL controller AS LONG, BYVAL value AS LONG)
+
+ ' Bend the pitch of a channel's notes: -8192 to 8191 (0 = no bend;
+ ' the full range is two semitones up or down).
+ DECLARE SUB MusicPitchBend(BYVAL music AS LONG, BYVAL channel AS LONG, BYVAL bend AS LONG)
+END DECLARE
diff --git a/src/include/basic/sound.bas b/src/include/basic/sound.bas
new file mode 100644
index 00000000..4381ec00
--- /dev/null
+++ b/src/include/basic/sound.bas
@@ -0,0 +1,73 @@
+' The MIT License (MIT)
+'
+' Copyright (C) 2026 Scott Duensing
+'
+' Permission is hereby granted, free of charge, to any person obtaining a copy
+' of this software and associated documentation files (the "Software"), to
+' deal in the Software without restriction, including without limitation the
+' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+' sell copies of the Software, and to permit persons to whom the Software is
+' furnished to do so, subject to the following conditions:
+'
+' The above copyright notice and this permission notice shall be included in
+' all copies or substantial portions of the Software.
+'
+' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+' IN THE SOFTWARE.
+
+' sound.bas -- Sound Library for DVX BASIC
+'
+' Plays sound files and tones through whatever sound hardware the
+' computer has (a Sound Blaster, the PC speaker, or nothing at all - your
+' program works the same either way). Several programs can play at the
+' same time; everything your program plays stops when it ends.
+'
+' Sound files: WAV (uncompressed 8- or 16-bit) and VOC. Each Play...
+' function returns a voice number for StopWave and SoundPlaying, or 0
+' when the sound could not be played (SoundError$ says why). Volumes are
+' percent (0 to 100).
+'
+' Usage: Add this file to your project.
+
+' Voice number returned when a sound could not be played
+CONST SOUND_FAILED = 0
+
+DECLARE LIBRARY "audio"
+ ' Play a WAV or VOC file once.
+ DECLARE FUNCTION PlayWave(BYVAL fileName AS STRING) AS LONG
+
+ ' Play a WAV or VOC file over and over until StopWave.
+ DECLARE FUNCTION PlayWaveLoop(BYVAL fileName AS STRING) AS LONG
+
+ ' Stop a sound started by PlayWave, PlayWaveLoop, PlayTone or PlayNoise.
+ DECLARE SUB StopWave(BYVAL voice AS LONG)
+
+ ' Stop every sound this program is playing.
+ DECLARE SUB StopAllSounds()
+
+ ' True while the sound is still playing, else False.
+ DECLARE FUNCTION SoundPlaying(BYVAL voice AS LONG) AS LONG
+
+ ' Play a tone of frequency hertz (20 to 20000) for milliseconds (0: until
+ ' StopWave) at volume percent.
+ DECLARE FUNCTION PlayTone(BYVAL frequency AS LONG, BYVAL milliseconds AS LONG, BYVAL volume AS LONG) AS LONG
+
+ ' Play noise (like a drum or an explosion); higher frequencies sound
+ ' brighter.
+ DECLARE FUNCTION PlayNoise(BYVAL frequency AS LONG, BYVAL milliseconds AS LONG, BYVAL volume AS LONG) AS LONG
+
+ ' Set the volume of everything this program plays (percent).
+ DECLARE SUB SoundVolume(BYVAL volume AS LONG)
+
+ ' A description of the sound device, like "Sound Blaster 16 (DSP 4.05)
+ ' at 220h, 22050 Hz, 16-bit stereo", or "No sound device".
+ DECLARE FUNCTION SoundInfo$() AS STRING
+
+ ' Why the last sound could not be played.
+ DECLARE FUNCTION SoundError$() AS STRING
+END DECLARE
diff --git a/src/installer/Makefile b/src/installer/Makefile
new file mode 100644
index 00000000..db40b6e0
--- /dev/null
+++ b/src/installer/Makefile
@@ -0,0 +1,137 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# The DVX installer.
+#
+# bin/setup.exe SETUP.EXE: one static DOS program -- the DVX core,
+# the widgets Setup uses, fileops, miniz's inflate and
+# the Setup program -- linked with --gc-sections so
+# only what Setup reaches is in it. Also shipped in
+# the package (the installed copy uninstalls).
+# bin/host/dvxpkg the package tool `make dist` runs (src/installer/
+# mkDist.sh).
+#
+# The core sources are taken as they are (every libdvx/*.c and the DOS
+# platform files, into an archive so only the objects used are linked);
+# the widgets are compiled with their wgtRegister renamed to
+# wgtRegister, which setup.c calls.
+
+include ../common.mk
+
+LIBDVX = ../libs/kpunch/libdvx
+KPUNCH = ../libs/kpunch
+WIDGETS = ../widgets/kpunch
+MINIZ = thirdparty/miniz
+LICENSE = ../../LICENSE.txt
+
+OBJDIR = ../../obj/installer
+DOSOBJ = $(OBJDIR)/dos
+HOSTOBJ = $(OBJDIR)/host
+BINDIR = ../../bin
+HOSTBIN = ../../bin/host
+TARGET = $(BINDIR)/setup.exe
+TOOL = $(HOSTBIN)/dvxpkg
+LICENSE_H = $(OBJDIR)/setupLicense.h
+
+INCLUDES = -I. -I$(MINIZ) -I$(OBJDIR) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I$(KPUNCH) -I$(KPUNCH)/libtasks -I$(WIDGETS)
+MINIZ_DOS = -DMINIZ_NO_ARCHIVE_APIS -DMINIZ_NO_STDIO -DMINIZ_NO_TIME -DMINIZ_NO_DEFLATE_APIS -DMINIZ_NO_ZLIB_COMPATIBLE_NAMES
+CFLAGS = $(DOS_CFLAGS) -ffunction-sections -fdata-sections $(INCLUDES) $(MINIZ_DOS)
+LDFLAGS = -s -Wl,--gc-sections -lm
+
+# Setup's own sources; the core, fileops and text help as they are
+SETUP_SRCS = setup.c setupCore.c pkgRead.c pkgFormat.c $(MINIZ)/miniz.c
+CORE_SRCS = $(wildcard $(LIBDVX)/*.c) $(LIBDVX)/platform/dvxPlatformDos.c $(LIBDVX)/platform/dvxPlatformIrq.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/platform/dvxMemTrack.c $(LIBDVX)/thirdparty/stb_ds_impl.c ../tools/dvxResWrite.c $(wildcard $(KPUNCH)/fileops/*.c) $(KPUNCH)/texthelp/textHelp.c
+
+# The widgets Setup uses: /: (wgtRegister -> wgtRegister)
+SETUP_WGTS = box/widgetBox:Box button/widgetButton:Button checkbox/widgetCheckbox:Checkbox label/widgetLabel:Label progressBar/widgetProgressBar:Progress textInput/widgetTextInput:TextInput
+
+SETUP_OBJS = $(patsubst %.c,$(DOSOBJ)/%.o,$(notdir $(SETUP_SRCS)))
+CORE_OBJS = $(patsubst %.c,$(DOSOBJ)/core/%.o,$(notdir $(CORE_SRCS)))
+WGT_OBJS = $(foreach w,$(SETUP_WGTS),$(DOSOBJ)/wgt/$(notdir $(firstword $(subst :, ,$w))).o)
+CORE_LIB = $(DOSOBJ)/setupCore.a
+
+# The package tool (host)
+TOOL_SRCS = pkgTool.c pkgBuild.c pkgFormat.c $(MINIZ)/miniz.c
+TOOL_OBJS = $(patsubst %.c,$(HOSTOBJ)/%.o,$(notdir $(TOOL_SRCS)))
+TOOL_FLAGS = $(HOST_TOOL_CFLAGS) -I. -I$(MINIZ)
+
+vpath %.c . $(MINIZ)
+
+.PHONY: all clean
+
+all: $(TARGET) $(TOOL)
+
+$(TARGET): $(SETUP_OBJS) $(CORE_LIB) | $(BINDIR)
+ $(CC) -o $@ $(SETUP_OBJS) $(CORE_LIB) $(LDFLAGS)
+ $(EXE2COFF) $@
+ cat $(CWSDSTUB) $(BINDIR)/setup > $@
+ rm -f $(BINDIR)/setup
+
+$(CORE_LIB): $(CORE_OBJS) $(WGT_OBJS)
+ rm -f $@
+ $(DJGPP_PREFIX)/bin/$(DJGPP_TRIPLE)-ar rcs $@ $^
+
+$(DOSOBJ)/%.o: %.c $(DVX_MAKEFILES) | $(LICENSE_H)
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+define CORE_RULE
+$(DOSOBJ)/core/$(notdir $(1:.c=.o)): $1 $$(DVX_MAKEFILES)
+ @mkdir -p $$(dir $$@)
+ $$(CC) $$(CFLAGS) -c -o $$@ $$<
+endef
+
+$(foreach s,$(CORE_SRCS),$(eval $(call CORE_RULE,$s)))
+
+define WGT_RULE
+$(DOSOBJ)/wgt/$(notdir $(firstword $(subst :, ,$1))).o: $(WIDGETS)/$(firstword $(subst :, ,$1)).c $$(DVX_MAKEFILES)
+ @mkdir -p $$(dir $$@)
+ $$(CC) $$(CFLAGS) -DwgtRegister=wgtRegister$(lastword $(subst :, ,$1)) -c -o $$@ $$<
+endef
+
+$(foreach w,$(SETUP_WGTS),$(eval $(call WGT_RULE,$w)))
+
+# LICENSE.txt as a C string, in ASCII (the copyright sign becomes (C)),
+# its paragraphs re-flowed to the width of Setup's text box.
+LICENSE_COLS = 58
+
+$(LICENSE_H): $(LICENSE) $(DVX_MAKEFILES)
+ @mkdir -p $(dir $@)
+ { echo '// Generated from LICENSE.txt by src/installer/Makefile'; \
+ echo 'static const char sSetupLicense[] ='; \
+ tr -d '\r' < $< | iconv -f UTF-8 -t ASCII//TRANSLIT | fmt -w $(LICENSE_COLS) | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/^/ "/' -e 's/$$/\\n"/'; \
+ echo ';'; } > $@.tmp && mv $@.tmp $@
+
+$(TOOL): $(TOOL_OBJS) | $(HOSTBIN)
+ $(HOSTCC) -o $@ $(TOOL_OBJS)
+
+$(HOSTOBJ)/%.o: %.c $(DVX_MAKEFILES)
+ @mkdir -p $(dir $@)
+ $(HOSTCC) $(TOOL_FLAGS) -c -o $@ $<
+
+$(BINDIR) $(HOSTBIN):
+ mkdir -p $@
+
+-include $(SETUP_OBJS:.o=.d) $(CORE_OBJS:.o=.d) $(WGT_OBJS:.o=.d) $(TOOL_OBJS:.o=.d)
+
+clean:
+ rm -rf $(OBJDIR) $(TARGET) $(TOOL)
diff --git a/src/installer/README.TXT b/src/installer/README.TXT
new file mode 100644
index 00000000..fca02e79
--- /dev/null
+++ b/src/installer/README.TXT
@@ -0,0 +1,77 @@
+DVX
+===
+
+DVX is a graphical desktop for DOS: windows, a Program Manager, a File
+Manager, a Notepad, a Terminal, DVX BASIC and more.
+
+
+What you need
+-------------
+
+ * DOS on a 386 or newer (486 recommended, Pentium for the best speed)
+ * A video card with VESA BIOS Extensions 2.0 or newer (linear frame
+ buffer); a VESA driver such as UNIVBE works with older cards
+ * 4 MB of extended memory (8 MB or more recommended)
+ * A two-button mouse and its DOS driver
+ * About 10 MB of free space on the hard disk
+
+
+Installing
+----------
+
+From the CD: change to the CD drive and type SETUP, for example
+
+ D:
+ SETUP
+
+From floppy disks: put Disk 1 in drive A: and type
+
+ A:
+ SETUP
+
+Setup shows the licence, asks for the folder to install DVX in (C:\DVX
+unless you change it), checks the free space and unpacks DVX. With
+floppies it asks for each disk in turn. Setup can also add a line to
+AUTOEXEC.BAT so DVX starts with the computer; it shows the line first and
+keeps a copy of the old file as AUTOEXEC.DVX.
+
+Setup never writes to the CD or the floppy disks.
+
+
+Upgrading or repairing
+----------------------
+
+Run Setup again and choose the same folder. Setup asks whether to keep
+your settings (the CONFIG folder); say Yes to upgrade or repair DVX and
+keep them.
+
+
+Removing DVX
+------------
+
+Run SETUP.EXE in the DVX folder (for example C:\DVX\SETUP) and choose
+Uninstall. Setup removes the files it installed and, if you ask, your
+settings; files you added yourself stay. It can also take the DVX line
+out of AUTOEXEC.BAT.
+
+
+Starting DVX
+------------
+
+Type C:\DVX\DVX (or the folder you chose). If DVX cannot use the
+display mode in its settings, hold down Shift while it starts, or type
+DVX /SAFE, to start it in 640x480.
+
+
+Unattended install
+------------------
+
+ SETUP /AUTO=C:\DVX [/LOG=C:\SETUP.LOG]
+
+installs without asking (the licence is taken as accepted, settings are
+kept on an upgrade, AUTOEXEC.BAT is not changed). The exit code is 0
+when DVX was installed.
+
+
+DVX is free software under the MIT licence (see the licence in Setup).
+Copyright 2026 Scott Duensing.
diff --git a/src/installer/mkDist.sh b/src/installer/mkDist.sh
new file mode 100755
index 00000000..d62c0602
--- /dev/null
+++ b/src/installer/mkDist.sh
@@ -0,0 +1,153 @@
+#!/bin/bash
+
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# mkDist.sh [FLOPPY] -- the release media (`make dist FLOPPY=...`)
+#
+# From a built bin/ (make all):
+# dist/dvx.zip the package (bin/ without the build machine's
+# files: host tools, tests, SDK, map, logs)
+# dist/cd/SETUP.EXE SETUP.EXE with the package appended
+# dist/cd/README.TXT
+# dist/DVX.ISO ISO 9660 image of dist/cd
+# dist/floppy/DISKnn.IMG floppy images: Disk 1 = SETUP.EXE, README.TXT
+# and DVX.P01; Disk n = DVX.P0n
+#
+# FLOPPY: 360K, 720K, 1.2M, 1.44M (default), 2.88M -- images of that
+# format -- or a number of bytes: the room on each disk, and folders
+# dist/floppy/DISKnn/ instead of images. Fails when SETUP.EXE and
+# README.TXT do not fit on Disk 1.
+#
+# DVX_DIST_BIN / DVX_DIST_OUT replace bin/ and dist/ (the DOS test
+# harness builds from its bin snapshot into its run folder).
+#
+# Needs mtools (mformat, mcopy, mdir) and mkisofs on the build machine
+# (the DOS test harness already needs mtools; mkcd.sh mkisofs).
+
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+BIN="${DVX_DIST_BIN:-${ROOT}/bin}"
+DIST="${DVX_DIST_OUT:-${ROOT}/dist}"
+STAGE="${DIST}/stage"
+TOOL="${BIN}/host/dvxpkg"
+SETUP="${BIN}/setup.exe"
+README="${ROOT}/src/installer/README.TXT"
+FLOPPY="${1:-1.44M}"
+
+export MTOOLS_SKIP_CHECK=1
+
+# What in bin/ is not part of an installed DVX
+EXCLUDES=(--exclude=./host --exclude=./test --exclude=./sdk --exclude='*.map' --exclude='*.LOG' --exclude='*.log')
+
+# Floppy formats: name -> mformat -f size (KB)
+declare -A FORMATS=([360K]=360 [720K]=720 [1.2M]=1200 [1.44M]=1440 [2.88M]=2880)
+
+fail() {
+ echo "mkDist: $*" >&2
+ exit 1
+}
+
+
+# freeBytes IMAGE -- the free bytes mtools reports for a disk image
+freeBytes() {
+ mdir -i "$1" :: 2>/dev/null | sed -n 's/^ *\([0-9 ]*\) bytes free.*/\1/p' | tr -d ' '
+}
+
+
+# format IMAGE KB LABEL
+format() {
+ rm -f "$1"
+ mformat -C -i "$1" -f "$2" -v "$3" ::
+}
+
+
+for f in "${TOOL}" "${SETUP}"; do
+ [ -x "${f}" ] || [ -f "${f}" ] || fail "${f} is missing (run make first)"
+done
+
+rm -rf "${DIST}"
+mkdir -p "${STAGE}" "${DIST}/cd" "${DIST}/floppy" "${DIST}/parts"
+
+tar -C "${BIN}" "${EXCLUDES[@]}" -cf - . | tar -C "${STAGE}" -xf -
+"${TOOL}" zip "${STAGE}" "${DIST}/dvx.zip"
+sed 's/$/\r/' "${README}" > "${DIST}/README.TXT"
+
+SETUP_SIZE=$(stat -c %s "${SETUP}")
+README_SIZE=$(stat -c %s "${DIST}/README.TXT")
+
+# CD
+"${TOOL}" cd "${SETUP}" "${DIST}/dvx.zip" "${DIST}/cd/SETUP.EXE"
+cp "${DIST}/README.TXT" "${DIST}/cd/README.TXT"
+mkisofs -quiet -iso-level 1 -V DVX -o "${DIST}/DVX.ISO" "${DIST}/cd"
+
+# Floppies
+if [ -n "${FORMATS[${FLOPPY}]:-}" ]; then
+ KB="${FORMATS[${FLOPPY}]}"
+ format "${DIST}/floppy/DISK01.IMG" "${KB}" DVX1
+ ROOM=$(freeBytes "${DIST}/floppy/DISK01.IMG")
+
+ if ! mcopy -i "${DIST}/floppy/DISK01.IMG" "${SETUP}" ::SETUP.EXE 2>/dev/null || ! mcopy -i "${DIST}/floppy/DISK01.IMG" "${DIST}/README.TXT" ::README.TXT 2>/dev/null; then
+ fail "SETUP.EXE (${SETUP_SIZE} bytes) and README.TXT (${README_SIZE} bytes) do not fit on a ${FLOPPY} disk (${ROOM} bytes free when empty). A smaller disk needs a bootstrap Setup (claudeDocs/installer.md)."
+ fi
+
+ ROOM1=$(freeBytes "${DIST}/floppy/DISK01.IMG")
+elif [[ "${FLOPPY}" =~ ^[0-9]+$ ]]; then
+ ROOM="${FLOPPY}"
+ ROOM1=$((FLOPPY - SETUP_SIZE - README_SIZE))
+
+ if [ "${ROOM1}" -le 0 ]; then
+ fail "SETUP.EXE (${SETUP_SIZE} bytes) and README.TXT (${README_SIZE} bytes) do not fit in ${FLOPPY} bytes."
+ fi
+else
+ fail "FLOPPY=${FLOPPY}: use 360K, 720K, 1.2M, 1.44M, 2.88M or a number of bytes"
+fi
+
+DISKS=$("${TOOL}" split "${DIST}/dvx.zip" "${DIST}/parts" "${ROOM1}" "${ROOM}") || fail "the package could not be split for ${FLOPPY} disks"
+
+for ((n = 1; n <= DISKS; n++)); do
+ part=$(printf 'DVX.P%02d' "${n}")
+ disk=$(printf 'DISK%02d' "${n}")
+
+ if [ -n "${KB:-}" ]; then
+ if [ "${n}" -gt 1 ]; then
+ format "${DIST}/floppy/${disk}.IMG" "${KB}" "DVX${n}"
+ fi
+
+ mcopy -i "${DIST}/floppy/${disk}.IMG" "${DIST}/parts/${part}" "::${part}" || fail "${part} does not fit on ${disk}.IMG"
+ else
+ mkdir -p "${DIST}/floppy/${disk}"
+ cp "${DIST}/parts/${part}" "${DIST}/floppy/${disk}/"
+
+ if [ "${n}" -eq 1 ]; then
+ cp "${SETUP}" "${DIST}/floppy/${disk}/SETUP.EXE"
+ cp "${DIST}/README.TXT" "${DIST}/floppy/${disk}/"
+ fi
+ fi
+done
+
+rm -rf "${STAGE}"
+
+echo "mkDist: SETUP.EXE ${SETUP_SIZE} bytes; package $(stat -c %s "${DIST}/dvx.zip") bytes"
+echo "mkDist: CD: ${DIST}/DVX.ISO (SETUP.EXE $(stat -c %s "${DIST}/cd/SETUP.EXE") bytes with the package)"
+echo "mkDist: floppies (${FLOPPY}): ${DISKS} disk(s) in ${DIST}/floppy, ${ROOM1} bytes of the package on Disk 1"
diff --git a/src/installer/pkgBuild.c b/src/installer/pkgBuild.c
new file mode 100644
index 00000000..0b3dbb4f
--- /dev/null
+++ b/src/installer/pkgBuild.c
@@ -0,0 +1,378 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// pkgBuild.c -- building the DVX package (see pkgBuild.h)
+
+#include "pkgBuild.h"
+#include "miniz.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#define PATH_MAX_LEN 1024
+#define LIST_GROW 64
+
+// Names found under the package folder, relative, '/'-separated; folders
+// end with '/'
+typedef struct {
+ char **names;
+ int32_t count;
+ int32_t cap;
+} NameListT;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int byName(const void *a, const void *b);
+static bool listAdd(NameListT *list, const char *name);
+static void listFree(NameListT *list);
+static bool loadFile(const char *path, uint8_t **data, uint32_t *size);
+bool pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize);
+bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize);
+int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize);
+bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t errSize);
+static bool scanFolder(const char *root, const char *rel, NameListT *list, char *err, int32_t errSize);
+static bool writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen);
+
+
+static int byName(const void *a, const void *b) {
+ return strcmp(*(char *const *)a, *(char *const *)b);
+}
+
+
+static bool listAdd(NameListT *list, const char *name) {
+ if (list->count == list->cap) {
+ int32_t cap = list->cap + LIST_GROW;
+ char **names = (char **)realloc(list->names, (size_t)cap * sizeof(char *));
+
+ if (!names) {
+ return false;
+ }
+
+ list->names = names;
+ list->cap = cap;
+ }
+
+ list->names[list->count] = strdup(name);
+
+ if (!list->names[list->count]) {
+ return false;
+ }
+
+ list->count++;
+ return true;
+}
+
+
+static void listFree(NameListT *list) {
+ for (int32_t i = 0; i < list->count; i++) {
+ free(list->names[i]);
+ }
+
+ free(list->names);
+ memset(list, 0, sizeof(*list));
+}
+
+
+static bool loadFile(const char *path, uint8_t **data, uint32_t *size) {
+ FILE *f = fopen(path, "rb");
+
+ *data = NULL;
+ *size = 0;
+
+ if (!f) {
+ return false;
+ }
+
+ bool ok = fseek(f, 0, SEEK_END) == 0;
+ long len = ok ? ftell(f) : -1;
+
+ ok = ok && len >= 0 && fseek(f, 0, SEEK_SET) == 0;
+
+ if (ok) {
+ *data = (uint8_t *)malloc(len > 0 ? (size_t)len : 1);
+ ok = *data && fread(*data, 1, (size_t)len, f) == (size_t)len;
+ }
+
+ fclose(f);
+
+ if (!ok) {
+ free(*data);
+ *data = NULL;
+ return false;
+ }
+
+ *size = (uint32_t)len;
+ return true;
+}
+
+
+bool pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize) {
+ PkgHeaderT h;
+ uint8_t *exe = NULL;
+ uint8_t *zip = NULL;
+ uint32_t exeSize = 0;
+ uint32_t zipSize = 0;
+
+ if (!pkgBuildInfo(zipPath, &h, err, errSize)) {
+ return false;
+ }
+
+ if (!loadFile(exePath, &exe, &exeSize) || !loadFile(zipPath, &zip, &zipSize)) {
+ snprintf(err, errSize, "cannot read %s or %s", exePath, zipPath);
+ free(exe);
+ free(zip);
+ return false;
+ }
+
+ h.disk = 1;
+ h.diskCount = 1;
+ h.partOffset = 0;
+ h.partLength = zipSize;
+
+ bool ok = writePart(outPath, zip, zipSize, &h, exe, exeSize);
+
+ if (!ok) {
+ snprintf(err, errSize, "cannot write %s", outPath);
+ }
+
+ free(exe);
+ free(zip);
+ return ok;
+}
+
+
+bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize) {
+ mz_zip_archive zip;
+ uint8_t *data = NULL;
+ uint32_t size = 0;
+
+ memset(info, 0, sizeof(*info));
+ memset(&zip, 0, sizeof(zip));
+
+ if (!loadFile(zipPath, &data, &size)) {
+ snprintf(err, errSize, "cannot read %s", zipPath);
+ return false;
+ }
+
+ if (!mz_zip_reader_init_mem(&zip, data, size, 0)) {
+ snprintf(err, errSize, "%s is not a ZIP file", zipPath);
+ free(data);
+ return false;
+ }
+
+ for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); i++) {
+ mz_zip_archive_file_stat st;
+
+ if (mz_zip_reader_file_stat(&zip, i, &st) && !st.m_is_directory) {
+ info->fileCount++;
+ info->unpackedBytes += st.m_uncomp_size;
+ }
+ }
+
+ info->zipSize = size;
+ info->setId = (uint32_t)mz_crc32(MZ_CRC32_INIT, data, size);
+ mz_zip_reader_end(&zip);
+ free(data);
+ return true;
+}
+
+
+int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize) {
+ PkgHeaderT h;
+ uint8_t *zip = NULL;
+ uint32_t size = 0;
+
+ if (firstRoom <= PKG_HEADER_SIZE || diskRoom <= PKG_HEADER_SIZE) {
+ snprintf(err, errSize, "no room for a package part (disk 1: %u bytes, others: %u bytes)", (unsigned)firstRoom, (unsigned)diskRoom);
+ return -1;
+ }
+
+ if (!pkgBuildInfo(zipPath, &h, err, errSize) || !loadFile(zipPath, &zip, &size)) {
+ return -1;
+ }
+
+ uint32_t first = firstRoom - PKG_HEADER_SIZE;
+ uint32_t other = diskRoom - PKG_HEADER_SIZE;
+ uint32_t count = 1;
+
+ if (size > first) {
+ count += (size - first + other - 1) / other;
+ }
+
+ if (count > PKG_MAX_DISKS) {
+ snprintf(err, errSize, "the package needs %u disks; at most %d are possible", (unsigned)count, PKG_MAX_DISKS);
+ free(zip);
+ return -1;
+ }
+
+ h.diskCount = (uint16_t)count;
+
+ uint32_t offset = 0;
+
+ for (uint32_t disk = 1; disk <= count; disk++) {
+ char name[PKG_PART_NAME_MAX];
+ char path[PATH_MAX_LEN];
+ uint32_t room = disk == 1 ? first : other;
+ uint32_t len = size - offset < room ? size - offset : room;
+
+ h.disk = (uint16_t)disk;
+ h.partOffset = offset;
+ h.partLength = len;
+
+ if (!pkgPartName((int32_t)disk, name, sizeof(name)) || snprintf(path, sizeof(path), "%s/%s", outDir, name) >= (int)sizeof(path) || !writePart(path, zip + offset, len, &h, NULL, 0)) {
+ snprintf(err, errSize, "cannot write part %u in %s", (unsigned)disk, outDir);
+ free(zip);
+ return -1;
+ }
+
+ offset += len;
+ }
+
+ free(zip);
+ return (int32_t)count;
+}
+
+
+bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t errSize) {
+ NameListT list;
+ mz_zip_archive zip;
+ bool ok;
+
+ memset(&list, 0, sizeof(list));
+ memset(&zip, 0, sizeof(zip));
+
+ if (!scanFolder(folder, "", &list, err, errSize)) {
+ listFree(&list);
+ return false;
+ }
+
+ qsort(list.names, (size_t)list.count, sizeof(char *), byName);
+ ok = mz_zip_writer_init_file(&zip, zipPath, 0);
+
+ if (!ok) {
+ snprintf(err, errSize, "cannot create %s", zipPath);
+ }
+
+ for (int32_t i = 0; ok && i < list.count; i++) {
+ const char *name = list.names[i];
+ size_t len = strlen(name);
+ char full[PATH_MAX_LEN];
+
+ if (name[len - 1] == PKG_NAME_SEP) {
+ ok = mz_zip_writer_add_mem(&zip, name, "", 0, MZ_NO_COMPRESSION);
+ } else {
+ snprintf(full, sizeof(full), "%s/%s", folder, name);
+ ok = mz_zip_writer_add_file(&zip, name, full, NULL, 0, MZ_UBER_COMPRESSION | MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_ZIP_FLAG_ASCII_FILENAME);
+ }
+
+ if (!ok) {
+ snprintf(err, errSize, "cannot add %s: %s", name, mz_zip_get_error_string(mz_zip_get_last_error(&zip)));
+ }
+ }
+
+ if (ok && !mz_zip_writer_finalize_archive(&zip)) {
+ snprintf(err, errSize, "cannot finish %s", zipPath);
+ ok = false;
+ }
+
+ mz_zip_writer_end(&zip);
+ listFree(&list);
+
+ if (!ok) {
+ remove(zipPath);
+ }
+
+ return ok;
+}
+
+
+// Add every file and folder under root/rel (rel "" or ending in '/').
+static bool scanFolder(const char *root, const char *rel, NameListT *list, char *err, int32_t errSize) {
+ char dirPath[PATH_MAX_LEN];
+
+ snprintf(dirPath, sizeof(dirPath), "%s/%s", root, rel);
+
+ DIR *d = opendir(dirPath);
+
+ if (!d) {
+ snprintf(err, errSize, "cannot read the folder %s", dirPath);
+ return false;
+ }
+
+ bool ok = true;
+ struct dirent *e;
+
+ while (ok && (e = readdir(d)) != NULL) {
+ char name[PKG_NAME_MAX];
+ char full[PATH_MAX_LEN];
+ struct stat st;
+
+ if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) {
+ continue;
+ }
+
+ if (snprintf(name, sizeof(name), "%s%s", rel, e->d_name) >= (int)sizeof(name) || !pkgNameIsValid(name)) {
+ snprintf(err, errSize, "%s%s is not a DOS 8.3 name", rel, e->d_name);
+ ok = false;
+ break;
+ }
+
+ snprintf(full, sizeof(full), "%s/%s", root, name);
+
+ if (stat(full, &st) != 0) {
+ snprintf(err, errSize, "cannot read %s", full);
+ ok = false;
+ } else if (S_ISDIR(st.st_mode)) {
+ char sub[PKG_NAME_MAX];
+
+ snprintf(sub, sizeof(sub), "%s%c", name, PKG_NAME_SEP);
+ ok = listAdd(list, sub) && scanFolder(root, sub, list, err, errSize);
+ } else if (S_ISREG(st.st_mode)) {
+ ok = listAdd(list, name);
+ }
+ }
+
+ closedir(d);
+ return ok;
+}
+
+
+// A part: prefix (the program, for the CD), data, then the header.
+static bool writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen) {
+ uint8_t hdr[PKG_HEADER_SIZE];
+ FILE *f = fopen(path, "wb");
+
+ if (!f) {
+ return false;
+ }
+
+ pkgHeaderEncode(h, hdr);
+
+ bool ok = (prefixLen == 0 || fwrite(prefix, 1, prefixLen, f) == prefixLen) && fwrite(data, 1, len, f) == len && fwrite(hdr, 1, sizeof(hdr), f) == sizeof(hdr);
+
+ return fclose(f) == 0 && ok;
+}
diff --git a/src/installer/pkgBuild.h b/src/installer/pkgBuild.h
new file mode 100644
index 00000000..3e5e5be9
--- /dev/null
+++ b/src/installer/pkgBuild.h
@@ -0,0 +1,55 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// pkgBuild.h -- building the DVX package on the build machine (the
+// dvxpkg tool and the host tests; see pkgFormat.h)
+
+#ifndef PKG_BUILD_H
+#define PKG_BUILD_H
+
+#include "pkgFormat.h"
+
+#include
+#include
+
+// Longest message the functions below return
+#define PKG_BUILD_ERR_MAX 400
+
+// The CD form: outPath = the program at exePath, the ZIP, and a header
+// (disk 1 of 1).
+bool pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize);
+
+// What a finished ZIP holds: set ID, file count and unpacked size (the
+// part fields are left 0).
+bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize);
+
+// The floppy form: cut the ZIP into outDir/DVX.P01, DVX.P02, ... where
+// DVX.P01 takes at most firstRoom bytes (what is left on disk 1 beside
+// SETUP.EXE) and every other part at most diskRoom (headers included).
+// Returns the number of disks, or -1 with a reason in err.
+int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize);
+
+// ZIP every file and folder under folder (sorted by name, best
+// compression, sizes in the local headers). Every name must be DOS 8.3.
+bool pkgBuildZip(const char *folder, const char *zipPath, char *err, int32_t errSize);
+
+#endif // PKG_BUILD_H
diff --git a/src/installer/pkgFormat.c b/src/installer/pkgFormat.c
new file mode 100644
index 00000000..3629ce76
--- /dev/null
+++ b/src/installer/pkgFormat.c
@@ -0,0 +1,236 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// pkgFormat.c -- the part header and package names (see pkgFormat.h)
+
+#include "pkgFormat.h"
+#include "miniz.h"
+
+#include
+#include
+
+// Header field offsets
+#define HDR_VERSION 8
+#define HDR_DISK 10
+#define HDR_DISK_COUNT 12
+#define HDR_SET_ID 16
+#define HDR_PART_OFFSET 20
+#define HDR_PART_LENGTH 24
+#define HDR_ZIP_SIZE 28
+#define HDR_FILE_COUNT 32
+#define HDR_UNPACKED 36
+#define HDR_UNPACKED_HI 40
+#define HDR_CRC 44
+
+#define BYTE_BITS 8
+#define BYTE_MASK 0xFF
+#define HALF_BITS 16
+#define HALF_MASK 0xFFFFu
+#define WORD_BITS 32
+
+// The separator of local paths (DVX_PATH_SEP, without needing dvxPlat.h:
+// the package tool is built on its own)
+#ifdef __DJGPP__
+#define PKG_DIR_SEP "\\"
+#else
+#define PKG_DIR_SEP "/"
+#endif
+
+// Characters DOS takes in a file name besides letters and digits
+#define DOS_NAME_EXTRA "!#$%&'()-@^_`{}~"
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool dosPartIsValid(const char *part, int32_t len);
+uint16_t pkgGet16(const uint8_t *p);
+uint32_t pkgGet32(const uint8_t *p);
+bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h);
+void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out);
+bool pkgHeaderRead(FILE *f, PkgHeaderT *h, uint32_t *dataStart);
+bool pkgLocalPath(const char *dir, const char *relName, char *out, int32_t outSize);
+bool pkgNameIsValid(const char *name);
+bool pkgPartName(int32_t disk, char *out, int32_t outSize);
+static void put16(uint8_t *p, uint16_t v);
+static void put32(uint8_t *p, uint32_t v);
+
+
+// One name part: base of 1-8 characters, optional '.' and 1-3 more.
+static bool dosPartIsValid(const char *part, int32_t len) {
+ int32_t base = -1;
+
+ for (int32_t i = 0; i < len; i++) {
+ char c = part[i];
+
+ if (c == '.') {
+ if (base >= 0) {
+ return false;
+ }
+
+ base = i;
+ } else if (!isalnum((unsigned char)c) && !strchr(DOS_NAME_EXTRA, c)) {
+ return false;
+ }
+ }
+
+ if (base < 0) {
+ return len > 0 && len <= PKG_DOS_BASE_MAX;
+ }
+
+ return base > 0 && base <= PKG_DOS_BASE_MAX && len - base - 1 > 0 && len - base - 1 <= PKG_DOS_EXT_MAX;
+}
+
+
+uint16_t pkgGet16(const uint8_t *p) {
+ return (uint16_t)(p[0] | (p[1] << BYTE_BITS));
+}
+
+
+uint32_t pkgGet32(const uint8_t *p) {
+ return (uint32_t)pkgGet16(p) | ((uint32_t)pkgGet16(p + 2) << HALF_BITS);
+}
+
+
+bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) {
+ if (memcmp(in, PKG_MAGIC, PKG_MAGIC_SIZE) != 0 || pkgGet16(in + HDR_VERSION) != PKG_FORMAT_VERSION) {
+ return false;
+ }
+
+ if ((uint32_t)mz_crc32(MZ_CRC32_INIT, in, HDR_CRC) != pkgGet32(in + HDR_CRC)) {
+ return false;
+ }
+
+ h->disk = pkgGet16(in + HDR_DISK);
+ h->diskCount = pkgGet16(in + HDR_DISK_COUNT);
+ h->setId = pkgGet32(in + HDR_SET_ID);
+ h->partOffset = pkgGet32(in + HDR_PART_OFFSET);
+ h->partLength = pkgGet32(in + HDR_PART_LENGTH);
+ h->zipSize = pkgGet32(in + HDR_ZIP_SIZE);
+ h->fileCount = pkgGet32(in + HDR_FILE_COUNT);
+ h->unpackedBytes = (uint64_t)pkgGet32(in + HDR_UNPACKED) | ((uint64_t)pkgGet32(in + HDR_UNPACKED_HI) << WORD_BITS);
+
+ return h->disk >= 1 && h->disk <= h->diskCount && h->diskCount <= PKG_MAX_DISKS && h->partOffset <= h->zipSize && h->partLength <= h->zipSize - h->partOffset;
+}
+
+
+void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out) {
+ memset(out, 0, PKG_HEADER_SIZE);
+ memcpy(out, PKG_MAGIC, PKG_MAGIC_SIZE);
+ put16(out + HDR_VERSION, PKG_FORMAT_VERSION);
+ put16(out + HDR_DISK, h->disk);
+ put16(out + HDR_DISK_COUNT, h->diskCount);
+ put32(out + HDR_SET_ID, h->setId);
+ put32(out + HDR_PART_OFFSET, h->partOffset);
+ put32(out + HDR_PART_LENGTH, h->partLength);
+ put32(out + HDR_ZIP_SIZE, h->zipSize);
+ put32(out + HDR_FILE_COUNT, h->fileCount);
+ put32(out + HDR_UNPACKED, (uint32_t)h->unpackedBytes);
+ put32(out + HDR_UNPACKED_HI, (uint32_t)(h->unpackedBytes >> WORD_BITS));
+ put32(out + HDR_CRC, (uint32_t)mz_crc32(MZ_CRC32_INIT, out, HDR_CRC));
+}
+
+
+bool pkgHeaderRead(FILE *f, PkgHeaderT *h, uint32_t *dataStart) {
+ uint8_t buf[PKG_HEADER_SIZE];
+
+ if (fseek(f, 0, SEEK_END) != 0) {
+ return false;
+ }
+
+ long size = ftell(f);
+
+ if (size < PKG_HEADER_SIZE || fseek(f, size - PKG_HEADER_SIZE, SEEK_SET) != 0 || fread(buf, 1, sizeof(buf), f) != sizeof(buf) || !pkgHeaderDecode(buf, h)) {
+ return false;
+ }
+
+ if ((uint32_t)(size - PKG_HEADER_SIZE) < h->partLength) {
+ return false;
+ }
+
+ *dataStart = (uint32_t)(size - PKG_HEADER_SIZE) - h->partLength;
+ return true;
+}
+
+
+bool pkgLocalPath(const char *dir, const char *relName, char *out, int32_t outSize) {
+ size_t dirLen = strlen(dir);
+ bool sep = dirLen > 0 && (dir[dirLen - 1] == '/' || dir[dirLen - 1] == '\\');
+ int32_t n = snprintf(out, outSize, "%s%s%s", dir, sep ? "" : PKG_DIR_SEP, relName);
+
+ if (n <= 0 || n >= outSize) {
+ return false;
+ }
+
+ for (char *p = out + dirLen; *p; p++) {
+ if (*p == PKG_NAME_SEP) {
+ *p = PKG_DIR_SEP[0];
+ }
+ }
+
+ return true;
+}
+
+
+bool pkgNameIsValid(const char *name) {
+ int32_t len = (int32_t)strlen(name);
+
+ if (len == 0 || len >= PKG_NAME_MAX || name[0] == PKG_NAME_SEP || name[len - 1] == PKG_NAME_SEP) {
+ return false;
+ }
+
+ for (const char *p = name; *p; ) {
+ const char *end = strchr(p, PKG_NAME_SEP);
+ int32_t n = end ? (int32_t)(end - p) : (int32_t)strlen(p);
+
+ if (!dosPartIsValid(p, n)) {
+ return false;
+ }
+
+ p += n + (end ? 1 : 0);
+ }
+
+ return true;
+}
+
+
+bool pkgPartName(int32_t disk, char *out, int32_t outSize) {
+ if (disk < 1 || disk > PKG_MAX_DISKS) {
+ return false;
+ }
+
+ int32_t n = snprintf(out, outSize, PKG_PART_FMT, (int)disk);
+
+ return n > 0 && n < outSize;
+}
+
+
+static void put16(uint8_t *p, uint16_t v) {
+ p[0] = (uint8_t)(v & BYTE_MASK);
+ p[1] = (uint8_t)(v >> BYTE_BITS);
+}
+
+
+static void put32(uint8_t *p, uint32_t v) {
+ put16(p, (uint16_t)(v & HALF_MASK));
+ put16(p + 2, (uint16_t)(v >> HALF_BITS));
+}
diff --git a/src/installer/pkgFormat.h b/src/installer/pkgFormat.h
new file mode 100644
index 00000000..53d9a60f
--- /dev/null
+++ b/src/installer/pkgFormat.h
@@ -0,0 +1,120 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// pkgFormat.h -- the DVX package: one standard ZIP (deflate) of the DVX
+// folder, carried in one or more parts.
+//
+// A part is the bytes of a slice of the ZIP followed by a PKG_HEADER_SIZE
+// header. On a CD the ZIP is one part appended to SETUP.EXE (the header
+// is the last bytes of SETUP.EXE); on floppies the ZIP is cut into
+// DVX.P01, DVX.P02, ... (Disk N holds DVX.P0N). Every header carries the
+// set ID (the CRC-32 of the whole ZIP, the same in every part), the
+// part's disk number and the number of disks, so Setup asks for the right
+// disk and refuses a disk from another set. Disk 1's header (or the CD's)
+// also says how many files the ZIP holds and how many bytes they unpack
+// to, for the free space check and the progress bar.
+//
+// The ZIP's local headers carry their sizes (no data descriptors), so
+// Setup reads the ZIP front to back as a stream, across disks, and checks
+// every file's CRC-32. Every name is a relative path of DOS 8.3 parts
+// separated by '/'.
+//
+// Shared by SETUP.EXE (pkgRead.c), the package tool (pkgTool.c) and the
+// host tests.
+
+#ifndef PKG_FORMAT_H
+#define PKG_FORMAT_H
+
+#include
+#include
+#include
+
+// Part header: magic, format version, then the fields of PkgHeaderT and
+// a CRC-32 of everything before it. Little-endian.
+#define PKG_HEADER_SIZE 48
+#define PKG_MAGIC "DVXPKG\032"
+#define PKG_MAGIC_SIZE 8
+#define PKG_FORMAT_VERSION 1
+
+// Part file names: DVX.P01 .. DVX.P99 (PKG_PART_NAME_MAX with the NUL)
+#define PKG_PART_FMT "DVX.P%02d"
+#define PKG_PART_SCAN "DVX.P%2d"
+#define PKG_PART_NAME_MAX 8
+#define PKG_MAX_DISKS 99
+
+// ZIP record signatures and sizes (APPNOTE 4.3.7, 4.3.12)
+#define PKG_ZIP_LOCAL_SIG 0x04034b50u
+#define PKG_ZIP_CENTRAL_SIG 0x02014b50u
+#define PKG_ZIP_LOCAL_SIZE 30
+#define PKG_ZIP_STORED 0
+#define PKG_ZIP_DEFLATED 8
+#define PKG_ZIP_FLAG_DESC 0x0008 // sizes after the data: not allowed here
+#define PKG_ZIP_FLAG_CRYPT 0x0001 // encrypted: not allowed here
+
+// Longest name in the package (a relative path)
+#define PKG_NAME_MAX 260
+
+// Separator of the names in the package
+#define PKG_NAME_SEP '/'
+
+// DOS 8.3 limits every path part must keep to
+#define PKG_DOS_BASE_MAX 8
+#define PKG_DOS_EXT_MAX 3
+
+typedef struct {
+ uint16_t disk; // this part's disk, 1 .. diskCount
+ uint16_t diskCount; // parts in the set
+ uint32_t setId; // CRC-32 of the whole ZIP
+ uint32_t partOffset; // where this part's bytes start in the ZIP
+ uint32_t partLength; // bytes of the ZIP in this part
+ uint32_t zipSize; // bytes in the whole ZIP
+ uint32_t fileCount; // files in the ZIP (not folders)
+ uint64_t unpackedBytes; // their size unpacked
+} PkgHeaderT;
+
+// Parse a header; false when the magic, version, CRC or fields are wrong.
+bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h);
+
+// Write h as PKG_HEADER_SIZE bytes.
+void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out);
+
+// Read the header at the end of an open file and check that the part's
+// bytes fit in front of it; *dataStart = file offset of the part's first
+// byte. False when the file has no valid header.
+bool pkgHeaderRead(FILE *f, PkgHeaderT *h, uint32_t *dataStart);
+
+// dir + the relative name with the platform's separator. False when it
+// does not fit.
+bool pkgLocalPath(const char *dir, const char *relName, char *out, int32_t outSize);
+
+// True when every '/'-separated part of a relative name is a DOS 8.3 name
+// (no empty part, no "." or "..", no drive, no backslash).
+bool pkgNameIsValid(const char *name);
+
+// "DVX.Pnn" for disk (1 .. PKG_MAX_DISKS). False when out of range.
+bool pkgPartName(int32_t disk, char *out, int32_t outSize);
+
+// Little-endian field access (shared with the ZIP reader)
+uint16_t pkgGet16(const uint8_t *p);
+uint32_t pkgGet32(const uint8_t *p);
+
+#endif // PKG_FORMAT_H
diff --git a/src/installer/pkgRead.c b/src/installer/pkgRead.c
new file mode 100644
index 00000000..ef353891
--- /dev/null
+++ b/src/installer/pkgRead.c
@@ -0,0 +1,798 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// pkgRead.c -- finding the DVX package and unpacking it (see pkgRead.h)
+
+#include "pkgRead.h"
+#include "fileops/fileOps.h"
+#include "miniz.h"
+#include "stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "dvxMem.h"
+
+// Bytes read from a part at a time, and the work one step does
+#define PKG_IN_BUF 16384
+#define PKG_STEP_BYTES 32768
+
+// Local header field offsets (after the signature)
+#define LH_FLAGS 6
+#define LH_METHOD 8
+#define LH_TIME 10
+#define LH_DATE 12
+#define LH_CRC 14
+#define LH_PACKED 18
+#define LH_SIZE 22
+#define LH_NAME_LEN 26
+#define LH_EXTRA_LEN 28
+#define ZIP_SIG_SIZE 4
+
+// DOS date and time fields
+#define DOS_YEAR_SHIFT 9
+#define DOS_YEAR_MASK 0x7F
+#define DOS_YEAR_BASE 1980
+#define DOS_MONTH_SHIFT 5
+#define DOS_MONTH_MASK 0x0F
+#define DOS_DAY_MASK 0x1F
+#define DOS_HOUR_SHIFT 11
+#define DOS_MINUTE_SHIFT 5
+#define DOS_MINUTE_MASK 0x3F
+#define DOS_SECOND_MASK 0x1F
+#define TM_YEAR_BASE 1900
+
+#define PERCENT_FULL 100
+
+typedef enum {
+ JobHeaderE, // next: a local header or the central directory
+ JobDataE, // unpacking a file
+ JobDoneE
+} JobStateE;
+
+struct PkgJobT {
+ PkgSourceT src;
+ char destDir[DVX_MAX_PATH];
+ PkgAskDiskFnT ask;
+ PkgKeepFnT keep;
+ void *user;
+ FILE *part;
+ int32_t disk;
+ uint32_t partLeft; // bytes of the part not read yet
+ uint32_t zipPos; // ZIP bytes read from all parts
+ uint8_t in[PKG_IN_BUF];
+ uint32_t inPos;
+ uint32_t inLen;
+ JobStateE state;
+ bool cancel;
+ bool failed;
+ char msg[PKG_MSG_MAX];
+ // the file being unpacked
+ char name[PKG_NAME_MAX];
+ char outPath[DVX_MAX_PATH];
+ FILE *out;
+ bool skipping; // kept: its data is read, not written
+ uint16_t method;
+ uint16_t dosDate;
+ uint16_t dosTime;
+ uint32_t crcWant;
+ uint32_t crcHave;
+ uint32_t packedLeft;
+ uint32_t size;
+ uint32_t sizeDone;
+ tinfl_decompressor inflator;
+ uint8_t dict[TINFL_LZ_DICT_SIZE];
+ uint32_t dictPos;
+ // totals
+ uint64_t bytesDone; // files finished
+ int32_t filesDone;
+ char **files;
+ char **written;
+ char **createdDirs;
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void dirOfPath(const char *path, char *out, int32_t outSize);
+static time_t dosTimeToTime(uint16_t date, uint16_t time);
+static bool ensureDirs(PkgJobT *job, const char *relName, bool lastIsDir);
+static void entryAbort(PkgJobT *job);
+static PkgStepE entryData(PkgJobT *job);
+static bool entryFinish(PkgJobT *job);
+static PkgStepE entryHeader(PkgJobT *job);
+static bool entryOpen(PkgJobT *job);
+static PkgStepE fail(PkgJobT *job, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+static bool fill(PkgJobT *job);
+static bool openPart(PkgJobT *job, int32_t disk);
+static PkgDiskProblemE otherDisk(const PkgJobT *job, int32_t wanted);
+static bool partPath(const PkgSourceT *src, int32_t disk, char *out, int32_t outSize);
+void pkgFind(const char *exePath, PkgSourceT *src);
+void pkgJobCancel(PkgJobT *job);
+PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user);
+char *const *pkgJobCreatedDirs(const PkgJobT *job);
+char *const *pkgJobFiles(const PkgJobT *job);
+void pkgJobFree(PkgJobT *job);
+const char *pkgJobMessage(const PkgJobT *job);
+void pkgJobProgress(const PkgJobT *job, PkgProgressT *out);
+PkgStepE pkgJobStep(PkgJobT *job);
+char *const *pkgJobWritten(const PkgJobT *job);
+static bool readBytes(PkgJobT *job, uint8_t *buf, uint32_t len);
+static bool writeOut(PkgJobT *job, const uint8_t *data, uint32_t len);
+
+
+// The folder part of path ("." when it has none).
+static void dirOfPath(const char *path, char *out, int32_t outSize) {
+ const char *end = platformPathDirEnd(path);
+
+ if (!end) {
+ snprintf(out, outSize, ".");
+ return;
+ }
+
+ snprintf(out, outSize, "%.*s", (int)(end - path), path);
+}
+
+
+static time_t dosTimeToTime(uint16_t date, uint16_t time) {
+ struct tm t;
+
+ memset(&t, 0, sizeof(t));
+ t.tm_year = ((date >> DOS_YEAR_SHIFT) & DOS_YEAR_MASK) + DOS_YEAR_BASE - TM_YEAR_BASE;
+ t.tm_mon = ((date >> DOS_MONTH_SHIFT) & DOS_MONTH_MASK) - 1;
+ t.tm_mday = date & DOS_DAY_MASK;
+ t.tm_hour = time >> DOS_HOUR_SHIFT;
+ t.tm_min = (time >> DOS_MINUTE_SHIFT) & DOS_MINUTE_MASK;
+ t.tm_sec = (time & DOS_SECOND_MASK) * 2;
+ t.tm_isdst = -1;
+ return mktime(&t);
+}
+
+
+// Make every folder of relName that does not exist yet (the last part
+// too when lastIsDir), recording each one made.
+static bool ensureDirs(PkgJobT *job, const char *relName, bool lastIsDir) {
+ int32_t len = (int32_t)strlen(relName);
+
+ for (int32_t i = 1; i <= len; i++) {
+ if (relName[i] != PKG_NAME_SEP && !(i == len && lastIsDir)) {
+ continue;
+ }
+
+ char rel[PKG_NAME_MAX];
+ char full[DVX_MAX_PATH];
+ struct stat st;
+
+ snprintf(rel, sizeof(rel), "%.*s", (int)i, relName);
+
+ if (!pkgLocalPath(job->destDir, rel, full, sizeof(full))) {
+ fail(job, "The path of %s is too long.", rel);
+ return false;
+ }
+
+ if (stat(full, &st) == 0) {
+ continue;
+ }
+
+ if (platformMkdirRecursive(full) != 0) {
+ fail(job, "Setup could not create the folder %s.", full);
+ return false;
+ }
+
+ arrput(job->createdDirs, strdup(full));
+ }
+
+ return true;
+}
+
+
+// Close and remove a half-written file.
+static void entryAbort(PkgJobT *job) {
+ if (!job->out) {
+ return;
+ }
+
+ fclose(job->out);
+ job->out = NULL;
+ remove(job->outPath);
+
+ if (arrlen(job->written) > 0) {
+ free(arrpop(job->written));
+ }
+}
+
+
+// Unpack (or skip) up to PKG_STEP_BYTES of the current file.
+static PkgStepE entryData(PkgJobT *job) {
+ uint32_t work = 0;
+
+ while (work < PKG_STEP_BYTES) {
+ if (job->packedLeft > 0 && !fill(job)) {
+ return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
+ }
+
+ uint32_t avail = job->inLen - job->inPos;
+
+ if (avail > job->packedLeft) {
+ avail = job->packedLeft;
+ }
+
+ bool done;
+
+ if (job->skipping || job->method == PKG_ZIP_STORED) {
+ if (!job->skipping && !writeOut(job, job->in + job->inPos, avail)) {
+ return PkgStepErrorE;
+ }
+
+ job->inPos += avail;
+ job->packedLeft -= avail;
+ work += avail;
+ done = job->packedLeft == 0;
+ } else {
+ size_t inBytes = avail;
+ size_t outBytes = TINFL_LZ_DICT_SIZE - job->dictPos;
+ int flags = job->packedLeft > avail ? TINFL_FLAG_HAS_MORE_INPUT : 0;
+
+ tinfl_status status = tinfl_decompress(&job->inflator, job->in + job->inPos, &inBytes, job->dict, job->dict + job->dictPos, &outBytes, (mz_uint32)flags);
+
+ job->inPos += (uint32_t)inBytes;
+ job->packedLeft -= (uint32_t)inBytes;
+ work += (uint32_t)(inBytes + outBytes);
+
+ if (outBytes > 0 && !writeOut(job, job->dict + job->dictPos, (uint32_t)outBytes)) {
+ return PkgStepErrorE;
+ }
+
+ job->dictPos = (job->dictPos + (uint32_t)outBytes) & (TINFL_LZ_DICT_SIZE - 1);
+
+ if (status < TINFL_STATUS_DONE || (status == TINFL_STATUS_NEEDS_MORE_INPUT && job->packedLeft == 0)) {
+ entryAbort(job);
+ return fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk);
+ }
+
+ done = status == TINFL_STATUS_DONE;
+ }
+
+ if (done) {
+ return entryFinish(job) ? PkgStepMoreE : PkgStepErrorE;
+ }
+ }
+
+ return PkgStepMoreE;
+}
+
+
+// The file's data has all been read: check it, close it, date it.
+static bool entryFinish(PkgJobT *job) {
+ if (!job->skipping) {
+ if (job->packedLeft != 0 || job->sizeDone != job->size || job->crcHave != job->crcWant) {
+ entryAbort(job);
+ fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk);
+ return false;
+ }
+
+ FILE *f = job->out;
+
+ job->out = NULL;
+
+ if (fclose(f) != 0) {
+ remove(job->outPath);
+ fail(job, "Setup could not write %s. The disk may be full.", job->outPath);
+ return false;
+ }
+
+ fileOpsSetTime(job->outPath, dosTimeToTime(job->dosDate, job->dosTime));
+ }
+
+ job->bytesDone += job->size;
+ job->filesDone++;
+ job->state = JobHeaderE;
+ return true;
+}
+
+
+// The next local header (or the central directory: the end).
+static PkgStepE entryHeader(PkgJobT *job) {
+ uint8_t hdr[PKG_ZIP_LOCAL_SIZE];
+
+ if (!readBytes(job, hdr, ZIP_SIG_SIZE)) {
+ return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
+ }
+
+ uint32_t sig = pkgGet32(hdr);
+
+ if (sig == PKG_ZIP_CENTRAL_SIG) {
+ if (job->filesDone != (int32_t)job->src.info.fileCount) {
+ return fail(job, "The DVX package is incomplete: %d of %d files.", (int)job->filesDone, (int)job->src.info.fileCount);
+ }
+
+ job->state = JobDoneE;
+ return PkgStepDoneE;
+ }
+
+ if (sig != PKG_ZIP_LOCAL_SIG) {
+ return fail(job, "The DVX package is damaged (disk %d).", (int)job->disk);
+ }
+
+ if (!readBytes(job, hdr + ZIP_SIG_SIZE, PKG_ZIP_LOCAL_SIZE - ZIP_SIG_SIZE)) {
+ return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
+ }
+
+ uint16_t flags = pkgGet16(hdr + LH_FLAGS);
+ uint16_t nameLen = pkgGet16(hdr + LH_NAME_LEN);
+ uint16_t extraLen = pkgGet16(hdr + LH_EXTRA_LEN);
+
+ job->method = pkgGet16(hdr + LH_METHOD);
+ job->dosTime = pkgGet16(hdr + LH_TIME);
+ job->dosDate = pkgGet16(hdr + LH_DATE);
+ job->crcWant = pkgGet32(hdr + LH_CRC);
+ job->packedLeft = pkgGet32(hdr + LH_PACKED);
+ job->size = pkgGet32(hdr + LH_SIZE);
+
+ if ((flags & (PKG_ZIP_FLAG_DESC | PKG_ZIP_FLAG_CRYPT)) || (job->method != PKG_ZIP_STORED && job->method != PKG_ZIP_DEFLATED) || nameLen == 0 || nameLen >= PKG_NAME_MAX) {
+ return fail(job, "The DVX package uses a form Setup cannot read (disk %d).", (int)job->disk);
+ }
+
+ if (!readBytes(job, (uint8_t *)job->name, nameLen)) {
+ return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
+ }
+
+ job->name[nameLen] = '\0';
+
+ for (uint16_t left = extraLen; left > 0; ) {
+ uint8_t skip[PKG_ZIP_LOCAL_SIZE];
+ uint16_t n = left < sizeof(skip) ? left : (uint16_t)sizeof(skip);
+
+ if (!readBytes(job, skip, n)) {
+ return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
+ }
+
+ left -= n;
+ }
+
+ bool isDir = job->name[nameLen - 1] == PKG_NAME_SEP;
+
+ if (isDir) {
+ job->name[nameLen - 1] = '\0';
+ }
+
+ if (!pkgNameIsValid(job->name)) {
+ return fail(job, "The DVX package holds a name Setup does not accept: %s", job->name);
+ }
+
+ if (isDir) {
+ if (!ensureDirs(job, job->name, true)) {
+ return PkgStepErrorE;
+ }
+
+ char *listed = (char *)malloc(nameLen + 1);
+
+ if (listed) {
+ snprintf(listed, nameLen + 1, "%s%c", job->name, PKG_NAME_SEP);
+ arrput(job->files, listed);
+ }
+
+ return PkgStepMoreE;
+ }
+
+ return entryOpen(job) ? PkgStepMoreE : PkgStepErrorE;
+}
+
+
+// Set up the file of the header just read: kept, or opened for writing.
+static bool entryOpen(PkgJobT *job) {
+ struct stat st;
+
+ if (!ensureDirs(job, job->name, false)) {
+ return false;
+ }
+
+ if (!pkgLocalPath(job->destDir, job->name, job->outPath, sizeof(job->outPath))) {
+ fail(job, "The path of %s is too long.", job->name);
+ return false;
+ }
+
+ arrput(job->files, strdup(job->name));
+ job->skipping = stat(job->outPath, &st) == 0 && job->keep && job->keep(job->user, job->name);
+ job->sizeDone = 0;
+ job->crcHave = (uint32_t)MZ_CRC32_INIT;
+ job->dictPos = 0;
+ job->state = JobDataE;
+ tinfl_init(&job->inflator);
+
+ if (job->skipping) {
+ return true;
+ }
+
+ job->out = fopen(job->outPath, "wb");
+
+ // A read-only copy (an older install copied from a CD) is replaced.
+ if (!job->out && fileOpsSetAttr(job->outPath, 0)) {
+ job->out = fopen(job->outPath, "wb");
+ }
+
+ if (!job->out) {
+ fail(job, "Setup could not write %s. The disk may be full or write-protected.", job->outPath);
+ return false;
+ }
+
+ arrput(job->written, strdup(job->outPath));
+ return true;
+}
+
+
+static PkgStepE fail(PkgJobT *job, const char *fmt, ...) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(job->msg, sizeof(job->msg), fmt, ap);
+ va_end(ap);
+ job->failed = true;
+ return PkgStepErrorE;
+}
+
+
+// Make sure the input buffer has a byte, opening the next part when this
+// one is used up. False on an error (message set) or a cancel.
+static bool fill(PkgJobT *job) {
+ if (job->inPos < job->inLen) {
+ return true;
+ }
+
+ while (job->partLeft == 0) {
+ if (job->disk >= job->src.info.diskCount) {
+ fail(job, "The DVX package ends too early (disk %d).", (int)job->disk);
+ return false;
+ }
+
+ if (!openPart(job, job->disk + 1)) {
+ return false;
+ }
+ }
+
+ uint32_t n = job->partLeft < PKG_IN_BUF ? job->partLeft : PKG_IN_BUF;
+
+ if (fread(job->in, 1, n, job->part) != n) {
+ fail(job, "Setup could not read the DVX package on disk %d.", (int)job->disk);
+ return false;
+ }
+
+ job->inPos = 0;
+ job->inLen = n;
+ job->partLeft -= n;
+ job->zipPos += n;
+ return true;
+}
+
+
+// Open disk's part (asking for the disk until it is the right one) and
+// position it at its first byte. False on a cancel or an error.
+static bool openPart(PkgJobT *job, int32_t disk) {
+ char path[DVX_MAX_PATH];
+
+ if (job->part) {
+ fclose(job->part);
+ job->part = NULL;
+ }
+
+ if (job->src.found == PkgFoundAppendedE) {
+ snprintf(path, sizeof(path), "%s", job->src.path);
+ } else if (!partPath(&job->src, disk, path, sizeof(path))) {
+ fail(job, "The folder %s is too long.", job->src.partDir);
+ return false;
+ }
+
+ for (;;) {
+ PkgHeaderT h;
+ uint32_t start = 0;
+ PkgDiskProblemE problem = PkgDiskMissingE;
+ FILE *f = fopen(path, "rb");
+
+ if (f) {
+ if (!pkgHeaderRead(f, &h, &start)) {
+ problem = PkgDiskDamagedE;
+ } else if (h.setId != job->src.info.setId) {
+ problem = PkgDiskOtherSetE;
+ } else if (h.disk != disk || h.partOffset != job->zipPos) {
+ problem = PkgDiskWrongE;
+ } else if (fseek(f, (long)start, SEEK_SET) != 0) {
+ problem = PkgDiskDamagedE;
+ } else {
+ job->part = f;
+ job->disk = disk;
+ job->partLeft = h.partLength;
+ return true;
+ }
+
+ fclose(f);
+ } else {
+ problem = otherDisk(job, disk);
+ }
+
+ if (job->src.found == PkgFoundAppendedE || !job->ask) {
+ fail(job, "Setup could not read the DVX package in %s.", path);
+ return false;
+ }
+
+ if (!job->ask(job->user, disk, problem)) {
+ job->cancel = true;
+ return false;
+ }
+ }
+}
+
+
+// The wanted part is not there: is another disk's part (of this set or
+// another one) in the drive instead? The disk before the wanted one is
+// the normal case (not swapped yet): just missing.
+static PkgDiskProblemE otherDisk(const PkgJobT *job, int32_t wanted) {
+ char **names = dvxReadDir(job->src.partDir);
+ PkgDiskProblemE problem = PkgDiskMissingE;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(names) && problem == PkgDiskMissingE; i++) {
+ char upper[PKG_PART_NAME_MAX];
+ char path[DVX_MAX_PATH];
+ int partNo = 0; // sscanf's %d wants an int
+ char end = '\0';
+ uint32_t start = 0;
+ PkgHeaderT h;
+
+ snprintf(upper, sizeof(upper), "%s", names[i]);
+
+ for (char *p = upper; *p; p++) {
+ *p = (char)toupper((unsigned char)*p);
+ }
+
+ if (strlen(names[i]) >= sizeof(upper) || sscanf(upper, PKG_PART_SCAN "%c", &partNo, &end) != 1 || !fileOpsPathJoin(path, sizeof(path), job->src.partDir, names[i])) {
+ continue;
+ }
+
+ FILE *f = fopen(path, "rb");
+
+ if (!f) {
+ continue;
+ }
+
+ if (!pkgHeaderRead(f, &h, &start)) {
+ problem = PkgDiskDamagedE;
+ } else if (h.setId != job->src.info.setId) {
+ problem = PkgDiskOtherSetE;
+ } else if (h.disk != wanted - 1) {
+ problem = PkgDiskWrongE;
+ }
+
+ fclose(f);
+ }
+
+ dvxReadDirFree(names);
+ return problem;
+}
+
+
+static bool partPath(const PkgSourceT *src, int32_t disk, char *out, int32_t outSize) {
+ char name[PKG_PART_NAME_MAX];
+
+ return pkgPartName(disk, name, sizeof(name)) && fileOpsPathJoin(out, outSize, src->partDir, name);
+}
+
+
+void pkgFind(const char *exePath, PkgSourceT *src) {
+ PkgHeaderT h;
+ uint32_t start = 0;
+
+ memset(&h, 0, sizeof(h));
+ memset(src, 0, sizeof(*src));
+ src->found = PkgFoundNoneE;
+ dirOfPath(exePath, src->partDir, sizeof(src->partDir));
+
+ FILE *f = fopen(exePath, "rb");
+
+ if (f) {
+ bool appended = pkgHeaderRead(f, &h, &start) && h.diskCount == 1 && h.partOffset == 0 && h.partLength == h.zipSize;
+
+ fclose(f);
+
+ if (appended) {
+ src->found = PkgFoundAppendedE;
+ src->info = h;
+ snprintf(src->path, sizeof(src->path), "%s", exePath);
+ return;
+ }
+ }
+
+ if (!partPath(src, 1, src->path, sizeof(src->path))) {
+ return;
+ }
+
+ f = fopen(src->path, "rb");
+
+ if (!f) {
+ return;
+ }
+
+ src->found = pkgHeaderRead(f, &h, &start) && h.disk == 1 && h.partOffset == 0 ? PkgFoundPartsE : PkgFoundDamagedE;
+ src->info = h;
+ fclose(f);
+}
+
+
+void pkgJobCancel(PkgJobT *job) {
+ job->cancel = true;
+}
+
+
+PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user) {
+ PkgJobT *job = (PkgJobT *)calloc(1, sizeof(PkgJobT));
+
+ if (!job) {
+ return NULL;
+ }
+
+ job->src = *src;
+ job->ask = ask;
+ job->keep = keep;
+ job->user = user;
+ job->state = JobHeaderE;
+ snprintf(job->destDir, sizeof(job->destDir), "%s", destDir);
+
+ // Disk 1 is the one pkgFind looked at; asking again is only for the
+ // disks after it.
+ if (!openPart(job, 1)) {
+ job->state = JobDoneE;
+ }
+
+ return job;
+}
+
+
+char *const *pkgJobCreatedDirs(const PkgJobT *job) {
+ return job->createdDirs;
+}
+
+
+char *const *pkgJobFiles(const PkgJobT *job) {
+ return job->files;
+}
+
+
+void pkgJobFree(PkgJobT *job) {
+ if (!job) {
+ return;
+ }
+
+ entryAbort(job);
+
+ if (job->part) {
+ fclose(job->part);
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(job->files); i++) {
+ free(job->files[i]);
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(job->written); i++) {
+ free(job->written[i]);
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(job->createdDirs); i++) {
+ free(job->createdDirs[i]);
+ }
+
+ arrfree(job->files);
+ arrfree(job->written);
+ arrfree(job->createdDirs);
+ free(job);
+}
+
+
+const char *pkgJobMessage(const PkgJobT *job) {
+ return job->msg;
+}
+
+
+void pkgJobProgress(const PkgJobT *job, PkgProgressT *out) {
+ uint64_t done = job->bytesDone + (job->state == JobDataE ? job->sizeDone : 0);
+
+ memset(out, 0, sizeof(*out));
+ out->bytesTotal = job->src.info.unpackedBytes;
+ out->bytesDone = done > out->bytesTotal ? out->bytesTotal : done;
+ out->filesDone = job->filesDone;
+ out->fileCount = (int32_t)job->src.info.fileCount;
+ out->percent = out->bytesTotal ? (int32_t)(out->bytesDone * PERCENT_FULL / out->bytesTotal) : PERCENT_FULL;
+ out->disk = job->disk;
+ out->current = job->state == JobDataE ? job->name : "";
+}
+
+
+PkgStepE pkgJobStep(PkgJobT *job) {
+ if (job->failed) {
+ return PkgStepErrorE;
+ }
+
+ if (job->cancel) {
+ entryAbort(job);
+ return PkgStepCancelledE;
+ }
+
+ switch (job->state) {
+ case JobHeaderE:
+ return entryHeader(job);
+
+ case JobDataE:
+ return entryData(job);
+
+ default:
+ return PkgStepDoneE;
+ }
+}
+
+
+char *const *pkgJobWritten(const PkgJobT *job) {
+ return job->written;
+}
+
+
+// Exactly len bytes of the stream, across parts.
+static bool readBytes(PkgJobT *job, uint8_t *buf, uint32_t len) {
+ while (len > 0) {
+ if (!fill(job)) {
+ return false;
+ }
+
+ uint32_t n = job->inLen - job->inPos;
+
+ if (n > len) {
+ n = len;
+ }
+
+ memcpy(buf, job->in + job->inPos, n);
+ job->inPos += n;
+ buf += n;
+ len -= n;
+ }
+
+ return true;
+}
+
+
+static bool writeOut(PkgJobT *job, const uint8_t *data, uint32_t len) {
+ if (job->sizeDone + len > job->size) {
+ entryAbort(job);
+ fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk);
+ return false;
+ }
+
+ if (fwrite(data, 1, len, job->out) != len) {
+ entryAbort(job);
+ fail(job, "Setup could not write %s. The disk may be full.", job->outPath);
+ return false;
+ }
+
+ job->crcHave = (uint32_t)mz_crc32(job->crcHave, data, len);
+ job->sizeDone += len;
+ return true;
+}
diff --git a/src/installer/pkgRead.h b/src/installer/pkgRead.h
new file mode 100644
index 00000000..5d3af980
--- /dev/null
+++ b/src/installer/pkgRead.h
@@ -0,0 +1,131 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// pkgRead.h -- finding the DVX package and unpacking it (see pkgFormat.h)
+//
+// pkgFind looks at SETUP.EXE itself (a package appended to it: the CD) and
+// then for DVX.P01 beside it (floppies). An unpack job reads the ZIP as a
+// stream in small steps (so the caller can draw progress and see Cancel
+// between them), opening the next part when one runs out: a missing part,
+// or one from another set or with the wrong disk number, is reported to
+// the ask callback, which asks for the disk and says whether to try again.
+// Every file is written straight to its place, its CRC-32 checked; a
+// damaged file is removed and ends the job with an error. The job records
+// what it wrote and which folders it made, so a cancelled new install can
+// be removed again (setupRemoveWritten).
+
+#ifndef PKG_READ_H
+#define PKG_READ_H
+
+#include "pkgFormat.h"
+#include "dvxPlat.h"
+
+#include
+#include
+
+// Longest message a job reports
+#define PKG_MSG_MAX (DVX_MAX_PATH + 160)
+
+typedef enum {
+ PkgFoundAppendedE, // appended to the program (CD)
+ PkgFoundPartsE, // DVX.P01 beside the program (floppies)
+ PkgFoundNoneE, // no package: an installed Setup (uninstall only)
+ PkgFoundDamagedE // something that looks like a package but is not
+} PkgFoundE;
+
+// Why a part could not be used
+typedef enum {
+ PkgDiskMissingE, // DVX.Pnn is not there (no disk, or another disk)
+ PkgDiskOtherSetE, // a part of another DVX set
+ PkgDiskWrongE, // the part of another disk of this set
+ PkgDiskDamagedE // not readable as a part
+} PkgDiskProblemE;
+
+typedef enum {
+ PkgStepMoreE,
+ PkgStepDoneE,
+ PkgStepErrorE, // message in pkgJobMessage
+ PkgStepCancelledE
+} PkgStepE;
+
+// Where the package is and what disk 1 (or the CD) says about it
+typedef struct {
+ PkgFoundE found;
+ PkgHeaderT info;
+ char path[DVX_MAX_PATH]; // the program (appended) or disk 1's DVX.P01
+ char partDir[DVX_MAX_PATH]; // folder the parts are read from
+} PkgSourceT;
+
+typedef struct {
+ uint64_t bytesDone; // unpacked bytes written (or kept)
+ uint64_t bytesTotal;
+ int32_t filesDone;
+ int32_t fileCount;
+ int32_t percent;
+ int32_t disk; // disk being read
+ const char *current; // file being unpacked (relative name)
+} PkgProgressT;
+
+// Ask for disk (1-based) because of problem; true = look again, false =
+// cancel the job.
+typedef bool (*PkgAskDiskFnT)(void *user, int32_t disk, PkgDiskProblemE problem);
+
+// A file that already exists at relName: true keeps it (not unpacked).
+typedef bool (*PkgKeepFnT)(void *user, const char *relName);
+
+typedef struct PkgJobT PkgJobT;
+
+// Look for the package of the program at exePath.
+void pkgFind(const char *exePath, PkgSourceT *src);
+
+// Ask the job to stop; the next step removes a half-written file and
+// returns PkgStepCancelledE.
+void pkgJobCancel(PkgJobT *job);
+
+// Start unpacking src into destDir (which must exist). keep may be NULL
+// (replace everything). NULL when out of memory or the package cannot
+// be opened (see pkgJobMessage of NULL: a fixed message).
+PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user);
+
+// Folders the job made (full paths, in the order made; stb_ds array owned
+// by the job).
+char *const *pkgJobCreatedDirs(const PkgJobT *job);
+
+void pkgJobFree(PkgJobT *job);
+
+// What went wrong (after PkgStepErrorE).
+const char *pkgJobMessage(const PkgJobT *job);
+
+void pkgJobProgress(const PkgJobT *job, PkgProgressT *out);
+
+// Do a little work: at most one file header and PKG_STEP_BYTES of data.
+PkgStepE pkgJobStep(PkgJobT *job);
+
+// Every file and folder of the package the job has unpacked or kept, as
+// relative names with '/' (a folder's ends with '/'; stb_ds array owned
+// by the job).
+char *const *pkgJobFiles(const PkgJobT *job);
+
+// Files the job wrote (full paths; not the ones kept).
+char *const *pkgJobWritten(const PkgJobT *job);
+
+#endif // PKG_READ_H
diff --git a/src/installer/pkgTool.c b/src/installer/pkgTool.c
new file mode 100644
index 00000000..02a1b9ad
--- /dev/null
+++ b/src/installer/pkgTool.c
@@ -0,0 +1,136 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// pkgTool.c -- dvxpkg, the package tool of `make dist` (build machine)
+//
+// dvxpkg zip FOLDER OUT.ZIP ZIP a staged DVX folder
+// dvxpkg cd SETUP.EXE PKG.ZIP OUT.EXE SETUP.EXE with the package
+// dvxpkg split PKG.ZIP OUTDIR DISK1ROOM ROOM DVX.P01.. for floppies
+// dvxpkg info FILE a part's header
+
+#include "pkgBuild.h"
+
+#include
+#include
+#include
+#include
+
+#define ARGS_ZIP 4
+#define ARGS_CD 5
+#define ARGS_SPLIT 6
+#define ARGS_INFO 3
+#define EXIT_USAGE 2
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int cmdInfo(const char *path);
+static bool parseBytes(const char *text, uint32_t *out);
+static int usage(void);
+int main(int argc, char *argv[]);
+
+
+static int cmdInfo(const char *path) {
+ PkgHeaderT h;
+ uint32_t start = 0;
+ FILE *f = fopen(path, "rb");
+
+ if (!f || !pkgHeaderRead(f, &h, &start)) {
+ fprintf(stderr, "dvxpkg: %s has no package header\n", path);
+
+ if (f) {
+ fclose(f);
+ }
+
+ return EXIT_FAILURE;
+ }
+
+ fclose(f);
+ printf("disk %u of %u, set %08" PRIX32 ", part bytes %" PRIu32 " at %" PRIu32 " (file offset %" PRIu32 "), zip %" PRIu32 " bytes, %" PRIu32 " files, %" PRIu64 " bytes unpacked\n", (unsigned)h.disk, (unsigned)h.diskCount, h.setId, h.partLength, h.partOffset, start, h.zipSize, h.fileCount, h.unpackedBytes);
+ return EXIT_SUCCESS;
+}
+
+
+static bool parseBytes(const char *text, uint32_t *out) {
+ char *end = NULL;
+ unsigned long v = strtoul(text, &end, 10);
+
+ *out = (uint32_t)v;
+ return end && end != text && *end == '\0' && v > 0 && v <= UINT32_MAX;
+}
+
+
+static int usage(void) {
+ fprintf(stderr, "usage: dvxpkg zip FOLDER OUT.ZIP\n dvxpkg cd SETUP.EXE PKG.ZIP OUT.EXE\n dvxpkg split PKG.ZIP OUTDIR DISK1ROOM ROOM\n dvxpkg info FILE\n");
+ return EXIT_USAGE;
+}
+
+
+int main(int argc, char *argv[]) {
+ char err[PKG_BUILD_ERR_MAX];
+ uint32_t first = 0;
+ uint32_t room = 0;
+
+ if (argc == ARGS_ZIP && strcmp(argv[1], "zip") == 0) {
+ PkgHeaderT info;
+
+ if (!pkgBuildZip(argv[2], argv[3], err, sizeof(err)) || !pkgBuildInfo(argv[3], &info, err, sizeof(err))) {
+ fprintf(stderr, "dvxpkg: %s\n", err);
+ return EXIT_FAILURE;
+ }
+
+ printf("%" PRIu32 " files, %" PRIu64 " bytes unpacked, %" PRIu32 " bytes packed, set %08" PRIX32 "\n", info.fileCount, info.unpackedBytes, info.zipSize, info.setId);
+ return EXIT_SUCCESS;
+ }
+
+ if (argc == ARGS_CD && strcmp(argv[1], "cd") == 0) {
+ if (!pkgBuildAppended(argv[2], argv[3], argv[4], err, sizeof(err))) {
+ fprintf(stderr, "dvxpkg: %s\n", err);
+ return EXIT_FAILURE;
+ }
+
+ return cmdInfo(argv[4]);
+ }
+
+ if (argc == ARGS_SPLIT && strcmp(argv[1], "split") == 0) {
+ if (!parseBytes(argv[4], &first) || !parseBytes(argv[5], &room)) {
+ return usage();
+ }
+
+ int32_t disks = pkgBuildSplit(argv[2], argv[3], first, room, err, sizeof(err));
+
+ if (disks < 0) {
+ fprintf(stderr, "dvxpkg: %s\n", err);
+ return EXIT_FAILURE;
+ }
+
+ printf("%d\n", (int)disks);
+ return EXIT_SUCCESS;
+ }
+
+ if (argc == ARGS_INFO && strcmp(argv[1], "info") == 0) {
+ return cmdInfo(argv[2]);
+ }
+
+ return usage();
+}
diff --git a/src/installer/setup.c b/src/installer/setup.c
new file mode 100644
index 00000000..7d06608c
--- /dev/null
+++ b/src/installer/setup.c
@@ -0,0 +1,784 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// setup.c -- SETUP.EXE: installs DVX from its CD or floppies
+//
+// One self-contained DOS program: the DVX core and the six widgets it
+// uses are linked in (no DVX folder needed), and it never writes to the
+// disk it runs from. The package is appended to SETUP.EXE on a CD, or in
+// DVX.P01.. beside it on floppies (pkgRead.h). Pages in one window:
+// licence, folder (with Uninstall), progress, done. Without a package
+// (the SETUP.EXE installed in the DVX folder) it offers only Uninstall of
+// its own folder.
+//
+// SETUP the wizard
+// SETUP /AUTO=C:\DVX install there without questions (licence
+// taken as accepted, settings kept on an
+// upgrade, AUTOEXEC.BAT untouched); exit
+// code 0 when installed
+// SETUP /LOG=C:\SETUP.LOG write the log there (else it stays in memory)
+//
+// The unpacking runs from the main loop in slices, drawing progress and
+// seeing Cancel between them; the questions (disk swaps, errors) are
+// message boxes.
+
+#include "dvxApp.h"
+#include "dvxDlg.h"
+#include "dvxPlat.h"
+#include "dvxWgt.h"
+#include "box/box.h"
+#include "button/button.h"
+#include "checkbox/checkbox.h"
+#include "label/label.h"
+#include "progressBar/progress.h"
+#include "textInput/textInpt.h"
+#include "fileops/fileOps.h"
+#include "pkgRead.h"
+#include "setupCore.h"
+#include "setupLicense.h"
+#include "stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// ============================================================
+// Constants
+// ============================================================
+
+#define SETUP_TITLE "DVX Setup"
+#define SETUP_WIN_W 520
+#define SETUP_WIN_H 380
+#define SETUP_SPACING 6
+#define SETUP_SLICE_MS 100
+#define SETUP_DEST_FOLDER "DVX"
+#define SETUP_MSG_MAX (DVX_MAX_PATH * 2 + SETUP_ERR_MAX)
+#define SETUP_ARG_AUTO "/AUTO="
+#define SETUP_ARG_LOG "/LOG="
+#define SETUP_BYTES_PER_MB (1024 * 1024)
+#define SETUP_EXIT_FAILED 1
+
+// The widgets linked in (each module's wgtRegister, renamed by the
+// Makefile)
+void wgtRegisterBox(void);
+void wgtRegisterButton(void);
+void wgtRegisterCheckbox(void);
+void wgtRegisterLabel(void);
+void wgtRegisterProgress(void);
+void wgtRegisterTextInput(void);
+
+typedef enum {
+ PageLicenceE,
+ PageFolderE,
+ PageCopyE,
+ PageDoneE,
+ PageUninstallE,
+ PageCountE
+} PageE;
+
+typedef struct {
+ AppContextT ctx;
+ WindowT *win;
+ WidgetT *pages[PageCountE];
+ PageE page; // the one shown
+ WidgetT *accept; // licence accepted
+ WidgetT *nextBtn;
+ WidgetT *dest; // target folder
+ WidgetT *autoexec; // "start with the computer" (NULL without a boot drive)
+ WidgetT *status;
+ WidgetT *bar;
+ WidgetT *doneText;
+ WidgetT *startNow;
+ WidgetT *unText;
+ WidgetT *removeSettings;
+ WidgetT *unBackBtn;
+ PkgSourceT src;
+ char exePath[DVX_MAX_PATH];
+ char destDir[DVX_MAX_PATH];
+ char autoDest[DVX_MAX_PATH];
+ char logPath[DVX_MAX_PATH];
+ bool quit;
+ bool autoMode;
+ bool installRequested;
+ bool uninstallRequested;
+ bool cancelRequested;
+ bool startDvx;
+ int32_t exitCode;
+} SetupT;
+
+// ============================================================
+// Module state
+// ============================================================
+
+static SetupT sSetup;
+
+// PkgFoundE for the log
+static const char *const sFoundNames[] = { "appended", "in parts", "none", "damaged" };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool askDisk(void *user, int32_t disk, PkgDiskProblemE problem);
+static void buildPages(WidgetT *root);
+static WidgetT *buttonRow(WidgetT *page);
+static void installAutoexec(const char *destDir);
+static bool installCheck(const char *destDir, bool *upgrade, bool *keepSettings);
+static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInstall);
+static void installRun(void);
+static bool keepSetting(void *user, const char *relName);
+static void onAccept(WidgetT *w);
+static void onBack(WidgetT *w);
+static void onCancel(WidgetT *w);
+static void onClose(WindowT *win);
+static void onFinish(WidgetT *w);
+static void onInstall(WidgetT *w);
+static void onNext(WidgetT *w);
+static void onUninstall(WidgetT *w);
+static void onUninstallAsk(WidgetT *w);
+static void parseArgs(int argc, char *argv[]);
+static void report(const char *msg);
+static void showPage(PageE page);
+static void showProgress(const PkgJobT *job);
+static void startDvx(void);
+static void uninstallRun(void);
+int main(int argc, char *argv[]);
+
+
+// A part could not be used: say which disk is wanted and why.
+static bool askDisk(void *user, int32_t disk, PkgDiskProblemE problem) {
+ char msg[SETUP_MSG_MAX];
+ char where[DVX_MAX_PATH];
+ const char *dir = sSetup.src.partDir;
+
+ (void)user;
+
+ if (sSetup.autoMode) {
+ dvxLog("Setup: disk %d is not in %s", (int)disk, dir);
+ return false;
+ }
+
+ if (dir[0] && dir[1] == ':') {
+ snprintf(where, sizeof(where), "drive %c:", dir[0]);
+ } else {
+ snprintf(where, sizeof(where), "%s", dir);
+ }
+
+ switch (problem) {
+ case PkgDiskOtherSetE:
+ snprintf(msg, sizeof(msg), "The disk in %s belongs to a different set of DVX disks.\n\nPlease insert Disk %d of %d of this set and choose OK.", where, (int)disk, (int)sSetup.src.info.diskCount);
+ break;
+
+ case PkgDiskWrongE:
+ snprintf(msg, sizeof(msg), "This is not Disk %d.\n\nPlease insert Disk %d of %d in %s and choose OK.", (int)disk, (int)disk, (int)sSetup.src.info.diskCount, where);
+ break;
+
+ case PkgDiskDamagedE:
+ snprintf(msg, sizeof(msg), "The disk in %s cannot be read.\n\nPlease insert Disk %d of %d and choose OK.", where, (int)disk, (int)sSetup.src.info.diskCount);
+ break;
+
+ default:
+ snprintf(msg, sizeof(msg), "Please insert Disk %d of %d in %s and choose OK.", (int)disk, (int)sSetup.src.info.diskCount, where);
+ break;
+ }
+
+ dvxLog("Setup: asking for disk %d (problem %d)", (int)disk, (int)problem);
+ return dvxMessageBox(&sSetup.ctx, SETUP_TITLE, msg, MB_OKCANCEL | MB_ICONINFO) == ID_OK;
+}
+
+
+static void buildPages(WidgetT *root) {
+ char text[SETUP_MSG_MAX];
+ char boot = platformBootDrive();
+
+ for (int32_t i = 0; i < PageCountE; i++) {
+ sSetup.pages[i] = wgtVBox(root);
+ sSetup.pages[i]->weight = WGT_WEIGHT_FILL;
+ sSetup.pages[i]->spacing = wgtPixels(SETUP_SPACING);
+ }
+
+ // Licence
+ WidgetT *p = sSetup.pages[PageLicenceE];
+
+ wgtLabel(p, "Welcome to DVX Setup. Setup installs DVX on your hard disk.");
+ wgtLabel(p, "Please read the licence:");
+
+ WidgetT *licence = wgtTextArea(p, (int32_t)sizeof(sSetupLicense));
+
+ wgtSetText(licence, sSetupLicense);
+ wgtSetReadOnly(licence, true);
+ licence->weight = WGT_WEIGHT_FILL;
+
+ sSetup.accept = wgtCheckbox(p, "I &accept the licence");
+ sSetup.accept->onChange = onAccept;
+
+ WidgetT *row = buttonRow(p);
+
+ sSetup.nextBtn = wgtButton(row, "&Next >");
+ sSetup.nextBtn->onClick = onNext;
+ wgtButton(row, "E&xit")->onClick = onCancel;
+ wgtSetEnabled(sSetup.nextBtn, false);
+
+ // Folder
+ p = sSetup.pages[PageFolderE];
+ snprintf(text, sizeof(text), "DVX needs about %" PRIu64 " MB of disk space.", (setupSpaceNeeded(&sSetup.src.info) + SETUP_BYTES_PER_MB - 1) / SETUP_BYTES_PER_MB);
+ wgtLabel(p, text);
+ wgtLabel(p, "Install DVX in this &folder:");
+ sSetup.dest = wgtTextInput(p, DVX_MAX_PATH - 1);
+ text[0] = '\0';
+
+ if (boot) {
+ snprintf(text, sizeof(text), "%c:" DVX_PATH_SEP SETUP_DEST_FOLDER, boot);
+ sSetup.autoexec = wgtCheckbox(p, "&Start DVX when the computer starts (AUTOEXEC.BAT)");
+ }
+
+ wgtSetText(sSetup.dest, text);
+ wgtLabel(p, "If DVX is already in that folder, Setup can upgrade or repair it and keep your settings, or remove it (Uninstall).");
+ row = buttonRow(p);
+ wgtButton(row, "< &Back")->onClick = onBack;
+ wgtButton(row, "&Uninstall...")->onClick = onUninstallAsk;
+ wgtButton(row, "&Install")->onClick = onInstall;
+ wgtButton(row, "E&xit")->onClick = onCancel;
+
+ // Progress
+ p = sSetup.pages[PageCopyE];
+ wgtLabel(p, "Setup is installing DVX. This can take a few minutes.");
+ sSetup.status = wgtLabel(p, "");
+ sSetup.bar = wgtProgressBar(p);
+ row = buttonRow(p);
+ wgtButton(row, "&Cancel")->onClick = onCancel;
+
+ // Done
+ p = sSetup.pages[PageDoneE];
+ sSetup.doneText = wgtLabel(p, "");
+ sSetup.startNow = wgtCheckbox(p, "Start &DVX now");
+ wgtCheckboxSetChecked(sSetup.startNow, true);
+ row = buttonRow(p);
+ wgtButton(row, "&Finish")->onClick = onFinish;
+
+ // Uninstall
+ p = sSetup.pages[PageUninstallE];
+ sSetup.unText = wgtLabel(p, "");
+ sSetup.removeSettings = wgtCheckbox(p, "Also remove my &settings (the CONFIG folder)");
+ wgtLabel(p, "Files you added to the folder yourself are not removed.");
+ row = buttonRow(p);
+ sSetup.unBackBtn = wgtButton(row, "< &Back");
+ sSetup.unBackBtn->onClick = onBack;
+ wgtButton(row, "&Uninstall")->onClick = onUninstall;
+ wgtButton(row, "E&xit")->onClick = onCancel;
+}
+
+
+// A right-aligned row of buttons at the bottom of a page.
+static WidgetT *buttonRow(WidgetT *page) {
+ WidgetT *spacer = wgtVBox(page);
+ WidgetT *row = wgtHBox(page);
+
+ spacer->weight = WGT_WEIGHT_FILL;
+ row->align = AlignEndE;
+ row->spacing = wgtPixels(SETUP_SPACING);
+ return row;
+}
+
+
+// Show the exact line, ask, then back up AUTOEXEC.BAT and add it.
+static void installAutoexec(const char *destDir) {
+ char path[DVX_MAX_PATH];
+ char line[DVX_MAX_PATH];
+ char bak[DVX_MAX_PATH];
+ char msg[SETUP_MSG_MAX];
+ char err[SETUP_ERR_MAX];
+
+ if (!setupAutoexecPath(platformBootDrive(), path, sizeof(path)) || !setupAutoexecLine(destDir, line, sizeof(line)) || !platformPathSibling(bak, sizeof(bak), path, SETUP_AUTOEXEC_BAK)) {
+ return;
+ }
+
+ if (setupAutoexecHasLine(path, line)) {
+ dvxLog("Setup: %s already starts DVX", path);
+ return;
+ }
+
+ snprintf(msg, sizeof(msg), "Setup will add this line to the end of %s, so DVX starts when the computer starts:\n\n%s\n\nThe file as it is now is saved as %s first. Add the line?", path, line, bak);
+
+ if (dvxMessageBox(&sSetup.ctx, SETUP_TITLE, msg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
+ return;
+ }
+
+ if (!setupAutoexecAdd(path, line, err, sizeof(err))) {
+ dvxErrorBox(&sSetup.ctx, SETUP_TITLE, err);
+ return;
+ }
+
+ dvxLog("Setup: added '%s' to %s", line, path);
+}
+
+
+// The folder, the free space, and on a folder that already has DVX the
+// choice between keeping and replacing the settings.
+static bool installCheck(const char *destDir, bool *upgrade, bool *keepSettings) {
+ char msg[SETUP_MSG_MAX];
+ char err[SETUP_ERR_MAX];
+ char needText[FILEOPS_SIZE_TEXT_MAX];
+ char freeText[FILEOPS_SIZE_TEXT_MAX];
+ uint64_t needed = setupSpaceNeeded(&sSetup.src.info);
+ uint64_t total = 0;
+ uint64_t avail = 0;
+
+ if (!setupCheckDest(sSetup.src.partDir, destDir, err, sizeof(err))) {
+ report(err);
+ return false;
+ }
+
+ *upgrade = setupIsInstalled(destDir);
+ *keepSettings = *upgrade;
+
+ if (*upgrade && !sSetup.autoMode) {
+ snprintf(msg, sizeof(msg), "DVX is already installed in %s.\n\nYes: upgrade or repair it and keep your settings.\nNo: replace everything, your settings too.", destDir);
+
+ int32_t answer = dvxMessageBox(&sSetup.ctx, SETUP_TITLE, msg, MB_YESNOCANCEL | MB_ICONQUESTION);
+
+ if (answer == ID_CANCEL) {
+ return false;
+ }
+
+ *keepSettings = answer == ID_YES;
+ }
+
+ // An upgrade replaces files of about the same size.
+ if (!*upgrade && platformDiskSpace(destDir, &total, &avail) && avail < needed) {
+ fileOpsFormatSize(needed, needText, sizeof(needText));
+ fileOpsFormatSize(avail, freeText, sizeof(freeText));
+ snprintf(msg, sizeof(msg), "There is not enough free space for DVX on that disk.\n\nDVX needs about %s bytes; %s bytes are free.", needText, freeText);
+ report(msg);
+ return false;
+ }
+
+ return true;
+}
+
+
+// Unpack in slices, drawing progress and seeing Cancel between them.
+// Returns how it ended; a new install that did not finish is removed.
+static PkgStepE installCopy(const char *destDir, bool keepSettings, bool newInstall) {
+ PkgJobT *job = pkgJobCreate(&sSetup.src, destDir, askDisk, keepSettings ? keepSetting : NULL, NULL);
+ PlatformTicksT slice = PLATFORM_MS_TO_TICKS(SETUP_SLICE_MS);
+ PlatformTicksT sliceStart = platformClock();
+ PkgStepE step = PkgStepErrorE;
+ char msg[SETUP_MSG_MAX];
+
+ if (!job) {
+ report("There is not enough memory to run Setup.");
+ return step;
+ }
+
+ for (step = pkgJobStep(job); step == PkgStepMoreE; step = pkgJobStep(job)) {
+ if (sSetup.cancelRequested) {
+ pkgJobCancel(job);
+ }
+
+ if (platformClock() - sliceStart >= slice) {
+ showProgress(job);
+ dvxUpdate(&sSetup.ctx);
+ sliceStart = platformClock();
+ }
+ }
+
+ showProgress(job);
+
+ if (step == PkgStepDoneE) {
+ char *const *files = pkgJobFiles(job);
+
+ if (!setupListWrite(destDir, files, (int32_t)arrlen(files))) {
+ dvxLog("Setup: could not write %s in %s", SETUP_LIST_NAME, destDir);
+ }
+ } else {
+ char *const *written = pkgJobWritten(job);
+ char *const *dirs = pkgJobCreatedDirs(job);
+
+ snprintf(msg, sizeof(msg), "%s", pkgJobMessage(job));
+
+ // Only a new install is taken away again: an upgrade has already
+ // replaced files it cannot put back.
+ if (newInstall) {
+ setupRemoveWritten(written, (int32_t)arrlen(written), dirs, (int32_t)arrlen(dirs));
+ }
+
+ if (step == PkgStepErrorE) {
+ report(msg);
+ }
+ }
+
+ pkgJobFree(job);
+ return step;
+}
+
+
+// Install clicked: check, unpack, settings, AUTOEXEC.BAT, done.
+static void installRun(void) {
+ char destDir[DVX_MAX_PATH];
+ char msg[SETUP_MSG_MAX];
+ bool upgrade = false;
+ bool keepSettings = false;
+ struct stat st;
+
+ dvxCopyTrimmed(destDir, sizeof(destDir), sSetup.autoMode ? sSetup.autoDest : wgtGetText(sSetup.dest));
+
+ if (!installCheck(destDir, &upgrade, &keepSettings)) {
+ sSetup.exitCode = SETUP_EXIT_FAILED;
+ return;
+ }
+
+ bool madeDest = stat(destDir, &st) != 0;
+
+ if (platformMkdirRecursive(destDir) != 0) {
+ snprintf(msg, sizeof(msg), "Setup could not create the folder %s.", destDir);
+ report(msg);
+ sSetup.exitCode = SETUP_EXIT_FAILED;
+ return;
+ }
+
+ snprintf(sSetup.destDir, sizeof(sSetup.destDir), "%s", destDir);
+ sSetup.cancelRequested = false;
+ showPage(PageCopyE);
+ dvxLog("Setup: installing in %s (%s)", destDir, upgrade ? (keepSettings ? "upgrade, settings kept" : "settings replaced") : "new");
+
+ PkgStepE result = installCopy(destDir, keepSettings, !upgrade);
+
+ if (result != PkgStepDoneE) {
+ if (madeDest) {
+ rmdir(destDir);
+ }
+
+ dvxLog("Setup: %s", result == PkgStepCancelledE ? "cancelled" : "failed");
+ sSetup.exitCode = SETUP_EXIT_FAILED;
+ showPage(PageFolderE);
+
+ if (result == PkgStepCancelledE && !sSetup.quit && !sSetup.autoMode) {
+ snprintf(msg, sizeof(msg), upgrade ? "Setup was cancelled. DVX in %s is incomplete now; run Setup again to repair it." : "Setup was cancelled. Nothing was installed in %s.", destDir);
+ dvxInfoBox(&sSetup.ctx, SETUP_TITLE, msg);
+ }
+
+ return;
+ }
+
+ // The settings are new: start in the mode Setup runs in (it works here).
+ if (!keepSettings && !setupWriteIni(destDir, sSetup.ctx.display.width, sSetup.ctx.display.height, sSetup.ctx.display.format.bitsPerPixel)) {
+ snprintf(msg, sizeof(msg), "DVX was installed, but the settings file in %s could not be written.", destDir);
+ report(msg);
+ }
+
+ if (sSetup.autoexec && wgtCheckboxIsChecked(sSetup.autoexec)) {
+ installAutoexec(destDir);
+ }
+
+ dvxLog("Setup: finished");
+ sSetup.exitCode = 0;
+
+ if (sSetup.autoMode) {
+ sSetup.quit = true;
+ return;
+ }
+
+ snprintf(msg, sizeof(msg), "DVX is installed in %s.\n\nTo start it from the DOS prompt, type %s" DVX_PATH_SEP "%s", destDir, destDir, SETUP_EXE_NAME);
+ wgtSetText(sSetup.doneText, msg);
+ showPage(PageDoneE);
+}
+
+
+static bool keepSetting(void *user, const char *relName) {
+ (void)user;
+ return setupKeepSetting(relName);
+}
+
+
+static void onAccept(WidgetT *w) {
+ wgtSetEnabled(sSetup.nextBtn, wgtCheckboxIsChecked(w));
+}
+
+
+// Back from the folder page to the licence, from Uninstall to the folder.
+static void onBack(WidgetT *w) {
+ (void)w;
+ showPage(sSetup.page == PageUninstallE ? PageFolderE : PageLicenceE);
+}
+
+
+// Exit, or Cancel during the unpacking.
+static void onCancel(WidgetT *w) {
+ (void)w;
+
+ if (sSetup.page == PageCopyE) {
+ sSetup.cancelRequested = true;
+ return;
+ }
+
+ sSetup.quit = true;
+}
+
+
+static void onClose(WindowT *win) {
+ (void)win;
+ sSetup.cancelRequested = true;
+ sSetup.quit = true;
+}
+
+
+static void onFinish(WidgetT *w) {
+ (void)w;
+ sSetup.startDvx = wgtCheckboxIsChecked(sSetup.startNow);
+ sSetup.quit = true;
+}
+
+
+static void onInstall(WidgetT *w) {
+ (void)w;
+ sSetup.installRequested = true;
+}
+
+
+static void onNext(WidgetT *w) {
+ (void)w;
+ showPage(PageFolderE);
+ wgtSetFocused(sSetup.dest);
+}
+
+
+static void onUninstall(WidgetT *w) {
+ (void)w;
+ sSetup.uninstallRequested = true;
+}
+
+
+// Uninstall... on the folder page: only for a folder with DVX in it.
+static void onUninstallAsk(WidgetT *w) {
+ char msg[SETUP_MSG_MAX];
+ char list[DVX_MAX_PATH];
+
+ (void)w;
+ dvxCopyTrimmed(sSetup.destDir, sizeof(sSetup.destDir), wgtGetText(sSetup.dest));
+
+ if (!fileOpsPathJoin(list, sizeof(list), sSetup.destDir, SETUP_LIST_NAME) || access(list, F_OK) != 0) {
+ snprintf(msg, sizeof(msg), "DVX is not installed in %s (or it was not installed by this Setup).", sSetup.destDir);
+ dvxErrorBox(&sSetup.ctx, SETUP_TITLE, msg);
+ return;
+ }
+
+ snprintf(msg, sizeof(msg), "Setup will remove DVX from %s.", sSetup.destDir);
+ wgtSetText(sSetup.unText, msg);
+ showPage(PageUninstallE);
+}
+
+
+static void parseArgs(int argc, char *argv[]) {
+ for (int32_t i = 1; i < argc; i++) {
+ if (strncasecmp(argv[i], SETUP_ARG_AUTO, strlen(SETUP_ARG_AUTO)) == 0) {
+ snprintf(sSetup.autoDest, sizeof(sSetup.autoDest), "%s", argv[i] + strlen(SETUP_ARG_AUTO));
+ sSetup.autoMode = true;
+ } else if (strncasecmp(argv[i], SETUP_ARG_LOG, strlen(SETUP_ARG_LOG)) == 0) {
+ snprintf(sSetup.logPath, sizeof(sSetup.logPath), "%s", argv[i] + strlen(SETUP_ARG_LOG));
+ }
+ }
+}
+
+
+// An error: a message box, or (unattended) the log.
+static void report(const char *msg) {
+ dvxLog("Setup: %s", msg);
+
+ if (!sSetup.autoMode) {
+ dvxErrorBox(&sSetup.ctx, SETUP_TITLE, msg);
+ }
+}
+
+
+static void showPage(PageE page) {
+ sSetup.page = page;
+
+ for (int32_t i = 0; i < PageCountE; i++) {
+ wgtSetVisible(sSetup.pages[i], i == page);
+ }
+
+ wgtInvalidate(sSetup.pages[page]);
+}
+
+
+static void showProgress(const PkgJobT *job) {
+ PkgProgressT p;
+ char line[SETUP_MSG_MAX];
+
+ pkgJobProgress(job, &p);
+
+ if (sSetup.src.info.diskCount > 1) {
+ snprintf(line, sizeof(line), "Disk %d: %s", (int)p.disk, p.current);
+ } else {
+ snprintf(line, sizeof(line), "%s", p.current);
+ }
+
+ wgtSetText(sSetup.status, line);
+ wgtProgressBarSetValue(sSetup.bar, p.percent);
+}
+
+
+// Run the installed DVX (Setup's screen is closed first; Setup's own
+// memory stays in use until DVX exits).
+static void startDvx(void) {
+ char exe[DVX_MAX_PATH];
+
+ if (!fileOpsPathJoin(exe, sizeof(exe), sSetup.destDir, SETUP_EXE_NAME) || platformChdir(sSetup.destDir) != 0) {
+ return;
+ }
+
+ dvxLog("Setup: starting %s", exe);
+ spawnl(P_WAIT, exe, exe, (char *)NULL);
+}
+
+
+// Uninstall confirmed: take the AUTOEXEC.BAT line out (asking), then
+// remove the files.
+static void uninstallRun(void) {
+ char path[DVX_MAX_PATH];
+ char line[DVX_MAX_PATH];
+ char msg[SETUP_MSG_MAX];
+ char err[SETUP_ERR_MAX];
+ bool removeSettings = wgtCheckboxIsChecked(sSetup.removeSettings);
+ char root[] = { sSetup.destDir[0], ':', DVX_PATH_SEP[0], '\0' };
+
+ if (setupAutoexecPath(platformBootDrive(), path, sizeof(path)) && setupAutoexecLine(sSetup.destDir, line, sizeof(line)) && setupAutoexecHasLine(path, line)) {
+ snprintf(msg, sizeof(msg), "%s starts DVX with the line\n\n%s\n\nRemove that line? (The file as it is now is saved as AUTOEXEC.DVX first.)", path, line);
+
+ if (dvxMessageBox(&sSetup.ctx, SETUP_TITLE, msg, MB_YESNO | MB_ICONQUESTION) == ID_YES && !setupAutoexecRemove(path, line, err, sizeof(err))) {
+ dvxErrorBox(&sSetup.ctx, SETUP_TITLE, err);
+ }
+ }
+
+ // The folder being removed must not be the current one.
+ platformChdir(root);
+
+ dvxSetBusy(&sSetup.ctx, true);
+ bool ok = setupUninstall(sSetup.destDir, removeSettings, err, sizeof(err));
+ dvxSetBusy(&sSetup.ctx, false);
+ dvxLog("Setup: uninstall of %s %s", sSetup.destDir, ok ? "done" : "incomplete");
+
+ if (!ok) {
+ dvxErrorBox(&sSetup.ctx, SETUP_TITLE, err);
+ return;
+ }
+
+ snprintf(msg, sizeof(msg), "DVX was removed from %s.", sSetup.destDir);
+ dvxInfoBox(&sSetup.ctx, SETUP_TITLE, msg);
+ sSetup.quit = true;
+}
+
+
+int main(int argc, char *argv[]) {
+ memset(&sSetup, 0, sizeof(sSetup));
+ snprintf(sSetup.exePath, sizeof(sSetup.exePath), "%s", argv[0]);
+ parseArgs(argc, argv);
+
+ // Never a log file on the disk Setup runs from.
+ if (sSetup.logPath[0]) {
+ platformSetLogPath(sSetup.logPath);
+ } else {
+ platformSetLogMemory();
+ }
+
+ platformInit();
+ wgtRegisterBox();
+ wgtRegisterButton();
+ wgtRegisterCheckbox();
+ wgtRegisterLabel();
+ wgtRegisterProgress();
+ wgtRegisterTextInput();
+ pkgFind(sSetup.exePath, &sSetup.src);
+ dvxLog("Setup: %s, package %s (%d disk(s), %d files)", sSetup.exePath, sFoundNames[sSetup.src.found], (int)sSetup.src.info.diskCount, (int)sSetup.src.info.fileCount);
+
+ // Without a package Setup can only uninstall the DVX in its own folder.
+ char list[DVX_MAX_PATH];
+ bool listed = fileOpsPathJoin(list, sizeof(list), sSetup.src.partDir, SETUP_LIST_NAME) && access(list, F_OK) == 0;
+
+ if (sSetup.src.found == PkgFoundDamagedE || (sSetup.src.found == PkgFoundNoneE && (sSetup.autoMode || !listed))) {
+ fprintf(stderr, "DVX Setup: there is no DVX package (or it is damaged) beside %s, and DVX is not installed in its folder.\n", sSetup.exePath);
+ return SETUP_EXIT_FAILED;
+ }
+
+ if (dvxInit(&sSetup.ctx, DVX_DEFAULT_VIDEO_W, DVX_DEFAULT_VIDEO_H, DVX_DEFAULT_VIDEO_BPP) != 0) {
+ fprintf(stderr, "DVX Setup needs a VESA 2.0 video card (see README.TXT).\n");
+ return SETUP_EXIT_FAILED;
+ }
+
+ sSetup.win = dvxCreateWindowCentered(&sSetup.ctx, SETUP_TITLE, SETUP_WIN_W, SETUP_WIN_H, false);
+
+ if (!sSetup.win) {
+ dvxShutdown(&sSetup.ctx);
+ return SETUP_EXIT_FAILED;
+ }
+
+ sSetup.win->onClose = onClose;
+
+ WidgetT *root = wgtInitWindow(&sSetup.ctx, sSetup.win);
+
+ buildPages(root);
+
+ // No package: this is the Setup in a DVX folder, which can only
+ // remove DVX from its own folder.
+ if (sSetup.src.found == PkgFoundNoneE) {
+ char msg[SETUP_MSG_MAX];
+
+ snprintf(sSetup.destDir, sizeof(sSetup.destDir), "%s", sSetup.src.partDir);
+ snprintf(msg, sizeof(msg), "Setup will remove DVX from %s. To install or repair DVX, run SETUP from the DVX CD or Disk 1.", sSetup.destDir);
+ wgtSetText(sSetup.unText, msg);
+ wgtSetVisible(sSetup.unBackBtn, false);
+ showPage(PageUninstallE);
+ } else {
+ showPage(PageLicenceE);
+ }
+
+ sSetup.installRequested = sSetup.autoMode;
+
+ while (!sSetup.quit && dvxUpdate(&sSetup.ctx)) {
+ if (sSetup.installRequested) {
+ sSetup.installRequested = false;
+ installRun();
+ sSetup.quit = sSetup.quit || sSetup.autoMode;
+ }
+
+ if (sSetup.uninstallRequested) {
+ sSetup.uninstallRequested = false;
+ uninstallRun();
+ }
+ }
+
+ dvxShutdown(&sSetup.ctx);
+
+ if (sSetup.startDvx) {
+ startDvx();
+ }
+
+ return sSetup.exitCode;
+}
diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c
new file mode 100644
index 00000000..f06a8327
--- /dev/null
+++ b/src/installer/setupCore.c
@@ -0,0 +1,534 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// setupCore.c -- Setup's work without its window (see setupCore.h)
+
+#include "setupCore.h"
+#include "dvxPlat.h"
+#include "dvxPrefs.h"
+#include "fileops/fileOps.h"
+#include "stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "dvxMem.h"
+
+// Line ending written into AUTOEXEC.BAT and SETUP.LST
+#define SETUP_EOL "\r\n"
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool autoexecRewrite(const char *path, const char *text, int32_t len, char *err, int32_t errSize);
+static int byLengthDesc(const void *a, const void *b);
+static bool lineMatches(char *l, const char *line);
+static void listFree(char **names);
+static bool listNameIsValid(const char *name);
+static char **listRead(const char *destDir);
+static bool removeFile(const char *path);
+bool setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize);
+bool setupAutoexecHasLine(const char *path, const char *line);
+bool setupAutoexecLine(const char *destDir, char *out, int32_t outSize);
+bool setupAutoexecPath(char bootDrive, char *out, int32_t outSize);
+bool setupAutoexecRemove(const char *path, const char *line, char *err, int32_t errSize);
+bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
+bool setupIsInstalled(const char *destDir);
+bool setupKeepSetting(const char *relName);
+bool setupListWrite(const char *destDir, char *const *files, int32_t count);
+void setupRemoveWritten(char *const *written, int32_t writtenCount, char *const *dirs, int32_t dirCount);
+uint64_t setupSpaceNeeded(const PkgHeaderT *info);
+bool setupUninstall(const char *destDir, bool removeSettings, char *err, int32_t errSize);
+bool setupWriteIni(const char *destDir, int32_t width, int32_t height, int32_t bpp);
+static bool writeAll(FILE *f, const char *data, int32_t len);
+static bool writeReplace(const char *path, const char *text, int32_t len);
+
+
+// Back up path (when it exists) as its SETUP_AUTOEXEC_BAK sibling, then
+// replace it with text through a scratch file.
+static bool autoexecRewrite(const char *path, const char *text, int32_t len, char *err, int32_t errSize) {
+ char bak[DVX_MAX_PATH];
+ struct stat st;
+
+ if (!platformPathSibling(bak, sizeof(bak), path, SETUP_AUTOEXEC_BAK)) {
+ snprintf(err, errSize, "The path %s is too long.", path);
+ return false;
+ }
+
+ if (stat(path, &st) == 0 && !platformCopyFile(path, bak)) {
+ snprintf(err, errSize, "Could not make the backup copy %s, so %s was not changed.", bak, path);
+ return false;
+ }
+
+ if (!writeReplace(path, text, len)) {
+ snprintf(err, errSize, "Could not write %s (is the disk full or write-protected?).", path);
+ return false;
+ }
+
+ return true;
+}
+
+
+// Longest path first, so a folder comes after everything inside it.
+static int byLengthDesc(const void *a, const void *b) {
+ size_t la = strlen(*(char *const *)a);
+ size_t lb = strlen(*(char *const *)b);
+
+ return (la < lb) - (la > lb);
+}
+
+
+// A line of a batch file (trimmed in place) is line, case ignored.
+static bool lineMatches(char *l, const char *line) {
+ dvxTrimRight(l);
+ return strcasecmp(dvxSkipWs(l), line) == 0;
+}
+
+
+static void listFree(char **names) {
+ for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
+ free(names[i]);
+ }
+
+ arrfree(names);
+}
+
+
+// A SETUP.LST line: a package name, or a folder's with a '/' after it.
+static bool listNameIsValid(const char *name) {
+ char bare[PKG_NAME_MAX];
+ int32_t len = (int32_t)strlen(name);
+
+ if (len > 0 && name[len - 1] == PKG_NAME_SEP) {
+ len--;
+ }
+
+ snprintf(bare, sizeof(bare), "%.*s", (int)len, name);
+ return pkgNameIsValid(bare);
+}
+
+
+// The names in destDir's SETUP.LST (stb_ds array; NULL when there is no
+// list).
+static char **listRead(const char *destDir) {
+ char path[DVX_MAX_PATH];
+
+ if (!fileOpsPathJoin(path, sizeof(path), destDir, SETUP_LIST_NAME)) {
+ return NULL;
+ }
+
+ char *text = platformReadFile(path, NULL);
+
+ if (!text) {
+ return NULL;
+ }
+
+ char **names = NULL;
+
+ for (char *l = text; l; ) {
+ char *next = strchr(l, '\n');
+
+ if (next) {
+ *next++ = '\0';
+ }
+
+ dvxTrimRight(l);
+
+ if (listNameIsValid(l)) {
+ arrput(names, strdup(l));
+ }
+
+ l = next;
+ }
+
+ free(text);
+
+ // An empty list is still a list.
+ if (!names) {
+ arrsetcap(names, 1);
+ }
+
+ return names;
+}
+
+
+// Remove a file, read-only or not. A file that is not there counts as
+// removed.
+static bool removeFile(const char *path) {
+ if (remove(path) == 0 || errno == ENOENT) {
+ return true;
+ }
+
+ return fileOpsSetAttr(path, 0) && remove(path) == 0;
+}
+
+
+bool setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize) {
+ int32_t len = 0;
+ char *old = platformReadFile(path, &len);
+ char *buf = (char *)malloc((size_t)len + strlen(line) + 2 * strlen(SETUP_EOL) + 1);
+
+ if (!buf) {
+ free(old);
+ snprintf(err, errSize, "There is not enough memory to change %s.", path);
+ return false;
+ }
+
+ int32_t n = 0;
+
+ if (old) {
+ memcpy(buf, old, (size_t)len);
+ n = len;
+
+ // A last line without its line break would run into the new one.
+ if (len > 0 && old[len - 1] != '\n') {
+ n += sprintf(buf + n, "%s", SETUP_EOL);
+ }
+ }
+
+ n += sprintf(buf + n, "%s%s", line, SETUP_EOL);
+
+ bool ok = autoexecRewrite(path, buf, n, err, errSize);
+
+ free(buf);
+ free(old);
+ return ok;
+}
+
+
+bool setupAutoexecHasLine(const char *path, const char *line) {
+ char *text = platformReadFile(path, NULL);
+ bool has = false;
+
+ if (!text) {
+ return false;
+ }
+
+ for (char *l = text; l && !has; ) {
+ char *next = strchr(l, '\n');
+
+ if (next) {
+ *next++ = '\0';
+ }
+
+ has = lineMatches(l, line);
+ l = next;
+ }
+
+ free(text);
+ return has;
+}
+
+
+bool setupAutoexecLine(const char *destDir, char *out, int32_t outSize) {
+ return fileOpsPathJoin(out, outSize, destDir, SETUP_EXE_NAME);
+}
+
+
+bool setupAutoexecPath(char bootDrive, char *out, int32_t outSize) {
+ if (!bootDrive) {
+ return false;
+ }
+
+ int32_t n = snprintf(out, outSize, "%c:" DVX_PATH_SEP SETUP_AUTOEXEC_NAME, bootDrive);
+
+ return n > 0 && n < outSize;
+}
+
+
+// Lines are copied with their own endings; a matching line is left out.
+bool setupAutoexecRemove(const char *path, const char *line, char *err, int32_t errSize) {
+ int32_t len = 0;
+ char *text = platformReadFile(path, &len);
+
+ if (!text) {
+ return true;
+ }
+
+ char *buf = (char *)malloc((size_t)len + 1);
+ int32_t n = 0;
+ bool removed = false;
+
+ if (!buf) {
+ free(text);
+ snprintf(err, errSize, "There is not enough memory to change %s.", path);
+ return false;
+ }
+
+ for (int32_t start = 0; start < len; ) {
+ int32_t end = start;
+
+ while (end < len && text[end] != '\n') {
+ end++;
+ }
+
+ if (end < len) {
+ end++;
+ }
+
+ char one[DVX_MAX_PATH];
+
+ snprintf(one, sizeof(one), "%.*s", (int)(end - start), text + start);
+
+ if (lineMatches(one, line)) {
+ removed = true;
+ } else {
+ memcpy(buf + n, text + start, (size_t)(end - start));
+ n += end - start;
+ }
+
+ start = end;
+ }
+
+ bool ok = !removed || autoexecRewrite(path, buf, n, err, errSize);
+
+ free(buf);
+ free(text);
+ return ok;
+}
+
+
+bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize) {
+ if (!destDir[0]) {
+ snprintf(err, errSize, "Type the folder to install DVX in.");
+ return false;
+ }
+
+ if (!platformPathIsAbsolute(destDir)) {
+ snprintf(err, errSize, "Type the whole path of the folder, starting with the drive, for example C:\\DVX.");
+ return false;
+ }
+
+ if (fileOpsPathIsInside(destDir, srcDir)) {
+ snprintf(err, errSize, "Setup is reading DVX from %s. Choose a folder that is not this one and not inside it.", srcDir);
+ return false;
+ }
+
+ return true;
+}
+
+
+bool setupIsInstalled(const char *destDir) {
+ char path[DVX_MAX_PATH];
+ struct stat st;
+
+ return fileOpsPathJoin(path, sizeof(path), destDir, DVX_INI_PATH) && stat(path, &st) == 0 && S_ISREG(st.st_mode);
+}
+
+
+bool setupKeepSetting(const char *relName) {
+ size_t len = strlen(DVX_CONFIG_DIR);
+
+ return strncasecmp(relName, DVX_CONFIG_DIR, len) == 0 && relName[len] == PKG_NAME_SEP;
+}
+
+
+bool setupListWrite(const char *destDir, char *const *files, int32_t count) {
+ char path[DVX_MAX_PATH];
+ char **old = listRead(destDir);
+ char *text = NULL;
+ bool ok = fileOpsPathJoin(path, sizeof(path), destDir, SETUP_LIST_NAME);
+
+ for (int32_t i = 0; i < count; i++) {
+ for (const char *p = files[i]; *p; p++) {
+ arrput(text, *p);
+ }
+
+ for (const char *p = SETUP_EOL; *p; p++) {
+ arrput(text, *p);
+ }
+ }
+
+ // Old names not in this package whose files are still there (a file
+ // an older version installed) stay listed, so Uninstall removes them.
+ for (int32_t i = 0; i < (int32_t)arrlen(old); i++) {
+ char full[DVX_MAX_PATH];
+ struct stat st;
+ bool listed = false;
+
+ for (int32_t j = 0; j < count && !listed; j++) {
+ listed = strcasecmp(files[j], old[i]) == 0;
+ }
+
+ if (listed || !pkgLocalPath(destDir, old[i], full, sizeof(full)) || stat(full, &st) != 0) {
+ continue;
+ }
+
+ for (const char *p = old[i]; *p; p++) {
+ arrput(text, *p);
+ }
+
+ for (const char *p = SETUP_EOL; *p; p++) {
+ arrput(text, *p);
+ }
+ }
+
+ ok = ok && writeReplace(path, text ? text : "", (int32_t)arrlen(text));
+ arrfree(text);
+ listFree(old);
+ return ok;
+}
+
+
+void setupRemoveWritten(char *const *written, int32_t writtenCount, char *const *dirs, int32_t dirCount) {
+ for (int32_t i = 0; i < writtenCount; i++) {
+ removeFile(written[i]);
+ }
+
+ for (int32_t i = dirCount - 1; i >= 0; i--) {
+ rmdir(dirs[i]);
+ }
+}
+
+
+uint64_t setupSpaceNeeded(const PkgHeaderT *info) {
+ return info->unpackedBytes + (uint64_t)info->fileCount * SETUP_FILE_SLACK;
+}
+
+
+bool setupUninstall(const char *destDir, bool removeSettings, char *err, int32_t errSize) {
+ char **names = listRead(destDir);
+ char **dirs = NULL;
+ char path[DVX_MAX_PATH];
+ int32_t failed = 0;
+
+ if (!names) {
+ snprintf(err, errSize, "%s has no list of installed files (%s), so Setup does not know what to remove.", destDir, SETUP_LIST_NAME);
+ return false;
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
+ if (!pkgLocalPath(destDir, names[i], path, sizeof(path))) {
+ continue;
+ }
+
+ // A folder of the package: removed with the others when empty.
+ size_t len = strlen(path);
+
+ if (path[len - 1] == DVX_PATH_SEP[0]) {
+ path[len - 1] = '\0';
+ arrput(dirs, strdup(path));
+ continue;
+ }
+
+ // Every folder the name is in, for the empty-folder pass.
+ for (char *sep = strrchr(path, DVX_PATH_SEP[0]); sep && sep > path + strlen(destDir); sep = strrchr(path, DVX_PATH_SEP[0])) {
+ *sep = '\0';
+ arrput(dirs, strdup(path));
+ }
+
+ if ((!removeSettings && setupKeepSetting(names[i])) || !pkgLocalPath(destDir, names[i], path, sizeof(path))) {
+ continue;
+ }
+
+ if (!removeFile(path)) {
+ failed++;
+ }
+ }
+
+ if (removeSettings && fileOpsPathJoin(path, sizeof(path), destDir, DVX_CONFIG_DIR)) {
+ FileOpsDeleteTree(path);
+ }
+
+ if (fileOpsPathJoin(path, sizeof(path), destDir, DVX_LOG_FILE)) {
+ removeFile(path);
+ }
+
+ if (fileOpsPathJoin(path, sizeof(path), destDir, SETUP_LIST_NAME) && failed == 0) {
+ removeFile(path);
+ }
+
+ qsort(dirs, (size_t)arrlen(dirs), sizeof(char *), byLengthDesc);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(dirs); i++) {
+ rmdir(dirs[i]);
+ }
+
+ rmdir(destDir);
+ listFree(dirs);
+ listFree(names);
+
+ if (failed > 0) {
+ snprintf(err, errSize, "%d file(s) in %s could not be removed. Close any program using them and run Uninstall again.", (int)failed, destDir);
+ return false;
+ }
+
+ return true;
+}
+
+
+bool setupWriteIni(const char *destDir, int32_t width, int32_t height, int32_t bpp) {
+ char path[DVX_MAX_PATH];
+
+ if (!fileOpsPathJoin(path, sizeof(path), destDir, DVX_INI_PATH)) {
+ return false;
+ }
+
+ PrefsHandleT *ini = prefsLoad(path);
+
+ if (!ini) {
+ return false;
+ }
+
+ prefsSetVideoMode(ini, width, height, bpp);
+
+ bool ok = prefsSave(ini);
+
+ prefsClose(ini);
+ return ok;
+}
+
+
+static bool writeAll(FILE *f, const char *data, int32_t len) {
+ return (int32_t)fwrite(data, 1, (size_t)len, f) == len;
+}
+
+
+// Write text to a scratch file and swap it in for path (so a full disk
+// never leaves a cut-off file).
+static bool writeReplace(const char *path, const char *text, int32_t len) {
+ char tmp[DVX_MAX_PATH];
+ char swap[DVX_MAX_PATH];
+
+ if (!platformPathSibling(tmp, sizeof(tmp), path, PLATFORM_TMP_EXT) || !platformPathSibling(swap, sizeof(swap), path, PLATFORM_BAK_EXT)) {
+ return false;
+ }
+
+ FILE *f = fopen(tmp, "wb");
+ bool ok = f != NULL && writeAll(f, text, len);
+
+ if (f && fclose(f) != 0) {
+ ok = false;
+ }
+
+ if (!ok) {
+ remove(tmp);
+ return false;
+ }
+
+ return platformReplaceFile(tmp, path, swap);
+}
diff --git a/src/installer/setupCore.h b/src/installer/setupCore.h
new file mode 100644
index 00000000..85acc40d
--- /dev/null
+++ b/src/installer/setupCore.h
@@ -0,0 +1,119 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// setupCore.h -- Setup's work without its window: the target folder,
+// the list of installed files, removing DVX, DVX.INI and AUTOEXEC.BAT
+// (host tested)
+//
+// SETUP.EXE unpacks the DVX package (pkgRead.h) into the target folder.
+// On a folder that already holds DVX (its CONFIG\DVX.INI exists) an
+// upgrade keeps the files under CONFIG, so the user's settings, groups
+// and file types survive. Every file the package put there is listed in
+// SETUP.LST in the folder; Uninstall removes those files (and CONFIG when
+// asked), then the folders left empty. A new install gets DVX.INI with
+// the display mode Setup ran in (known to work on this computer).
+
+#ifndef SETUP_CORE_H
+#define SETUP_CORE_H
+
+#include "pkgFormat.h"
+
+#include
+#include
+
+// The program AUTOEXEC.BAT starts, in the target folder
+#define SETUP_EXE_NAME "DVX.EXE"
+
+// AUTOEXEC.BAT in the root of the boot drive, and the copy of it Setup
+// keeps before changing it (AUTOEXEC.DVX)
+#define SETUP_AUTOEXEC_NAME "AUTOEXEC.BAT"
+#define SETUP_AUTOEXEC_BAK ".DVX"
+
+// The list of installed files, in the DVX folder (one relative name with
+// '/' per line)
+#define SETUP_LIST_NAME "SETUP.LST"
+
+// Room a file takes beyond its size on the target disk (half a 16 KB
+// cluster, typical of a FAT16 hard disk), for the free space check
+#define SETUP_FILE_SLACK 8192
+
+// Longest message the functions below return
+#define SETUP_ERR_MAX 320
+
+// Add line (with a CR LF) to the end of the batch file at path, after
+// copying it to its SETUP_AUTOEXEC_BAK sibling. A missing file is
+// created (nothing to back up). The change is written to a scratch file
+// and swapped in, so a failure leaves the file as it was. False with a
+// reason in err.
+bool setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize);
+
+// True when the batch file at path already has line (blanks around it and
+// case ignored).
+bool setupAutoexecHasLine(const char *path, const char *line);
+
+// The AUTOEXEC.BAT line that starts DVX from destDir: destDir\DVX.EXE (the
+// loader changes to its own folder first). False when it does not fit.
+bool setupAutoexecLine(const char *destDir, char *out, int32_t outSize);
+
+// Path of AUTOEXEC.BAT on the boot drive ("C:\AUTOEXEC.BAT"). False when
+// there is no boot drive (0).
+bool setupAutoexecPath(char bootDrive, char *out, int32_t outSize);
+
+// Take every copy of line out of the batch file at path (backed up and
+// swapped in like setupAutoexecAdd). True when there was nothing to
+// remove; false with a reason in err.
+bool setupAutoexecRemove(const char *path, const char *line, char *err, int32_t errSize);
+
+// Check a target folder: given, a full path, and neither the folder the
+// package is read from nor inside it. False with a reason in err.
+bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
+
+// True when destDir already holds a DVX with settings (CONFIG\DVX.INI).
+bool setupIsInstalled(const char *destDir);
+
+// An upgrade keeps an existing file when it is a setting (under CONFIG).
+bool setupKeepSetting(const char *relName);
+
+// Write destDir's SETUP.LST: the names in files (relative, '/') plus
+// those of the old list whose files are still there. False when it
+// cannot be written.
+bool setupListWrite(const char *destDir, char *const *files, int32_t count);
+
+// Remove what a cancelled new install wrote: the files, then the folders
+// it made (newest first) when they are empty.
+void setupRemoveWritten(char *const *written, int32_t writtenCount, char *const *dirs, int32_t dirCount);
+
+// Bytes the package needs on the target disk.
+uint64_t setupSpaceNeeded(const PkgHeaderT *info);
+
+// Remove DVX from destDir: every file in SETUP.LST (except the settings
+// under CONFIG unless removeSettings), the log, the list itself, then
+// every folder left empty, destDir too. Files the user added stay.
+// False with a reason in err when the list is missing or a file could
+// not be removed (the rest is still removed).
+bool setupUninstall(const char *destDir, bool removeSettings, char *err, int32_t errSize);
+
+// Store the display mode in destDir's CONFIG\DVX.INI. False when it
+// cannot be saved.
+bool setupWriteIni(const char *destDir, int32_t width, int32_t height, int32_t bpp);
+
+#endif // SETUP_CORE_H
diff --git a/src/installer/thirdparty/miniz/DVX-VERSION.md b/src/installer/thirdparty/miniz/DVX-VERSION.md
new file mode 100644
index 00000000..95081b90
--- /dev/null
+++ b/src/installer/thirdparty/miniz/DVX-VERSION.md
@@ -0,0 +1,34 @@
+# miniz for DVX
+
+| Item | Value |
+|---|---|
+| Project | miniz, a single-file deflate/inflate, zlib-subset and ZIP library, https://github.com/richgel999/miniz |
+| Version | 3.1.2 |
+| Download | https://github.com/richgel999/miniz/releases/download/3.1.2/miniz-3.1.2.zip |
+| Zip SHA-256 | f0446d863f9c19926ad9483c523fdc42e42b8d4a6a431d27e09d49c79a140d9a (matches the digest GitHub publishes for the release asset) |
+| Files kept | miniz.c, miniz.h, LICENSE -- byte for byte as in the zip. Not kept: examples/, ChangeLog.md, readme.md. |
+| Licence | MIT (LICENSE: Rich Geldreich, RAD Game Tools, Valve Software). Same licence as DVX; keep the notice with the source. |
+| Patches | None |
+
+## Where it is used
+
+- `bin/host/dvxpkg` (src/installer/pkgTool.c, host build): the full
+ library -- the ZIP writer and deflate (level 10, the best miniz has)
+ build the DVX package.
+- `SETUP.EXE` (src/installer, DOS): only inflate (`tinfl_decompress`) and
+ `mz_crc32`, built with `MINIZ_NO_ARCHIVE_APIS MINIZ_NO_STDIO
+ MINIZ_NO_TIME MINIZ_NO_DEFLATE_APIS MINIZ_NO_ZLIB_COMPATIBLE_NAMES`.
+ Setup reads the ZIP's local headers as a stream (pkgRead.c), so the
+ package can run across floppy disks without seeking back.
+- Host tests (src/test/setup): both builds.
+
+miniz compiles with the normal DVX warning flags (`-Wall -Wextra
+-Werror`) for DJGPP and the host; no flags are relaxed for it.
+
+## Updating
+
+1. Download the new release zip, check its SHA-256 against the digest on
+ the GitHub release page, record the new version and hash here.
+2. Replace miniz.c, miniz.h and LICENSE, nothing else.
+3. Rebuild (`make installer`), run `make -C src/test/setup`, `make dist`
+ and the DOS script `install`.
diff --git a/src/installer/thirdparty/miniz/LICENSE b/src/installer/thirdparty/miniz/LICENSE
new file mode 100644
index 00000000..1982f4bb
--- /dev/null
+++ b/src/installer/thirdparty/miniz/LICENSE
@@ -0,0 +1,22 @@
+Copyright 2013-2014 RAD Game Tools and Valve Software
+Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
+
+All Rights Reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/installer/thirdparty/miniz/miniz.c b/src/installer/thirdparty/miniz/miniz.c
new file mode 100644
index 00000000..54fc5f39
--- /dev/null
+++ b/src/installer/thirdparty/miniz/miniz.c
@@ -0,0 +1,7922 @@
+#include "miniz.h"
+/**************************************************************************
+ *
+ * Copyright 2013-2014 RAD Game Tools and Valve Software
+ * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
+ * All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ **************************************************************************/
+
+
+
+typedef unsigned char mz_validate_uint16[sizeof(mz_uint16) == 2 ? 1 : -1];
+typedef unsigned char mz_validate_uint32[sizeof(mz_uint32) == 4 ? 1 : -1];
+typedef unsigned char mz_validate_uint64[sizeof(mz_uint64) == 8 ? 1 : -1];
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ /* ------------------- zlib-style API's */
+
+ mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len)
+ {
+ mz_uint32 i, s1 = (mz_uint32)(adler & 0xffff), s2 = (mz_uint32)(adler >> 16);
+ size_t block_len = buf_len % 5552;
+ if (!ptr)
+ return MZ_ADLER32_INIT;
+ while (buf_len)
+ {
+ for (i = 0; i + 7 < block_len; i += 8, ptr += 8)
+ {
+ s1 += ptr[0], s2 += s1;
+ s1 += ptr[1], s2 += s1;
+ s1 += ptr[2], s2 += s1;
+ s1 += ptr[3], s2 += s1;
+ s1 += ptr[4], s2 += s1;
+ s1 += ptr[5], s2 += s1;
+ s1 += ptr[6], s2 += s1;
+ s1 += ptr[7], s2 += s1;
+ }
+ for (; i < block_len; ++i)
+ s1 += *ptr++, s2 += s1;
+ s1 %= 65521U, s2 %= 65521U;
+ buf_len -= block_len;
+ block_len = 5552;
+ }
+ return (s2 << 16) + s1;
+ }
+
+/* Karl Malbrain's compact CRC-32. See "A compact CCITT crc16 and crc32 C implementation that balances processor cache usage against speed": http://www.geocities.com/malbrain/ */
+#if 0
+ mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len)
+ {
+ static const mz_uint32 s_crc32[16] = { 0, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
+ 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c };
+ mz_uint32 crcu32 = (mz_uint32)crc;
+ if (!ptr)
+ return MZ_CRC32_INIT;
+ crcu32 = ~crcu32;
+ while (buf_len--)
+ {
+ mz_uint8 b = *ptr++;
+ crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b & 0xF)];
+ crcu32 = (crcu32 >> 4) ^ s_crc32[(crcu32 & 0xF) ^ (b >> 4)];
+ }
+ return ~crcu32;
+ }
+#elif defined(USE_EXTERNAL_MZCRC)
+/* If USE_EXTERNAL_CRC is defined, an external module will export the
+ * mz_crc32() symbol for us to use, e.g. an SSE-accelerated version.
+ * Depending on the impl, it may be necessary to ~ the input/output crc values.
+ */
+mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len);
+#else
+/* Faster, but larger CPU cache footprint.
+ */
+mz_ulong mz_crc32(mz_ulong crc, const mz_uint8 *ptr, size_t buf_len)
+{
+ static const mz_uint32 s_crc_table[256] = {
+ 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535,
+ 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD,
+ 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D,
+ 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
+ 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4,
+ 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C,
+ 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC,
+ 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
+ 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB,
+ 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F,
+ 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB,
+ 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
+ 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA,
+ 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE,
+ 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A,
+ 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
+ 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409,
+ 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81,
+ 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739,
+ 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
+ 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268,
+ 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0,
+ 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8,
+ 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
+ 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF,
+ 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703,
+ 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7,
+ 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
+ 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE,
+ 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
+ 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6,
+ 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
+ 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D,
+ 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5,
+ 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605,
+ 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
+ 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
+ };
+
+ mz_uint32 crc32 = (mz_uint32)crc ^ 0xFFFFFFFF;
+ const mz_uint8 *pByte_buf = (const mz_uint8 *)ptr;
+
+ while (buf_len >= 4)
+ {
+ crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF];
+ crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[1]) & 0xFF];
+ crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[2]) & 0xFF];
+ crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[3]) & 0xFF];
+ pByte_buf += 4;
+ buf_len -= 4;
+ }
+
+ while (buf_len)
+ {
+ crc32 = (crc32 >> 8) ^ s_crc_table[(crc32 ^ pByte_buf[0]) & 0xFF];
+ ++pByte_buf;
+ --buf_len;
+ }
+
+ return ~crc32;
+}
+#endif
+
+ void mz_free(void *p)
+ {
+ MZ_FREE(p);
+ }
+
+ MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size)
+ {
+ (void)opaque, (void)items, (void)size;
+ return MZ_MALLOC(items * size);
+ }
+ MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address)
+ {
+ (void)opaque, (void)address;
+ MZ_FREE(address);
+ }
+ MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size)
+ {
+ (void)opaque, (void)address, (void)items, (void)size;
+ return MZ_REALLOC(address, items * size);
+ }
+
+ const char *mz_version(void)
+ {
+ return MZ_VERSION;
+ }
+
+#ifndef MINIZ_NO_ZLIB_APIS
+
+#ifndef MINIZ_NO_DEFLATE_APIS
+
+ int mz_deflateInit(mz_streamp pStream, int level)
+ {
+ return mz_deflateInit2(pStream, level, MZ_DEFLATED, MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY);
+ }
+
+ int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy)
+ {
+ tdefl_compressor *pComp;
+ mz_uint comp_flags = TDEFL_COMPUTE_ADLER32 | tdefl_create_comp_flags_from_zip_params(level, window_bits, strategy);
+
+ if (!pStream)
+ return MZ_STREAM_ERROR;
+ if ((method != MZ_DEFLATED) || ((mem_level < 1) || (mem_level > 9)) || ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS)))
+ return MZ_PARAM_ERROR;
+
+ pStream->data_type = 0;
+ pStream->adler = MZ_ADLER32_INIT;
+ pStream->msg = NULL;
+ pStream->reserved = 0;
+ pStream->total_in = 0;
+ pStream->total_out = 0;
+ if (!pStream->zalloc)
+ pStream->zalloc = miniz_def_alloc_func;
+ if (!pStream->zfree)
+ pStream->zfree = miniz_def_free_func;
+
+ pComp = (tdefl_compressor *)pStream->zalloc(pStream->opaque, 1, sizeof(tdefl_compressor));
+ if (!pComp)
+ return MZ_MEM_ERROR;
+
+ pStream->state = (struct mz_internal_state *)pComp;
+
+ if (tdefl_init(pComp, NULL, NULL, comp_flags) != TDEFL_STATUS_OKAY)
+ {
+ mz_deflateEnd(pStream);
+ return MZ_PARAM_ERROR;
+ }
+
+ return MZ_OK;
+ }
+
+ int mz_deflateReset(mz_streamp pStream)
+ {
+ if ((!pStream) || (!pStream->state) || (!pStream->zalloc) || (!pStream->zfree))
+ return MZ_STREAM_ERROR;
+ pStream->total_in = pStream->total_out = 0;
+ tdefl_init((tdefl_compressor *)pStream->state, NULL, NULL, ((tdefl_compressor *)pStream->state)->m_flags);
+ return MZ_OK;
+ }
+
+ int mz_deflate(mz_streamp pStream, int flush)
+ {
+ size_t in_bytes, out_bytes;
+ mz_ulong orig_total_in, orig_total_out;
+ int mz_status = MZ_OK;
+
+ if ((!pStream) || (!pStream->state) || (flush < 0) || (flush > MZ_FINISH) || (!pStream->next_out))
+ return MZ_STREAM_ERROR;
+ if (!pStream->avail_out)
+ return MZ_BUF_ERROR;
+
+ if (flush == MZ_PARTIAL_FLUSH)
+ flush = MZ_SYNC_FLUSH;
+
+ if (((tdefl_compressor *)pStream->state)->m_prev_return_status == TDEFL_STATUS_DONE)
+ return (flush == MZ_FINISH) ? MZ_STREAM_END : MZ_BUF_ERROR;
+
+ orig_total_in = pStream->total_in;
+ orig_total_out = pStream->total_out;
+ for (;;)
+ {
+ tdefl_status defl_status;
+ in_bytes = pStream->avail_in;
+ out_bytes = pStream->avail_out;
+
+ defl_status = tdefl_compress((tdefl_compressor *)pStream->state, pStream->next_in, &in_bytes, pStream->next_out, &out_bytes, (tdefl_flush)flush);
+ pStream->next_in += (mz_uint)in_bytes;
+ pStream->avail_in -= (mz_uint)in_bytes;
+ pStream->total_in += (mz_uint)in_bytes;
+ pStream->adler = tdefl_get_adler32((tdefl_compressor *)pStream->state);
+
+ pStream->next_out += (mz_uint)out_bytes;
+ pStream->avail_out -= (mz_uint)out_bytes;
+ pStream->total_out += (mz_uint)out_bytes;
+
+ if (defl_status < 0)
+ {
+ mz_status = MZ_STREAM_ERROR;
+ break;
+ }
+ else if (defl_status == TDEFL_STATUS_DONE)
+ {
+ mz_status = MZ_STREAM_END;
+ break;
+ }
+ else if (!pStream->avail_out)
+ break;
+ else if ((!pStream->avail_in) && (flush != MZ_FINISH))
+ {
+ if ((flush) || (pStream->total_in != orig_total_in) || (pStream->total_out != orig_total_out))
+ break;
+ return MZ_BUF_ERROR; /* Can't make forward progress without some input.
+ */
+ }
+ }
+ return mz_status;
+ }
+
+ int mz_deflateEnd(mz_streamp pStream)
+ {
+ if (!pStream)
+ return MZ_STREAM_ERROR;
+ if (pStream->state)
+ {
+ pStream->zfree(pStream->opaque, pStream->state);
+ pStream->state = NULL;
+ }
+ return MZ_OK;
+ }
+
+ mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len)
+ {
+ (void)pStream;
+ /* This is really over conservative. (And lame, but it's actually pretty tricky to compute a true upper bound given the way tdefl's blocking works.) */
+ return MZ_MAX(128 + (source_len * 110) / 100, 128 + source_len + ((source_len / (31 * 1024)) + 1) * 5);
+ }
+
+ int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level)
+ {
+ int status;
+ mz_stream stream;
+ memset(&stream, 0, sizeof(stream));
+
+ /* In case mz_ulong is 64-bits (argh I hate longs). */
+ if ((mz_uint64)(source_len | *pDest_len) > 0xFFFFFFFFU)
+ return MZ_PARAM_ERROR;
+
+ stream.next_in = pSource;
+ stream.avail_in = (mz_uint32)source_len;
+ stream.next_out = pDest;
+ stream.avail_out = (mz_uint32)*pDest_len;
+
+ status = mz_deflateInit(&stream, level);
+ if (status != MZ_OK)
+ return status;
+
+ status = mz_deflate(&stream, MZ_FINISH);
+ if (status != MZ_STREAM_END)
+ {
+ mz_deflateEnd(&stream);
+ return (status == MZ_OK) ? MZ_BUF_ERROR : status;
+ }
+
+ *pDest_len = stream.total_out;
+ return mz_deflateEnd(&stream);
+ }
+
+ int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len)
+ {
+ return mz_compress2(pDest, pDest_len, pSource, source_len, MZ_DEFAULT_COMPRESSION);
+ }
+
+ mz_ulong mz_compressBound(mz_ulong source_len)
+ {
+ return mz_deflateBound(NULL, source_len);
+ }
+
+#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
+
+#ifndef MINIZ_NO_INFLATE_APIS
+
+ typedef struct
+ {
+ tinfl_decompressor m_decomp;
+ mz_uint m_dict_ofs, m_dict_avail, m_first_call, m_has_flushed;
+ int m_window_bits;
+ mz_uint8 m_dict[TINFL_LZ_DICT_SIZE];
+ tinfl_status m_last_status;
+ } inflate_state;
+
+ int mz_inflateInit2(mz_streamp pStream, int window_bits)
+ {
+ inflate_state *pDecomp;
+ if (!pStream)
+ return MZ_STREAM_ERROR;
+ if ((window_bits != MZ_DEFAULT_WINDOW_BITS) && (-window_bits != MZ_DEFAULT_WINDOW_BITS))
+ return MZ_PARAM_ERROR;
+
+ pStream->data_type = 0;
+ pStream->adler = 0;
+ pStream->msg = NULL;
+ pStream->total_in = 0;
+ pStream->total_out = 0;
+ pStream->reserved = 0;
+ if (!pStream->zalloc)
+ pStream->zalloc = miniz_def_alloc_func;
+ if (!pStream->zfree)
+ pStream->zfree = miniz_def_free_func;
+
+ pDecomp = (inflate_state *)pStream->zalloc(pStream->opaque, 1, sizeof(inflate_state));
+ if (!pDecomp)
+ return MZ_MEM_ERROR;
+
+ pStream->state = (struct mz_internal_state *)pDecomp;
+
+ tinfl_init(&pDecomp->m_decomp);
+ pDecomp->m_dict_ofs = 0;
+ pDecomp->m_dict_avail = 0;
+ pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT;
+ pDecomp->m_first_call = 1;
+ pDecomp->m_has_flushed = 0;
+ pDecomp->m_window_bits = window_bits;
+
+ return MZ_OK;
+ }
+
+ int mz_inflateInit(mz_streamp pStream)
+ {
+ return mz_inflateInit2(pStream, MZ_DEFAULT_WINDOW_BITS);
+ }
+
+ int mz_inflateReset(mz_streamp pStream)
+ {
+ inflate_state *pDecomp;
+ if (!pStream)
+ return MZ_STREAM_ERROR;
+
+ pStream->data_type = 0;
+ pStream->adler = 0;
+ pStream->msg = NULL;
+ pStream->total_in = 0;
+ pStream->total_out = 0;
+ pStream->reserved = 0;
+
+ pDecomp = (inflate_state *)pStream->state;
+
+ tinfl_init(&pDecomp->m_decomp);
+ pDecomp->m_dict_ofs = 0;
+ pDecomp->m_dict_avail = 0;
+ pDecomp->m_last_status = TINFL_STATUS_NEEDS_MORE_INPUT;
+ pDecomp->m_first_call = 1;
+ pDecomp->m_has_flushed = 0;
+ /* pDecomp->m_window_bits = window_bits */;
+
+ return MZ_OK;
+ }
+
+ int mz_inflate(mz_streamp pStream, int flush)
+ {
+ inflate_state *pState;
+ mz_uint n, first_call, decomp_flags = TINFL_FLAG_COMPUTE_ADLER32;
+ size_t in_bytes, out_bytes, orig_avail_in;
+ tinfl_status status;
+
+ if ((!pStream) || (!pStream->state))
+ return MZ_STREAM_ERROR;
+ if (flush == MZ_PARTIAL_FLUSH)
+ flush = MZ_SYNC_FLUSH;
+ if ((flush) && (flush != MZ_SYNC_FLUSH) && (flush != MZ_FINISH))
+ return MZ_STREAM_ERROR;
+
+ pState = (inflate_state *)pStream->state;
+ if (pState->m_window_bits > 0)
+ decomp_flags |= TINFL_FLAG_PARSE_ZLIB_HEADER;
+ orig_avail_in = pStream->avail_in;
+
+ first_call = pState->m_first_call;
+ pState->m_first_call = 0;
+ if (pState->m_last_status < 0)
+ return MZ_DATA_ERROR;
+
+ if (pState->m_has_flushed && (flush != MZ_FINISH))
+ return MZ_STREAM_ERROR;
+ pState->m_has_flushed |= (flush == MZ_FINISH);
+
+ if ((flush == MZ_FINISH) && (first_call))
+ {
+ /* MZ_FINISH on the first call implies that the input and output buffers are large enough to hold the entire compressed/decompressed file. */
+ decomp_flags |= TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF;
+ in_bytes = pStream->avail_in;
+ out_bytes = pStream->avail_out;
+ status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pStream->next_out, pStream->next_out, &out_bytes, decomp_flags);
+ pState->m_last_status = status;
+ pStream->next_in += (mz_uint)in_bytes;
+ pStream->avail_in -= (mz_uint)in_bytes;
+ pStream->total_in += (mz_uint)in_bytes;
+ pStream->adler = tinfl_get_adler32(&pState->m_decomp);
+ pStream->next_out += (mz_uint)out_bytes;
+ pStream->avail_out -= (mz_uint)out_bytes;
+ pStream->total_out += (mz_uint)out_bytes;
+
+ if (status < 0)
+ return MZ_DATA_ERROR;
+ else if (status != TINFL_STATUS_DONE)
+ {
+ pState->m_last_status = TINFL_STATUS_FAILED;
+ return MZ_BUF_ERROR;
+ }
+ return MZ_STREAM_END;
+ }
+ /* flush != MZ_FINISH then we must assume there's more input. */
+ if (flush != MZ_FINISH)
+ decomp_flags |= TINFL_FLAG_HAS_MORE_INPUT;
+
+ if (pState->m_dict_avail)
+ {
+ n = MZ_MIN(pState->m_dict_avail, pStream->avail_out);
+ memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n);
+ pStream->next_out += n;
+ pStream->avail_out -= n;
+ pStream->total_out += n;
+ pState->m_dict_avail -= n;
+ pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1);
+ return ((pState->m_last_status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK;
+ }
+
+ for (;;)
+ {
+ in_bytes = pStream->avail_in;
+ out_bytes = TINFL_LZ_DICT_SIZE - pState->m_dict_ofs;
+
+ status = tinfl_decompress(&pState->m_decomp, pStream->next_in, &in_bytes, pState->m_dict, pState->m_dict + pState->m_dict_ofs, &out_bytes, decomp_flags);
+ pState->m_last_status = status;
+
+ pStream->next_in += (mz_uint)in_bytes;
+ pStream->avail_in -= (mz_uint)in_bytes;
+ pStream->total_in += (mz_uint)in_bytes;
+ pStream->adler = tinfl_get_adler32(&pState->m_decomp);
+
+ pState->m_dict_avail = (mz_uint)out_bytes;
+
+ n = MZ_MIN(pState->m_dict_avail, pStream->avail_out);
+ memcpy(pStream->next_out, pState->m_dict + pState->m_dict_ofs, n);
+ pStream->next_out += n;
+ pStream->avail_out -= n;
+ pStream->total_out += n;
+ pState->m_dict_avail -= n;
+ pState->m_dict_ofs = (pState->m_dict_ofs + n) & (TINFL_LZ_DICT_SIZE - 1);
+
+ if (status < 0)
+ return MZ_DATA_ERROR; /* Stream is corrupted (there could be some uncompressed data left in the output dictionary - oh well). */
+ else if ((status == TINFL_STATUS_NEEDS_MORE_INPUT) && (!orig_avail_in))
+ return MZ_BUF_ERROR; /* Signal caller that we can't make forward progress without supplying more input or by setting flush to MZ_FINISH. */
+ else if (flush == MZ_FINISH)
+ {
+ /* The output buffer MUST be large to hold the remaining uncompressed data when flush==MZ_FINISH. */
+ if (status == TINFL_STATUS_DONE)
+ return pState->m_dict_avail ? MZ_BUF_ERROR : MZ_STREAM_END;
+ /* status here must be TINFL_STATUS_HAS_MORE_OUTPUT, which means there's at least 1 more byte on the way. If there's no more room left in the output buffer then something is wrong. */
+ else if (!pStream->avail_out)
+ return MZ_BUF_ERROR;
+ }
+ else if ((status == TINFL_STATUS_DONE) || (!pStream->avail_in) || (!pStream->avail_out) || (pState->m_dict_avail))
+ break;
+ }
+
+ return ((status == TINFL_STATUS_DONE) && (!pState->m_dict_avail)) ? MZ_STREAM_END : MZ_OK;
+ }
+
+ int mz_inflateEnd(mz_streamp pStream)
+ {
+ if (!pStream)
+ return MZ_STREAM_ERROR;
+ if (pStream->state)
+ {
+ pStream->zfree(pStream->opaque, pStream->state);
+ pStream->state = NULL;
+ }
+ return MZ_OK;
+ }
+ int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len)
+ {
+ mz_stream stream;
+ int status;
+ memset(&stream, 0, sizeof(stream));
+
+ /* In case mz_ulong is 64-bits (argh I hate longs). */
+ if ((mz_uint64)(*pSource_len | *pDest_len) > 0xFFFFFFFFU)
+ return MZ_PARAM_ERROR;
+
+ stream.next_in = pSource;
+ stream.avail_in = (mz_uint32)*pSource_len;
+ stream.next_out = pDest;
+ stream.avail_out = (mz_uint32)*pDest_len;
+
+ status = mz_inflateInit(&stream);
+ if (status != MZ_OK)
+ return status;
+
+ status = mz_inflate(&stream, MZ_FINISH);
+ *pSource_len = *pSource_len - stream.avail_in;
+ if (status != MZ_STREAM_END)
+ {
+ mz_inflateEnd(&stream);
+ return ((status == MZ_BUF_ERROR) && (!stream.avail_in)) ? MZ_DATA_ERROR : status;
+ }
+ *pDest_len = stream.total_out;
+
+ return mz_inflateEnd(&stream);
+ }
+
+ int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len)
+ {
+ return mz_uncompress2(pDest, pDest_len, pSource, &source_len);
+ }
+
+#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
+
+ const char *mz_error(int err)
+ {
+ static struct
+ {
+ int m_err;
+ const char *m_pDesc;
+ } s_error_descs[] = {
+ { MZ_OK, "" }, { MZ_STREAM_END, "stream end" }, { MZ_NEED_DICT, "need dictionary" }, { MZ_ERRNO, "file error" }, { MZ_STREAM_ERROR, "stream error" }, { MZ_DATA_ERROR, "data error" }, { MZ_MEM_ERROR, "out of memory" }, { MZ_BUF_ERROR, "buf error" }, { MZ_VERSION_ERROR, "version error" }, { MZ_PARAM_ERROR, "parameter error" }
+ };
+ mz_uint i;
+ for (i = 0; i < sizeof(s_error_descs) / sizeof(s_error_descs[0]); ++i)
+ if (s_error_descs[i].m_err == err)
+ return s_error_descs[i].m_pDesc;
+ return NULL;
+ }
+
+#endif /*MINIZ_NO_ZLIB_APIS */
+
+#ifdef __cplusplus
+}
+#endif
+
+/*
+ This is free and unencumbered software released into the public domain.
+
+ Anyone is free to copy, modify, publish, use, compile, sell, or
+ distribute this software, either in source code form or as a compiled
+ binary, for any purpose, commercial or non-commercial, and by any
+ means.
+
+ In jurisdictions that recognize copyright laws, the author or authors
+ of this software dedicate any and all copyright interest in the
+ software to the public domain. We make this dedication for the benefit
+ of the public at large and to the detriment of our heirs and
+ successors. We intend this dedication to be an overt act of
+ relinquishment in perpetuity of all present and future rights to this
+ software under copyright law.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+
+ For more information, please refer to
+*/
+/**************************************************************************
+ *
+ * Copyright 2013-2014 RAD Game Tools and Valve Software
+ * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
+ * All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ **************************************************************************/
+
+
+
+#ifndef MINIZ_NO_DEFLATE_APIS
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ /* ------------------- Low-level Compression (independent from all decompression API's) */
+
+ /* Purposely making these tables static for faster init and thread safety. */
+ static const mz_uint16 s_tdefl_len_sym[256] = {
+ 257, 258, 259, 260, 261, 262, 263, 264, 265, 265, 266, 266, 267, 267, 268, 268, 269, 269, 269, 269, 270, 270, 270, 270, 271, 271, 271, 271, 272, 272, 272, 272,
+ 273, 273, 273, 273, 273, 273, 273, 273, 274, 274, 274, 274, 274, 274, 274, 274, 275, 275, 275, 275, 275, 275, 275, 275, 276, 276, 276, 276, 276, 276, 276, 276,
+ 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 277, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278, 278,
+ 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 279, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280, 280,
+ 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281, 281,
+ 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282, 282,
+ 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283, 283,
+ 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 284, 285
+ };
+
+ static const mz_uint8 s_tdefl_len_extra[256] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 0
+ };
+
+ static const mz_uint8 s_tdefl_small_dist_sym[512] = {
+ 0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11,
+ 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13,
+ 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+ 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
+ 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
+ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
+ 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
+ 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
+ 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17
+ };
+
+ static const mz_uint8 s_tdefl_small_dist_extra[512] = {
+ 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5,
+ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
+ 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7
+ };
+
+ static const mz_uint8 s_tdefl_large_dist_sym[128] = {
+ 0, 0, 18, 19, 20, 20, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
+ 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
+ 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29
+ };
+
+ static const mz_uint8 s_tdefl_large_dist_extra[128] = {
+ 0, 0, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
+ 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
+ 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13
+ };
+
+ /* Radix sorts tdefl_sym_freq[] array by 16-bit key m_key. Returns ptr to sorted values. */
+ typedef struct
+ {
+ mz_uint16 m_key, m_sym_index;
+ } tdefl_sym_freq;
+ static tdefl_sym_freq *tdefl_radix_sort_syms(mz_uint num_syms, tdefl_sym_freq *pSyms0, tdefl_sym_freq *pSyms1)
+ {
+ mz_uint32 total_passes = 2, pass_shift, pass, i, hist[256 * 2];
+ tdefl_sym_freq *pCur_syms = pSyms0, *pNew_syms = pSyms1;
+ MZ_CLEAR_ARR(hist);
+ for (i = 0; i < num_syms; i++)
+ {
+ mz_uint freq = pSyms0[i].m_key;
+ hist[freq & 0xFF]++;
+ hist[256 + ((freq >> 8) & 0xFF)]++;
+ }
+ while ((total_passes > 1) && (num_syms == hist[(total_passes - 1) * 256]))
+ total_passes--;
+ for (pass_shift = 0, pass = 0; pass < total_passes; pass++, pass_shift += 8)
+ {
+ const mz_uint32 *pHist = &hist[pass << 8];
+ mz_uint offsets[256], cur_ofs = 0;
+ for (i = 0; i < 256; i++)
+ {
+ offsets[i] = cur_ofs;
+ cur_ofs += pHist[i];
+ }
+ for (i = 0; i < num_syms; i++)
+ pNew_syms[offsets[(pCur_syms[i].m_key >> pass_shift) & 0xFF]++] = pCur_syms[i];
+ {
+ tdefl_sym_freq *t = pCur_syms;
+ pCur_syms = pNew_syms;
+ pNew_syms = t;
+ }
+ }
+ return pCur_syms;
+ }
+
+ /* tdefl_calculate_minimum_redundancy() originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996. */
+ static void tdefl_calculate_minimum_redundancy(tdefl_sym_freq *A, int n)
+ {
+ int root, leaf, next, avbl, used, dpth;
+ if (n == 0)
+ return;
+ else if (n == 1)
+ {
+ A[0].m_key = 1;
+ return;
+ }
+ A[0].m_key += A[1].m_key;
+ root = 0;
+ leaf = 2;
+ for (next = 1; next < n - 1; next++)
+ {
+ if (leaf >= n || A[root].m_key < A[leaf].m_key)
+ {
+ A[next].m_key = A[root].m_key;
+ A[root++].m_key = (mz_uint16)next;
+ }
+ else
+ A[next].m_key = A[leaf++].m_key;
+ if (leaf >= n || (root < next && A[root].m_key < A[leaf].m_key))
+ {
+ A[next].m_key = (mz_uint16)(A[next].m_key + A[root].m_key);
+ A[root++].m_key = (mz_uint16)next;
+ }
+ else
+ A[next].m_key = (mz_uint16)(A[next].m_key + A[leaf++].m_key);
+ }
+ A[n - 2].m_key = 0;
+ for (next = n - 3; next >= 0; next--)
+ A[next].m_key = A[A[next].m_key].m_key + 1;
+ avbl = 1;
+ used = dpth = 0;
+ root = n - 2;
+ next = n - 1;
+ while (avbl > 0)
+ {
+ while (root >= 0 && (int)A[root].m_key == dpth)
+ {
+ used++;
+ root--;
+ }
+ while (avbl > used)
+ {
+ A[next--].m_key = (mz_uint16)(dpth);
+ avbl--;
+ }
+ avbl = 2 * used;
+ dpth++;
+ used = 0;
+ }
+ }
+
+ /* Limits canonical Huffman code table's max code size. */
+ enum
+ {
+ TDEFL_MAX_SUPPORTED_HUFF_CODESIZE = 32
+ };
+ static void tdefl_huffman_enforce_max_code_size(int *pNum_codes, int code_list_len, int max_code_size)
+ {
+ int i;
+ mz_uint32 total = 0;
+ if (code_list_len <= 1)
+ return;
+ for (i = max_code_size + 1; i <= TDEFL_MAX_SUPPORTED_HUFF_CODESIZE; i++)
+ pNum_codes[max_code_size] += pNum_codes[i];
+ for (i = max_code_size; i > 0; i--)
+ total += (((mz_uint32)pNum_codes[i]) << (max_code_size - i));
+ while (total != (1UL << max_code_size))
+ {
+ pNum_codes[max_code_size]--;
+ for (i = max_code_size - 1; i > 0; i--)
+ if (pNum_codes[i])
+ {
+ pNum_codes[i]--;
+ pNum_codes[i + 1] += 2;
+ break;
+ }
+ total--;
+ }
+ }
+
+ static void tdefl_optimize_huffman_table(tdefl_compressor *d, int table_num, int table_len, int code_size_limit, int static_table)
+ {
+ int i, j, l, num_codes[1 + TDEFL_MAX_SUPPORTED_HUFF_CODESIZE];
+ mz_uint next_code[TDEFL_MAX_SUPPORTED_HUFF_CODESIZE + 1];
+ MZ_CLEAR_ARR(num_codes);
+ if (static_table)
+ {
+ for (i = 0; i < table_len; i++)
+ num_codes[d->m_huff_code_sizes[table_num][i]]++;
+ }
+ else
+ {
+ tdefl_sym_freq syms0[TDEFL_MAX_HUFF_SYMBOLS], syms1[TDEFL_MAX_HUFF_SYMBOLS], *pSyms;
+ int num_used_syms = 0;
+ const mz_uint16 *pSym_count = &d->m_huff_count[table_num][0];
+ for (i = 0; i < table_len; i++)
+ if (pSym_count[i])
+ {
+ syms0[num_used_syms].m_key = (mz_uint16)pSym_count[i];
+ syms0[num_used_syms++].m_sym_index = (mz_uint16)i;
+ }
+
+ pSyms = tdefl_radix_sort_syms(num_used_syms, syms0, syms1);
+ tdefl_calculate_minimum_redundancy(pSyms, num_used_syms);
+
+ for (i = 0; i < num_used_syms; i++)
+ num_codes[pSyms[i].m_key]++;
+
+ tdefl_huffman_enforce_max_code_size(num_codes, num_used_syms, code_size_limit);
+
+ MZ_CLEAR_ARR(d->m_huff_code_sizes[table_num]);
+ MZ_CLEAR_ARR(d->m_huff_codes[table_num]);
+ for (i = 1, j = num_used_syms; i <= code_size_limit; i++)
+ for (l = num_codes[i]; l > 0; l--)
+ d->m_huff_code_sizes[table_num][pSyms[--j].m_sym_index] = (mz_uint8)(i);
+ }
+
+ next_code[1] = 0;
+ for (j = 0, i = 2; i <= code_size_limit; i++)
+ next_code[i] = j = ((j + num_codes[i - 1]) << 1);
+
+ for (i = 0; i < table_len; i++)
+ {
+ mz_uint rev_code = 0, code, code_size;
+ if ((code_size = d->m_huff_code_sizes[table_num][i]) == 0)
+ continue;
+ code = next_code[code_size]++;
+ for (l = code_size; l > 0; l--, code >>= 1)
+ rev_code = (rev_code << 1) | (code & 1);
+ d->m_huff_codes[table_num][i] = (mz_uint16)rev_code;
+ }
+ }
+
+#define TDEFL_PUT_BITS(b, l) \
+ do \
+ { \
+ mz_uint bits = b; \
+ mz_uint len = l; \
+ MZ_ASSERT(bits <= ((1U << len) - 1U)); \
+ d->m_bit_buffer |= (bits << d->m_bits_in); \
+ d->m_bits_in += len; \
+ while (d->m_bits_in >= 8) \
+ { \
+ if (d->m_pOutput_buf < d->m_pOutput_buf_end) \
+ *d->m_pOutput_buf++ = (mz_uint8)(d->m_bit_buffer); \
+ d->m_bit_buffer >>= 8; \
+ d->m_bits_in -= 8; \
+ } \
+ } \
+ MZ_MACRO_END
+
+#define TDEFL_RLE_PREV_CODE_SIZE() \
+ { \
+ if (rle_repeat_count) \
+ { \
+ if (rle_repeat_count < 3) \
+ { \
+ d->m_huff_count[2][prev_code_size] = (mz_uint16)(d->m_huff_count[2][prev_code_size] + rle_repeat_count); \
+ while (rle_repeat_count--) \
+ packed_code_sizes[num_packed_code_sizes++] = prev_code_size; \
+ } \
+ else \
+ { \
+ d->m_huff_count[2][16] = (mz_uint16)(d->m_huff_count[2][16] + 1); \
+ packed_code_sizes[num_packed_code_sizes++] = 16; \
+ packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_repeat_count - 3); \
+ } \
+ rle_repeat_count = 0; \
+ } \
+ }
+
+#define TDEFL_RLE_ZERO_CODE_SIZE() \
+ { \
+ if (rle_z_count) \
+ { \
+ if (rle_z_count < 3) \
+ { \
+ d->m_huff_count[2][0] = (mz_uint16)(d->m_huff_count[2][0] + rle_z_count); \
+ while (rle_z_count--) \
+ packed_code_sizes[num_packed_code_sizes++] = 0; \
+ } \
+ else if (rle_z_count <= 10) \
+ { \
+ d->m_huff_count[2][17] = (mz_uint16)(d->m_huff_count[2][17] + 1); \
+ packed_code_sizes[num_packed_code_sizes++] = 17; \
+ packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 3); \
+ } \
+ else \
+ { \
+ d->m_huff_count[2][18] = (mz_uint16)(d->m_huff_count[2][18] + 1); \
+ packed_code_sizes[num_packed_code_sizes++] = 18; \
+ packed_code_sizes[num_packed_code_sizes++] = (mz_uint8)(rle_z_count - 11); \
+ } \
+ rle_z_count = 0; \
+ } \
+ }
+
+ static const mz_uint8 s_tdefl_packed_code_size_syms_swizzle[] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };
+
+ static void tdefl_start_dynamic_block(tdefl_compressor *d)
+ {
+ int num_lit_codes, num_dist_codes, num_bit_lengths;
+ mz_uint i, total_code_sizes_to_pack, num_packed_code_sizes, rle_z_count, rle_repeat_count, packed_code_sizes_index;
+ mz_uint8 code_sizes_to_pack[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], packed_code_sizes[TDEFL_MAX_HUFF_SYMBOLS_0 + TDEFL_MAX_HUFF_SYMBOLS_1], prev_code_size = 0xFF;
+
+ d->m_huff_count[0][256] = 1;
+
+ tdefl_optimize_huffman_table(d, 0, TDEFL_MAX_HUFF_SYMBOLS_0, 15, MZ_FALSE);
+ tdefl_optimize_huffman_table(d, 1, TDEFL_MAX_HUFF_SYMBOLS_1, 15, MZ_FALSE);
+
+ for (num_lit_codes = 286; num_lit_codes > 257; num_lit_codes--)
+ if (d->m_huff_code_sizes[0][num_lit_codes - 1])
+ break;
+ for (num_dist_codes = 30; num_dist_codes > 1; num_dist_codes--)
+ if (d->m_huff_code_sizes[1][num_dist_codes - 1])
+ break;
+
+ memcpy(code_sizes_to_pack, &d->m_huff_code_sizes[0][0], num_lit_codes);
+ memcpy(code_sizes_to_pack + num_lit_codes, &d->m_huff_code_sizes[1][0], num_dist_codes);
+ total_code_sizes_to_pack = num_lit_codes + num_dist_codes;
+ num_packed_code_sizes = 0;
+ rle_z_count = 0;
+ rle_repeat_count = 0;
+
+ memset(&d->m_huff_count[2][0], 0, sizeof(d->m_huff_count[2][0]) * TDEFL_MAX_HUFF_SYMBOLS_2);
+ for (i = 0; i < total_code_sizes_to_pack; i++)
+ {
+ mz_uint8 code_size = code_sizes_to_pack[i];
+ if (!code_size)
+ {
+ TDEFL_RLE_PREV_CODE_SIZE();
+ if (++rle_z_count == 138)
+ {
+ TDEFL_RLE_ZERO_CODE_SIZE();
+ }
+ }
+ else
+ {
+ TDEFL_RLE_ZERO_CODE_SIZE();
+ if (code_size != prev_code_size)
+ {
+ TDEFL_RLE_PREV_CODE_SIZE();
+ d->m_huff_count[2][code_size] = (mz_uint16)(d->m_huff_count[2][code_size] + 1);
+ packed_code_sizes[num_packed_code_sizes++] = code_size;
+ }
+ else if (++rle_repeat_count == 6)
+ {
+ TDEFL_RLE_PREV_CODE_SIZE();
+ }
+ }
+ prev_code_size = code_size;
+ }
+ if (rle_repeat_count)
+ {
+ TDEFL_RLE_PREV_CODE_SIZE();
+ }
+ else
+ {
+ TDEFL_RLE_ZERO_CODE_SIZE();
+ }
+
+ tdefl_optimize_huffman_table(d, 2, TDEFL_MAX_HUFF_SYMBOLS_2, 7, MZ_FALSE);
+
+ TDEFL_PUT_BITS(2, 2);
+
+ TDEFL_PUT_BITS(num_lit_codes - 257, 5);
+ TDEFL_PUT_BITS(num_dist_codes - 1, 5);
+
+ for (num_bit_lengths = 18; num_bit_lengths >= 0; num_bit_lengths--)
+ if (d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[num_bit_lengths]])
+ break;
+ num_bit_lengths = MZ_MAX(4, (num_bit_lengths + 1));
+ TDEFL_PUT_BITS(num_bit_lengths - 4, 4);
+ for (i = 0; (int)i < num_bit_lengths; i++)
+ TDEFL_PUT_BITS(d->m_huff_code_sizes[2][s_tdefl_packed_code_size_syms_swizzle[i]], 3);
+
+ for (packed_code_sizes_index = 0; packed_code_sizes_index < num_packed_code_sizes;)
+ {
+ mz_uint code = packed_code_sizes[packed_code_sizes_index++];
+ MZ_ASSERT(code < TDEFL_MAX_HUFF_SYMBOLS_2);
+ TDEFL_PUT_BITS(d->m_huff_codes[2][code], d->m_huff_code_sizes[2][code]);
+ if (code >= 16)
+ TDEFL_PUT_BITS(packed_code_sizes[packed_code_sizes_index++], "\02\03\07"[code - 16]);
+ }
+ }
+
+ static void tdefl_start_static_block(tdefl_compressor *d)
+ {
+ mz_uint i;
+ mz_uint8 *p = &d->m_huff_code_sizes[0][0];
+
+ for (i = 0; i <= 143; ++i)
+ *p++ = 8;
+ for (; i <= 255; ++i)
+ *p++ = 9;
+ for (; i <= 279; ++i)
+ *p++ = 7;
+ for (; i <= 287; ++i)
+ *p++ = 8;
+
+ memset(d->m_huff_code_sizes[1], 5, 32);
+
+ tdefl_optimize_huffman_table(d, 0, 288, 15, MZ_TRUE);
+ tdefl_optimize_huffman_table(d, 1, 32, 15, MZ_TRUE);
+
+ TDEFL_PUT_BITS(1, 2);
+ }
+
+ static const mz_uint mz_bitmasks[17] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF };
+
+#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS
+ static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d)
+ {
+ mz_uint flags;
+ mz_uint8 *pLZ_codes;
+ mz_uint8 *pOutput_buf = d->m_pOutput_buf;
+ mz_uint8 *pLZ_code_buf_end = d->m_pLZ_code_buf;
+ mz_uint64 bit_buffer = d->m_bit_buffer;
+ mz_uint bits_in = d->m_bits_in;
+
+#define TDEFL_PUT_BITS_FAST(b, l) \
+ { \
+ bit_buffer |= (((mz_uint64)(b)) << bits_in); \
+ bits_in += (l); \
+ }
+
+ flags = 1;
+ for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < pLZ_code_buf_end; flags >>= 1)
+ {
+ if (flags == 1)
+ flags = *pLZ_codes++ | 0x100;
+
+ if (flags & 1)
+ {
+ mz_uint s0, s1, n0, n1, sym, num_extra_bits;
+ mz_uint match_len = pLZ_codes[0];
+ mz_uint match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8));
+ pLZ_codes += 3;
+
+ MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]);
+ TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]);
+ TDEFL_PUT_BITS_FAST(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]);
+
+ /* This sequence coaxes MSVC into using cmov's vs. jmp's. */
+ s0 = s_tdefl_small_dist_sym[match_dist & 511];
+ n0 = s_tdefl_small_dist_extra[match_dist & 511];
+ s1 = s_tdefl_large_dist_sym[match_dist >> 8];
+ n1 = s_tdefl_large_dist_extra[match_dist >> 8];
+ sym = (match_dist < 512) ? s0 : s1;
+ num_extra_bits = (match_dist < 512) ? n0 : n1;
+
+ MZ_ASSERT(d->m_huff_code_sizes[1][sym]);
+ TDEFL_PUT_BITS_FAST(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]);
+ TDEFL_PUT_BITS_FAST(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits);
+ }
+ else
+ {
+ mz_uint lit = *pLZ_codes++;
+ MZ_ASSERT(d->m_huff_code_sizes[0][lit]);
+ TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]);
+
+ if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end))
+ {
+ flags >>= 1;
+ lit = *pLZ_codes++;
+ MZ_ASSERT(d->m_huff_code_sizes[0][lit]);
+ TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]);
+
+ if (((flags & 2) == 0) && (pLZ_codes < pLZ_code_buf_end))
+ {
+ flags >>= 1;
+ lit = *pLZ_codes++;
+ MZ_ASSERT(d->m_huff_code_sizes[0][lit]);
+ TDEFL_PUT_BITS_FAST(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]);
+ }
+ }
+ }
+
+ if (pOutput_buf >= d->m_pOutput_buf_end)
+ return MZ_FALSE;
+
+ memcpy(pOutput_buf, &bit_buffer, sizeof(mz_uint64));
+ pOutput_buf += (bits_in >> 3);
+ bit_buffer >>= (bits_in & ~7);
+ bits_in &= 7;
+ }
+
+#undef TDEFL_PUT_BITS_FAST
+
+ d->m_pOutput_buf = pOutput_buf;
+ d->m_bits_in = 0;
+ d->m_bit_buffer = 0;
+
+ while (bits_in)
+ {
+ mz_uint32 n = MZ_MIN(bits_in, 16);
+ TDEFL_PUT_BITS((mz_uint)bit_buffer & mz_bitmasks[n], n);
+ bit_buffer >>= n;
+ bits_in -= n;
+ }
+
+ TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]);
+
+ return (d->m_pOutput_buf < d->m_pOutput_buf_end);
+ }
+#else
+static mz_bool tdefl_compress_lz_codes(tdefl_compressor *d)
+{
+ mz_uint flags;
+ mz_uint8 *pLZ_codes;
+
+ flags = 1;
+ for (pLZ_codes = d->m_lz_code_buf; pLZ_codes < d->m_pLZ_code_buf; flags >>= 1)
+ {
+ if (flags == 1)
+ flags = *pLZ_codes++ | 0x100;
+ if (flags & 1)
+ {
+ mz_uint sym, num_extra_bits;
+ mz_uint match_len = pLZ_codes[0], match_dist = (pLZ_codes[1] | (pLZ_codes[2] << 8));
+ pLZ_codes += 3;
+
+ MZ_ASSERT(d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]);
+ TDEFL_PUT_BITS(d->m_huff_codes[0][s_tdefl_len_sym[match_len]], d->m_huff_code_sizes[0][s_tdefl_len_sym[match_len]]);
+ TDEFL_PUT_BITS(match_len & mz_bitmasks[s_tdefl_len_extra[match_len]], s_tdefl_len_extra[match_len]);
+
+ if (match_dist < 512)
+ {
+ sym = s_tdefl_small_dist_sym[match_dist];
+ num_extra_bits = s_tdefl_small_dist_extra[match_dist];
+ }
+ else
+ {
+ sym = s_tdefl_large_dist_sym[match_dist >> 8];
+ num_extra_bits = s_tdefl_large_dist_extra[match_dist >> 8];
+ }
+ MZ_ASSERT(d->m_huff_code_sizes[1][sym]);
+ TDEFL_PUT_BITS(d->m_huff_codes[1][sym], d->m_huff_code_sizes[1][sym]);
+ TDEFL_PUT_BITS(match_dist & mz_bitmasks[num_extra_bits], num_extra_bits);
+ }
+ else
+ {
+ mz_uint lit = *pLZ_codes++;
+ MZ_ASSERT(d->m_huff_code_sizes[0][lit]);
+ TDEFL_PUT_BITS(d->m_huff_codes[0][lit], d->m_huff_code_sizes[0][lit]);
+ }
+ }
+
+ TDEFL_PUT_BITS(d->m_huff_codes[0][256], d->m_huff_code_sizes[0][256]);
+
+ return (d->m_pOutput_buf < d->m_pOutput_buf_end);
+}
+#endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN && MINIZ_HAS_64BIT_REGISTERS */
+
+ static mz_bool tdefl_compress_block(tdefl_compressor *d, mz_bool static_block)
+ {
+ if (static_block)
+ tdefl_start_static_block(d);
+ else
+ tdefl_start_dynamic_block(d);
+ return tdefl_compress_lz_codes(d);
+ }
+
+ static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
+
+ static int tdefl_flush_block(tdefl_compressor *d, int flush)
+ {
+ mz_uint saved_bit_buf, saved_bits_in;
+ mz_uint8 *pSaved_output_buf;
+ mz_bool comp_block_succeeded = MZ_FALSE;
+ int n, use_raw_block = ((d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS) != 0) && (d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size;
+ mz_uint8 *pOutput_buf_start = ((d->m_pPut_buf_func == NULL) && ((*d->m_pOut_buf_size - d->m_out_buf_ofs) >= TDEFL_OUT_BUF_SIZE)) ? ((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs) : d->m_output_buf;
+
+ d->m_pOutput_buf = pOutput_buf_start;
+ d->m_pOutput_buf_end = d->m_pOutput_buf + TDEFL_OUT_BUF_SIZE - 16;
+
+ MZ_ASSERT(!d->m_output_flush_remaining);
+ d->m_output_flush_ofs = 0;
+ d->m_output_flush_remaining = 0;
+
+ *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> d->m_num_flags_left);
+ d->m_pLZ_code_buf -= (d->m_num_flags_left == 8);
+
+ if ((d->m_flags & TDEFL_WRITE_ZLIB_HEADER) && (!d->m_block_index))
+ {
+ const mz_uint8 cmf = 0x78;
+ mz_uint8 flg, flevel = 3;
+ mz_uint header, i, mz_un = sizeof(s_tdefl_num_probes) / sizeof(mz_uint);
+
+ /* Determine compression level by reversing the process in tdefl_create_comp_flags_from_zip_params() */
+ for (i = 0; i < mz_un; i++)
+ if (s_tdefl_num_probes[i] == (d->m_flags & 0xFFF))
+ break;
+
+ if (i < 2)
+ flevel = 0;
+ else if (i < 6)
+ flevel = 1;
+ else if (i == 6)
+ flevel = 2;
+
+ header = cmf << 8 | (flevel << 6);
+ header += 31 - (header % 31);
+ flg = header & 0xFF;
+
+ TDEFL_PUT_BITS(cmf, 8);
+ TDEFL_PUT_BITS(flg, 8);
+ }
+
+ TDEFL_PUT_BITS(flush == TDEFL_FINISH, 1);
+
+ pSaved_output_buf = d->m_pOutput_buf;
+ saved_bit_buf = d->m_bit_buffer;
+ saved_bits_in = d->m_bits_in;
+
+ if (!use_raw_block)
+ comp_block_succeeded = tdefl_compress_block(d, (d->m_flags & TDEFL_FORCE_ALL_STATIC_BLOCKS) || (d->m_total_lz_bytes < 48));
+
+ /* If the block gets expanded, forget the current contents of the output buffer and send a raw block instead. */
+ if (((use_raw_block) || ((d->m_total_lz_bytes) && ((d->m_pOutput_buf - pSaved_output_buf + 1U) >= d->m_total_lz_bytes))) &&
+ ((d->m_lookahead_pos - d->m_lz_code_buf_dict_pos) <= d->m_dict_size))
+ {
+ mz_uint i;
+ d->m_pOutput_buf = pSaved_output_buf;
+ d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in;
+ TDEFL_PUT_BITS(0, 2);
+ if (d->m_bits_in)
+ {
+ TDEFL_PUT_BITS(0, 8 - d->m_bits_in);
+ }
+ for (i = 2; i; --i, d->m_total_lz_bytes ^= 0xFFFF)
+ {
+ TDEFL_PUT_BITS(d->m_total_lz_bytes & 0xFFFF, 16);
+ }
+ for (i = 0; i < d->m_total_lz_bytes; ++i)
+ {
+ TDEFL_PUT_BITS(d->m_dict[(d->m_lz_code_buf_dict_pos + i) & TDEFL_LZ_DICT_SIZE_MASK], 8);
+ }
+ }
+ /* Check for the extremely unlikely (if not impossible) case of the compressed block not fitting into the output buffer when using dynamic codes. */
+ else if (!comp_block_succeeded)
+ {
+ d->m_pOutput_buf = pSaved_output_buf;
+ d->m_bit_buffer = saved_bit_buf, d->m_bits_in = saved_bits_in;
+ tdefl_compress_block(d, MZ_TRUE);
+ }
+
+ if (flush)
+ {
+ if (flush == TDEFL_FINISH)
+ {
+ if (d->m_bits_in)
+ {
+ TDEFL_PUT_BITS(0, 8 - d->m_bits_in);
+ }
+ if (d->m_flags & TDEFL_WRITE_ZLIB_HEADER)
+ {
+ mz_uint i, a = d->m_adler32;
+ for (i = 0; i < 4; i++)
+ {
+ TDEFL_PUT_BITS((a >> 24) & 0xFF, 8);
+ a <<= 8;
+ }
+ }
+ }
+ else
+ {
+ mz_uint i, z = 0;
+ TDEFL_PUT_BITS(0, 3);
+ if (d->m_bits_in)
+ {
+ TDEFL_PUT_BITS(0, 8 - d->m_bits_in);
+ }
+ for (i = 2; i; --i, z ^= 0xFFFF)
+ {
+ TDEFL_PUT_BITS(z & 0xFFFF, 16);
+ }
+ }
+ }
+
+ MZ_ASSERT(d->m_pOutput_buf < d->m_pOutput_buf_end);
+
+ memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0);
+ memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1);
+
+ d->m_pLZ_code_buf = d->m_lz_code_buf + 1;
+ d->m_pLZ_flags = d->m_lz_code_buf;
+ d->m_num_flags_left = 8;
+ d->m_lz_code_buf_dict_pos += d->m_total_lz_bytes;
+ d->m_total_lz_bytes = 0;
+ d->m_block_index++;
+
+ if ((n = (int)(d->m_pOutput_buf - pOutput_buf_start)) != 0)
+ {
+ if (d->m_pPut_buf_func)
+ {
+ *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf;
+ if (!(*d->m_pPut_buf_func)(d->m_output_buf, n, d->m_pPut_buf_user))
+ return (d->m_prev_return_status = TDEFL_STATUS_PUT_BUF_FAILED);
+ }
+ else if (pOutput_buf_start == d->m_output_buf)
+ {
+ int bytes_to_copy = (int)MZ_MIN((size_t)n, (size_t)(*d->m_pOut_buf_size - d->m_out_buf_ofs));
+ memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf, bytes_to_copy);
+ d->m_out_buf_ofs += bytes_to_copy;
+ if ((n -= bytes_to_copy) != 0)
+ {
+ d->m_output_flush_ofs = bytes_to_copy;
+ d->m_output_flush_remaining = n;
+ }
+ }
+ else
+ {
+ d->m_out_buf_ofs += n;
+ }
+ }
+
+ return d->m_output_flush_remaining;
+ }
+
+#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES
+#ifdef MINIZ_UNALIGNED_USE_MEMCPY
+ static mz_uint16 TDEFL_READ_UNALIGNED_WORD(const mz_uint8 *p)
+ {
+ mz_uint16 ret;
+ memcpy(&ret, p, sizeof(mz_uint16));
+ return ret;
+ }
+ static mz_uint16 TDEFL_READ_UNALIGNED_WORD2(const mz_uint16 *p)
+ {
+ mz_uint16 ret;
+ memcpy(&ret, p, sizeof(mz_uint16));
+ return ret;
+ }
+#else
+#define TDEFL_READ_UNALIGNED_WORD(p) *(const mz_uint16 *)(p)
+#define TDEFL_READ_UNALIGNED_WORD2(p) *(const mz_uint16 *)(p)
+#endif
+ static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len)
+ {
+ mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len;
+ mz_uint num_probes_left = d->m_max_probes[match_len >= 32];
+ const mz_uint16 *s = (const mz_uint16 *)(d->m_dict + pos), *p, *q;
+ mz_uint16 c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]), s01 = TDEFL_READ_UNALIGNED_WORD2(s);
+ MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN);
+ if (max_match_len <= match_len)
+ return;
+ for (;;)
+ {
+ for (;;)
+ {
+ if (--num_probes_left == 0)
+ return;
+#define TDEFL_PROBE \
+ next_probe_pos = d->m_next[probe_pos]; \
+ if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \
+ return; \
+ probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \
+ if (TDEFL_READ_UNALIGNED_WORD(&d->m_dict[probe_pos + match_len - 1]) == c01) \
+ break;
+ TDEFL_PROBE;
+ TDEFL_PROBE;
+ TDEFL_PROBE;
+ }
+ if (!dist)
+ break;
+ q = (const mz_uint16 *)(d->m_dict + probe_pos);
+ if (TDEFL_READ_UNALIGNED_WORD2(q) != s01)
+ continue;
+ p = s;
+ probe_len = 32;
+ do
+ {
+ } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) &&
+ (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0));
+ if (!probe_len)
+ {
+ *pMatch_dist = dist;
+ *pMatch_len = MZ_MIN(max_match_len, (mz_uint)TDEFL_MAX_MATCH_LEN);
+ break;
+ }
+ else if ((probe_len = ((mz_uint)(p - s) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q)) > match_len)
+ {
+ *pMatch_dist = dist;
+ if ((*pMatch_len = match_len = MZ_MIN(max_match_len, probe_len)) == max_match_len)
+ break;
+ c01 = TDEFL_READ_UNALIGNED_WORD(&d->m_dict[pos + match_len - 1]);
+ }
+ }
+ }
+#else
+static MZ_FORCEINLINE void tdefl_find_match(tdefl_compressor *d, mz_uint lookahead_pos, mz_uint max_dist, mz_uint max_match_len, mz_uint *pMatch_dist, mz_uint *pMatch_len)
+{
+ mz_uint dist, pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK, match_len = *pMatch_len, probe_pos = pos, next_probe_pos, probe_len;
+ mz_uint num_probes_left = d->m_max_probes[match_len >= 32];
+ const mz_uint8 *s = d->m_dict + pos, *p, *q;
+ mz_uint8 c0 = d->m_dict[pos + match_len], c1 = d->m_dict[pos + match_len - 1];
+ MZ_ASSERT(max_match_len <= TDEFL_MAX_MATCH_LEN);
+ if (max_match_len <= match_len)
+ return;
+ for (;;)
+ {
+ for (;;)
+ {
+ if (--num_probes_left == 0)
+ return;
+#define TDEFL_PROBE \
+ next_probe_pos = d->m_next[probe_pos]; \
+ if ((!next_probe_pos) || ((dist = (mz_uint16)(lookahead_pos - next_probe_pos)) > max_dist)) \
+ return; \
+ probe_pos = next_probe_pos & TDEFL_LZ_DICT_SIZE_MASK; \
+ if ((d->m_dict[probe_pos + match_len] == c0) && (d->m_dict[probe_pos + match_len - 1] == c1)) \
+ break;
+ TDEFL_PROBE;
+ TDEFL_PROBE;
+ TDEFL_PROBE;
+ }
+ if (!dist)
+ break;
+ p = s;
+ q = d->m_dict + probe_pos;
+ for (probe_len = 0; probe_len < max_match_len; probe_len++)
+ if (*p++ != *q++)
+ break;
+ if (probe_len > match_len)
+ {
+ *pMatch_dist = dist;
+ if ((*pMatch_len = match_len = probe_len) == max_match_len)
+ return;
+ c0 = d->m_dict[pos + match_len];
+ c1 = d->m_dict[pos + match_len - 1];
+ }
+ }
+}
+#endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES */
+
+#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN
+#ifdef MINIZ_UNALIGNED_USE_MEMCPY
+ static mz_uint32 TDEFL_READ_UNALIGNED_WORD32(const mz_uint8 *p)
+ {
+ mz_uint32 ret;
+ memcpy(&ret, p, sizeof(mz_uint32));
+ return ret;
+ }
+#else
+#define TDEFL_READ_UNALIGNED_WORD32(p) *(const mz_uint32 *)(p)
+#endif
+ static mz_bool tdefl_compress_fast(tdefl_compressor *d)
+ {
+ /* Faster, minimally featured LZRW1-style match+parse loop with better register utilization. Intended for applications where raw throughput is valued more highly than ratio. */
+ mz_uint lookahead_pos = d->m_lookahead_pos, lookahead_size = d->m_lookahead_size, dict_size = d->m_dict_size, total_lz_bytes = d->m_total_lz_bytes, num_flags_left = d->m_num_flags_left;
+ mz_uint8 *pLZ_code_buf = d->m_pLZ_code_buf, *pLZ_flags = d->m_pLZ_flags;
+ mz_uint cur_pos = lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK;
+
+ while ((d->m_src_buf_left) || ((d->m_flush) && (lookahead_size)))
+ {
+ const mz_uint TDEFL_COMP_FAST_LOOKAHEAD_SIZE = 4096;
+ mz_uint dst_pos = (lookahead_pos + lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK;
+ mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(d->m_src_buf_left, TDEFL_COMP_FAST_LOOKAHEAD_SIZE - lookahead_size);
+ d->m_src_buf_left -= num_bytes_to_process;
+ lookahead_size += num_bytes_to_process;
+
+ while (num_bytes_to_process)
+ {
+ mz_uint32 n = MZ_MIN(TDEFL_LZ_DICT_SIZE - dst_pos, num_bytes_to_process);
+ memcpy(d->m_dict + dst_pos, d->m_pSrc, n);
+ if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1))
+ memcpy(d->m_dict + TDEFL_LZ_DICT_SIZE + dst_pos, d->m_pSrc, MZ_MIN(n, (TDEFL_MAX_MATCH_LEN - 1) - dst_pos));
+ d->m_pSrc += n;
+ dst_pos = (dst_pos + n) & TDEFL_LZ_DICT_SIZE_MASK;
+ num_bytes_to_process -= n;
+ }
+
+ dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - lookahead_size, dict_size);
+ if ((!d->m_flush) && (lookahead_size < TDEFL_COMP_FAST_LOOKAHEAD_SIZE))
+ break;
+
+ while (lookahead_size >= 4)
+ {
+ mz_uint cur_match_dist, cur_match_len = 1;
+ mz_uint8 *pCur_dict = d->m_dict + cur_pos;
+ mz_uint first_trigram = TDEFL_READ_UNALIGNED_WORD32(pCur_dict) & 0xFFFFFF;
+ mz_uint hash = (first_trigram ^ (first_trigram >> (24 - (TDEFL_LZ_HASH_BITS - 8)))) & TDEFL_LEVEL1_HASH_SIZE_MASK;
+ mz_uint probe_pos = d->m_hash[hash];
+ d->m_hash[hash] = (mz_uint16)lookahead_pos;
+
+ if (((cur_match_dist = (mz_uint16)(lookahead_pos - probe_pos)) <= dict_size) && ((TDEFL_READ_UNALIGNED_WORD32(d->m_dict + (probe_pos &= TDEFL_LZ_DICT_SIZE_MASK)) & 0xFFFFFF) == first_trigram))
+ {
+ const mz_uint16 *p = (const mz_uint16 *)pCur_dict;
+ const mz_uint16 *q = (const mz_uint16 *)(d->m_dict + probe_pos);
+ mz_uint32 probe_len = 32;
+ do
+ {
+ } while ((TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) &&
+ (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (TDEFL_READ_UNALIGNED_WORD2(++p) == TDEFL_READ_UNALIGNED_WORD2(++q)) && (--probe_len > 0));
+ cur_match_len = ((mz_uint)(p - (const mz_uint16 *)pCur_dict) * 2) + (mz_uint)(*(const mz_uint8 *)p == *(const mz_uint8 *)q);
+ if (!probe_len)
+ cur_match_len = cur_match_dist ? TDEFL_MAX_MATCH_LEN : 0;
+
+ if ((cur_match_len < TDEFL_MIN_MATCH_LEN) || ((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U)))
+ {
+ cur_match_len = 1;
+ *pLZ_code_buf++ = (mz_uint8)first_trigram;
+ *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1);
+ d->m_huff_count[0][(mz_uint8)first_trigram]++;
+ }
+ else
+ {
+ mz_uint32 s0, s1;
+ cur_match_len = MZ_MIN(cur_match_len, lookahead_size);
+
+ MZ_ASSERT((cur_match_len >= TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 1) && (cur_match_dist <= TDEFL_LZ_DICT_SIZE));
+
+ cur_match_dist--;
+
+ pLZ_code_buf[0] = (mz_uint8)(cur_match_len - TDEFL_MIN_MATCH_LEN);
+#ifdef MINIZ_UNALIGNED_USE_MEMCPY
+ memcpy(&pLZ_code_buf[1], &cur_match_dist, sizeof(cur_match_dist));
+#else
+ *(mz_uint16 *)(&pLZ_code_buf[1]) = (mz_uint16)cur_match_dist;
+#endif
+ pLZ_code_buf += 3;
+ *pLZ_flags = (mz_uint8)((*pLZ_flags >> 1) | 0x80);
+
+ s0 = s_tdefl_small_dist_sym[cur_match_dist & 511];
+ s1 = s_tdefl_large_dist_sym[cur_match_dist >> 8];
+ d->m_huff_count[1][(cur_match_dist < 512) ? s0 : s1]++;
+
+ d->m_huff_count[0][s_tdefl_len_sym[cur_match_len - TDEFL_MIN_MATCH_LEN]]++;
+ }
+ }
+ else
+ {
+ *pLZ_code_buf++ = (mz_uint8)first_trigram;
+ *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1);
+ d->m_huff_count[0][(mz_uint8)first_trigram]++;
+ }
+
+ if (--num_flags_left == 0)
+ {
+ num_flags_left = 8;
+ pLZ_flags = pLZ_code_buf++;
+ }
+
+ total_lz_bytes += cur_match_len;
+ lookahead_pos += cur_match_len;
+ dict_size = MZ_MIN(dict_size + cur_match_len, (mz_uint)TDEFL_LZ_DICT_SIZE);
+ cur_pos = (cur_pos + cur_match_len) & TDEFL_LZ_DICT_SIZE_MASK;
+ MZ_ASSERT(lookahead_size >= cur_match_len);
+ lookahead_size -= cur_match_len;
+
+ if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8])
+ {
+ int n;
+ d->m_lookahead_pos = lookahead_pos;
+ d->m_lookahead_size = lookahead_size;
+ d->m_dict_size = dict_size;
+ d->m_total_lz_bytes = total_lz_bytes;
+ d->m_pLZ_code_buf = pLZ_code_buf;
+ d->m_pLZ_flags = pLZ_flags;
+ d->m_num_flags_left = num_flags_left;
+ if ((n = tdefl_flush_block(d, 0)) != 0)
+ return (n < 0) ? MZ_FALSE : MZ_TRUE;
+ total_lz_bytes = d->m_total_lz_bytes;
+ pLZ_code_buf = d->m_pLZ_code_buf;
+ pLZ_flags = d->m_pLZ_flags;
+ num_flags_left = d->m_num_flags_left;
+ }
+ }
+
+ while (lookahead_size)
+ {
+ mz_uint8 lit = d->m_dict[cur_pos];
+
+ total_lz_bytes++;
+ *pLZ_code_buf++ = lit;
+ *pLZ_flags = (mz_uint8)(*pLZ_flags >> 1);
+ if (--num_flags_left == 0)
+ {
+ num_flags_left = 8;
+ pLZ_flags = pLZ_code_buf++;
+ }
+
+ d->m_huff_count[0][lit]++;
+
+ lookahead_pos++;
+ dict_size = MZ_MIN(dict_size + 1, (mz_uint)TDEFL_LZ_DICT_SIZE);
+ cur_pos = (cur_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK;
+ lookahead_size--;
+
+ if (pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8])
+ {
+ int n;
+ d->m_lookahead_pos = lookahead_pos;
+ d->m_lookahead_size = lookahead_size;
+ d->m_dict_size = dict_size;
+ d->m_total_lz_bytes = total_lz_bytes;
+ d->m_pLZ_code_buf = pLZ_code_buf;
+ d->m_pLZ_flags = pLZ_flags;
+ d->m_num_flags_left = num_flags_left;
+ if ((n = tdefl_flush_block(d, 0)) != 0)
+ return (n < 0) ? MZ_FALSE : MZ_TRUE;
+ total_lz_bytes = d->m_total_lz_bytes;
+ pLZ_code_buf = d->m_pLZ_code_buf;
+ pLZ_flags = d->m_pLZ_flags;
+ num_flags_left = d->m_num_flags_left;
+ }
+ }
+ }
+
+ d->m_lookahead_pos = lookahead_pos;
+ d->m_lookahead_size = lookahead_size;
+ d->m_dict_size = dict_size;
+ d->m_total_lz_bytes = total_lz_bytes;
+ d->m_pLZ_code_buf = pLZ_code_buf;
+ d->m_pLZ_flags = pLZ_flags;
+ d->m_num_flags_left = num_flags_left;
+ return MZ_TRUE;
+ }
+#endif /* MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */
+
+ static MZ_FORCEINLINE void tdefl_record_literal(tdefl_compressor *d, mz_uint8 lit)
+ {
+ d->m_total_lz_bytes++;
+ *d->m_pLZ_code_buf++ = lit;
+ *d->m_pLZ_flags = (mz_uint8)(*d->m_pLZ_flags >> 1);
+ if (--d->m_num_flags_left == 0)
+ {
+ d->m_num_flags_left = 8;
+ d->m_pLZ_flags = d->m_pLZ_code_buf++;
+ }
+ d->m_huff_count[0][lit]++;
+ }
+
+ static MZ_FORCEINLINE void tdefl_record_match(tdefl_compressor *d, mz_uint match_len, mz_uint match_dist)
+ {
+ mz_uint32 s0, s1;
+
+ MZ_ASSERT((match_len >= TDEFL_MIN_MATCH_LEN) && (match_dist >= 1) && (match_dist <= TDEFL_LZ_DICT_SIZE));
+
+ d->m_total_lz_bytes += match_len;
+
+ d->m_pLZ_code_buf[0] = (mz_uint8)(match_len - TDEFL_MIN_MATCH_LEN);
+
+ match_dist -= 1;
+ d->m_pLZ_code_buf[1] = (mz_uint8)(match_dist & 0xFF);
+ d->m_pLZ_code_buf[2] = (mz_uint8)(match_dist >> 8);
+ d->m_pLZ_code_buf += 3;
+
+ *d->m_pLZ_flags = (mz_uint8)((*d->m_pLZ_flags >> 1) | 0x80);
+ if (--d->m_num_flags_left == 0)
+ {
+ d->m_num_flags_left = 8;
+ d->m_pLZ_flags = d->m_pLZ_code_buf++;
+ }
+
+ s0 = s_tdefl_small_dist_sym[match_dist & 511];
+ s1 = s_tdefl_large_dist_sym[(match_dist >> 8) & 127];
+ d->m_huff_count[1][(match_dist < 512) ? s0 : s1]++;
+ d->m_huff_count[0][s_tdefl_len_sym[match_len - TDEFL_MIN_MATCH_LEN]]++;
+ }
+
+ static mz_bool tdefl_compress_normal(tdefl_compressor *d)
+ {
+ const mz_uint8 *pSrc = d->m_pSrc;
+ size_t src_buf_left = d->m_src_buf_left;
+ tdefl_flush flush = d->m_flush;
+
+ while ((src_buf_left) || ((flush) && (d->m_lookahead_size)))
+ {
+ mz_uint len_to_move, cur_match_dist, cur_match_len, cur_pos;
+ /* Update dictionary and hash chains. Keeps the lookahead size equal to TDEFL_MAX_MATCH_LEN. */
+ if ((d->m_lookahead_size + d->m_dict_size) >= (TDEFL_MIN_MATCH_LEN - 1))
+ {
+ mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK, ins_pos = d->m_lookahead_pos + d->m_lookahead_size - 2;
+ mz_uint hash = (d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK];
+ mz_uint num_bytes_to_process = (mz_uint)MZ_MIN(src_buf_left, TDEFL_MAX_MATCH_LEN - d->m_lookahead_size);
+ const mz_uint8 *pSrc_end = pSrc ? pSrc + num_bytes_to_process : NULL;
+ src_buf_left -= num_bytes_to_process;
+ d->m_lookahead_size += num_bytes_to_process;
+ while (pSrc != pSrc_end)
+ {
+ mz_uint8 c = *pSrc++;
+ d->m_dict[dst_pos] = c;
+ if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1))
+ d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c;
+ hash = ((hash << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1);
+ d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash];
+ d->m_hash[hash] = (mz_uint16)(ins_pos);
+ dst_pos = (dst_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK;
+ ins_pos++;
+ }
+ }
+ else
+ {
+ while ((src_buf_left) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN))
+ {
+ mz_uint8 c = *pSrc++;
+ mz_uint dst_pos = (d->m_lookahead_pos + d->m_lookahead_size) & TDEFL_LZ_DICT_SIZE_MASK;
+ src_buf_left--;
+ d->m_dict[dst_pos] = c;
+ if (dst_pos < (TDEFL_MAX_MATCH_LEN - 1))
+ d->m_dict[TDEFL_LZ_DICT_SIZE + dst_pos] = c;
+ if ((++d->m_lookahead_size + d->m_dict_size) >= TDEFL_MIN_MATCH_LEN)
+ {
+ mz_uint ins_pos = d->m_lookahead_pos + (d->m_lookahead_size - 1) - 2;
+ mz_uint hash = ((d->m_dict[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] << (TDEFL_LZ_HASH_SHIFT * 2)) ^ (d->m_dict[(ins_pos + 1) & TDEFL_LZ_DICT_SIZE_MASK] << TDEFL_LZ_HASH_SHIFT) ^ c) & (TDEFL_LZ_HASH_SIZE - 1);
+ d->m_next[ins_pos & TDEFL_LZ_DICT_SIZE_MASK] = d->m_hash[hash];
+ d->m_hash[hash] = (mz_uint16)(ins_pos);
+ }
+ }
+ }
+ d->m_dict_size = MZ_MIN(TDEFL_LZ_DICT_SIZE - d->m_lookahead_size, d->m_dict_size);
+ if ((!flush) && (d->m_lookahead_size < TDEFL_MAX_MATCH_LEN))
+ break;
+
+ /* Simple lazy/greedy parsing state machine. */
+ len_to_move = 1;
+ cur_match_dist = 0;
+ cur_match_len = d->m_saved_match_len ? d->m_saved_match_len : (TDEFL_MIN_MATCH_LEN - 1);
+ cur_pos = d->m_lookahead_pos & TDEFL_LZ_DICT_SIZE_MASK;
+ if (d->m_flags & (TDEFL_RLE_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS))
+ {
+ if ((d->m_dict_size) && (!(d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS)))
+ {
+ mz_uint8 c = d->m_dict[(cur_pos - 1) & TDEFL_LZ_DICT_SIZE_MASK];
+ cur_match_len = 0;
+ while (cur_match_len < d->m_lookahead_size)
+ {
+ if (d->m_dict[cur_pos + cur_match_len] != c)
+ break;
+ cur_match_len++;
+ }
+ if (cur_match_len < TDEFL_MIN_MATCH_LEN)
+ cur_match_len = 0;
+ else
+ cur_match_dist = 1;
+ }
+ }
+ else
+ {
+ tdefl_find_match(d, d->m_lookahead_pos, d->m_dict_size, d->m_lookahead_size, &cur_match_dist, &cur_match_len);
+ }
+ if (((cur_match_len == TDEFL_MIN_MATCH_LEN) && (cur_match_dist >= 8U * 1024U)) || (cur_pos == cur_match_dist) || ((d->m_flags & TDEFL_FILTER_MATCHES) && (cur_match_len <= 5)))
+ {
+ cur_match_dist = cur_match_len = 0;
+ }
+ if (d->m_saved_match_len)
+ {
+ if (cur_match_len > d->m_saved_match_len)
+ {
+ tdefl_record_literal(d, (mz_uint8)d->m_saved_lit);
+ if (cur_match_len >= 128)
+ {
+ tdefl_record_match(d, cur_match_len, cur_match_dist);
+ d->m_saved_match_len = 0;
+ len_to_move = cur_match_len;
+ }
+ else
+ {
+ d->m_saved_lit = d->m_dict[cur_pos];
+ d->m_saved_match_dist = cur_match_dist;
+ d->m_saved_match_len = cur_match_len;
+ }
+ }
+ else
+ {
+ tdefl_record_match(d, d->m_saved_match_len, d->m_saved_match_dist);
+ len_to_move = d->m_saved_match_len - 1;
+ d->m_saved_match_len = 0;
+ }
+ }
+ else if (!cur_match_dist)
+ tdefl_record_literal(d, d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)]);
+ else if ((d->m_greedy_parsing) || (d->m_flags & TDEFL_RLE_MATCHES) || (cur_match_len >= 128))
+ {
+ tdefl_record_match(d, cur_match_len, cur_match_dist);
+ len_to_move = cur_match_len;
+ }
+ else
+ {
+ d->m_saved_lit = d->m_dict[MZ_MIN(cur_pos, sizeof(d->m_dict) - 1)];
+ d->m_saved_match_dist = cur_match_dist;
+ d->m_saved_match_len = cur_match_len;
+ }
+ /* Move the lookahead forward by len_to_move bytes. */
+ d->m_lookahead_pos += len_to_move;
+ MZ_ASSERT(d->m_lookahead_size >= len_to_move);
+ d->m_lookahead_size -= len_to_move;
+ d->m_dict_size = MZ_MIN(d->m_dict_size + len_to_move, (mz_uint)TDEFL_LZ_DICT_SIZE);
+ /* Check if it's time to flush the current LZ codes to the internal output buffer. */
+ if ((d->m_pLZ_code_buf > &d->m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE - 8]) ||
+ ((d->m_total_lz_bytes > 31 * 1024) && (((((mz_uint)(d->m_pLZ_code_buf - d->m_lz_code_buf) * 115) >> 7) >= d->m_total_lz_bytes) || (d->m_flags & TDEFL_FORCE_ALL_RAW_BLOCKS))))
+ {
+ int n;
+ d->m_pSrc = pSrc;
+ d->m_src_buf_left = src_buf_left;
+ if ((n = tdefl_flush_block(d, 0)) != 0)
+ return (n < 0) ? MZ_FALSE : MZ_TRUE;
+ }
+ }
+
+ d->m_pSrc = pSrc;
+ d->m_src_buf_left = src_buf_left;
+ return MZ_TRUE;
+ }
+
+ static tdefl_status tdefl_flush_output_buffer(tdefl_compressor *d)
+ {
+ if (d->m_pIn_buf_size)
+ {
+ *d->m_pIn_buf_size = d->m_pSrc - (const mz_uint8 *)d->m_pIn_buf;
+ }
+
+ if (d->m_pOut_buf_size)
+ {
+ size_t n = MZ_MIN(*d->m_pOut_buf_size - d->m_out_buf_ofs, d->m_output_flush_remaining);
+ memcpy((mz_uint8 *)d->m_pOut_buf + d->m_out_buf_ofs, d->m_output_buf + d->m_output_flush_ofs, n);
+ d->m_output_flush_ofs += (mz_uint)n;
+ d->m_output_flush_remaining -= (mz_uint)n;
+ d->m_out_buf_ofs += n;
+
+ *d->m_pOut_buf_size = d->m_out_buf_ofs;
+ }
+
+ return (d->m_finished && !d->m_output_flush_remaining) ? TDEFL_STATUS_DONE : TDEFL_STATUS_OKAY;
+ }
+
+ tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush)
+ {
+ if (!d)
+ {
+ if (pIn_buf_size)
+ *pIn_buf_size = 0;
+ if (pOut_buf_size)
+ *pOut_buf_size = 0;
+ return TDEFL_STATUS_BAD_PARAM;
+ }
+
+ d->m_pIn_buf = pIn_buf;
+ d->m_pIn_buf_size = pIn_buf_size;
+ d->m_pOut_buf = pOut_buf;
+ d->m_pOut_buf_size = pOut_buf_size;
+ d->m_pSrc = (const mz_uint8 *)(pIn_buf);
+ d->m_src_buf_left = pIn_buf_size ? *pIn_buf_size : 0;
+ d->m_out_buf_ofs = 0;
+ d->m_flush = flush;
+
+ if (((d->m_pPut_buf_func != NULL) == ((pOut_buf != NULL) || (pOut_buf_size != NULL))) || (d->m_prev_return_status != TDEFL_STATUS_OKAY) ||
+ (d->m_wants_to_finish && (flush != TDEFL_FINISH)) || (pIn_buf_size && *pIn_buf_size && !pIn_buf) || (pOut_buf_size && *pOut_buf_size && !pOut_buf))
+ {
+ if (pIn_buf_size)
+ *pIn_buf_size = 0;
+ if (pOut_buf_size)
+ *pOut_buf_size = 0;
+ return (d->m_prev_return_status = TDEFL_STATUS_BAD_PARAM);
+ }
+ d->m_wants_to_finish |= (flush == TDEFL_FINISH);
+
+ if ((d->m_output_flush_remaining) || (d->m_finished))
+ return (d->m_prev_return_status = tdefl_flush_output_buffer(d));
+
+#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN
+ if (((d->m_flags & TDEFL_MAX_PROBES_MASK) == 1) &&
+ ((d->m_flags & TDEFL_GREEDY_PARSING_FLAG) != 0) &&
+ ((d->m_flags & (TDEFL_FILTER_MATCHES | TDEFL_FORCE_ALL_RAW_BLOCKS | TDEFL_RLE_MATCHES)) == 0))
+ {
+ if (!tdefl_compress_fast(d))
+ return d->m_prev_return_status;
+ }
+ else
+#endif /* #if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN */
+ {
+ if (!tdefl_compress_normal(d))
+ return d->m_prev_return_status;
+ }
+
+ if ((d->m_flags & (TDEFL_WRITE_ZLIB_HEADER | TDEFL_COMPUTE_ADLER32)) && (pIn_buf))
+ d->m_adler32 = (mz_uint32)mz_adler32(d->m_adler32, (const mz_uint8 *)pIn_buf, d->m_pSrc - (const mz_uint8 *)pIn_buf);
+
+ if ((flush) && (!d->m_lookahead_size) && (!d->m_src_buf_left) && (!d->m_output_flush_remaining))
+ {
+ if (tdefl_flush_block(d, flush) < 0)
+ return d->m_prev_return_status;
+ d->m_finished = (flush == TDEFL_FINISH);
+ if (flush == TDEFL_FULL_FLUSH)
+ {
+ MZ_CLEAR_ARR(d->m_hash);
+ MZ_CLEAR_ARR(d->m_next);
+ d->m_dict_size = 0;
+ }
+ }
+
+ return (d->m_prev_return_status = tdefl_flush_output_buffer(d));
+ }
+
+ tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush)
+ {
+ MZ_ASSERT(d->m_pPut_buf_func);
+ return tdefl_compress(d, pIn_buf, &in_buf_size, NULL, NULL, flush);
+ }
+
+ tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags)
+ {
+ d->m_pPut_buf_func = pPut_buf_func;
+ d->m_pPut_buf_user = pPut_buf_user;
+ d->m_flags = (mz_uint)(flags);
+ d->m_max_probes[0] = 1 + ((flags & 0xFFF) + 2) / 3;
+ d->m_greedy_parsing = (flags & TDEFL_GREEDY_PARSING_FLAG) != 0;
+ d->m_max_probes[1] = 1 + (((flags & 0xFFF) >> 2) + 2) / 3;
+ if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG))
+ MZ_CLEAR_ARR(d->m_hash);
+ d->m_lookahead_pos = d->m_lookahead_size = d->m_dict_size = d->m_total_lz_bytes = d->m_lz_code_buf_dict_pos = d->m_bits_in = 0;
+ d->m_output_flush_ofs = d->m_output_flush_remaining = d->m_finished = d->m_block_index = d->m_bit_buffer = d->m_wants_to_finish = 0;
+ d->m_pLZ_code_buf = d->m_lz_code_buf + 1;
+ d->m_pLZ_flags = d->m_lz_code_buf;
+ *d->m_pLZ_flags = 0;
+ d->m_num_flags_left = 8;
+ d->m_pOutput_buf = d->m_output_buf;
+ d->m_pOutput_buf_end = d->m_output_buf;
+ d->m_prev_return_status = TDEFL_STATUS_OKAY;
+ d->m_saved_match_dist = d->m_saved_match_len = d->m_saved_lit = 0;
+ d->m_adler32 = 1;
+ d->m_pIn_buf = NULL;
+ d->m_pOut_buf = NULL;
+ d->m_pIn_buf_size = NULL;
+ d->m_pOut_buf_size = NULL;
+ d->m_flush = TDEFL_NO_FLUSH;
+ d->m_pSrc = NULL;
+ d->m_src_buf_left = 0;
+ d->m_out_buf_ofs = 0;
+ if (!(flags & TDEFL_NONDETERMINISTIC_PARSING_FLAG))
+ MZ_CLEAR_ARR(d->m_dict);
+ memset(&d->m_huff_count[0][0], 0, sizeof(d->m_huff_count[0][0]) * TDEFL_MAX_HUFF_SYMBOLS_0);
+ memset(&d->m_huff_count[1][0], 0, sizeof(d->m_huff_count[1][0]) * TDEFL_MAX_HUFF_SYMBOLS_1);
+ return TDEFL_STATUS_OKAY;
+ }
+
+ tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d)
+ {
+ return d->m_prev_return_status;
+ }
+
+ mz_uint32 tdefl_get_adler32(tdefl_compressor *d)
+ {
+ return d->m_adler32;
+ }
+
+ mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags)
+ {
+ tdefl_compressor *pComp;
+ mz_bool succeeded;
+ if (((buf_len) && (!pBuf)) || (!pPut_buf_func))
+ return MZ_FALSE;
+ pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor));
+ if (!pComp)
+ return MZ_FALSE;
+ succeeded = (tdefl_init(pComp, pPut_buf_func, pPut_buf_user, flags) == TDEFL_STATUS_OKAY);
+ succeeded = succeeded && (tdefl_compress_buffer(pComp, pBuf, buf_len, TDEFL_FINISH) == TDEFL_STATUS_DONE);
+ MZ_FREE(pComp);
+ return succeeded;
+ }
+
+ typedef struct
+ {
+ size_t m_size, m_capacity;
+ mz_uint8 *m_pBuf;
+ mz_bool m_expandable;
+ } tdefl_output_buffer;
+
+ static mz_bool tdefl_output_buffer_putter(const void *pBuf, int len, void *pUser)
+ {
+ tdefl_output_buffer *p = (tdefl_output_buffer *)pUser;
+ size_t new_size = p->m_size + len;
+ if (new_size > p->m_capacity)
+ {
+ size_t new_capacity = p->m_capacity;
+ mz_uint8 *pNew_buf;
+ if (!p->m_expandable)
+ return MZ_FALSE;
+ do
+ {
+ new_capacity = MZ_MAX(128U, new_capacity << 1U);
+ } while (new_size > new_capacity);
+ pNew_buf = (mz_uint8 *)MZ_REALLOC(p->m_pBuf, new_capacity);
+ if (!pNew_buf)
+ return MZ_FALSE;
+ p->m_pBuf = pNew_buf;
+ p->m_capacity = new_capacity;
+ }
+ memcpy((mz_uint8 *)p->m_pBuf + p->m_size, pBuf, len);
+ p->m_size = new_size;
+ return MZ_TRUE;
+ }
+
+ void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags)
+ {
+ tdefl_output_buffer out_buf;
+ MZ_CLEAR_OBJ(out_buf);
+ if (!pOut_len)
+ return MZ_FALSE;
+ else
+ *pOut_len = 0;
+ out_buf.m_expandable = MZ_TRUE;
+ if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags))
+ return NULL;
+ *pOut_len = out_buf.m_size;
+ return out_buf.m_pBuf;
+ }
+
+ size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags)
+ {
+ tdefl_output_buffer out_buf;
+ MZ_CLEAR_OBJ(out_buf);
+ if (!pOut_buf)
+ return 0;
+ out_buf.m_pBuf = (mz_uint8 *)pOut_buf;
+ out_buf.m_capacity = out_buf_len;
+ if (!tdefl_compress_mem_to_output(pSrc_buf, src_buf_len, tdefl_output_buffer_putter, &out_buf, flags))
+ return 0;
+ return out_buf.m_size;
+ }
+
+ /* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */
+ mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy)
+ {
+ mz_uint comp_flags = s_tdefl_num_probes[(level >= 0) ? MZ_MIN(10, level) : MZ_DEFAULT_LEVEL] | ((level <= 3) ? TDEFL_GREEDY_PARSING_FLAG : 0);
+ if (window_bits > 0)
+ comp_flags |= TDEFL_WRITE_ZLIB_HEADER;
+
+ if (!level)
+ comp_flags |= TDEFL_FORCE_ALL_RAW_BLOCKS;
+ else if (strategy == MZ_FILTERED)
+ comp_flags |= TDEFL_FILTER_MATCHES;
+ else if (strategy == MZ_HUFFMAN_ONLY)
+ comp_flags &= ~TDEFL_MAX_PROBES_MASK;
+ else if (strategy == MZ_FIXED)
+ comp_flags |= TDEFL_FORCE_ALL_STATIC_BLOCKS;
+ else if (strategy == MZ_RLE)
+ comp_flags |= TDEFL_RLE_MATCHES;
+
+ return comp_flags;
+ }
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4204) /* nonstandard extension used : non-constant aggregate initializer (also supported by GNU C and C99, so no big deal) */
+#endif
+
+ /* Simple PNG writer function by Alex Evans, 2011. Released into the public domain: https://gist.github.com/908299, more context at
+ http://altdevblogaday.org/2011/04/06/a-smaller-jpg-encoder/.
+ This is actually a modification of Alex's original code so PNG files generated by this function pass pngcheck. */
+ void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip)
+ {
+ /* Using a local copy of this array here in case MINIZ_NO_ZLIB_APIS was defined. */
+ static const mz_uint s_tdefl_png_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
+ tdefl_compressor *pComp = (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor));
+ tdefl_output_buffer out_buf;
+ int i, bpl = w * num_chans, y, z;
+ mz_uint32 c;
+ *pLen_out = 0;
+ if (!pComp)
+ return NULL;
+ if (w <= 0 || h <= 0 || w > 0xFFFF || h > 0xFFFF || num_chans < 1 || num_chans > 4)
+ {
+ MZ_FREE(pComp);
+ return NULL;
+ }
+ MZ_CLEAR_OBJ(out_buf);
+ out_buf.m_expandable = MZ_TRUE;
+ out_buf.m_capacity = 57 + MZ_MAX(64, (1 + bpl) * h);
+ if (NULL == (out_buf.m_pBuf = (mz_uint8 *)MZ_MALLOC(out_buf.m_capacity)))
+ {
+ MZ_FREE(pComp);
+ return NULL;
+ }
+ /* write dummy header */
+ for (z = 41; z; --z)
+ tdefl_output_buffer_putter(&z, 1, &out_buf);
+ /* compress image data */
+ tdefl_init(pComp, tdefl_output_buffer_putter, &out_buf, s_tdefl_png_num_probes[MZ_MIN(10, level)] | TDEFL_WRITE_ZLIB_HEADER);
+ for (y = 0; y < h; ++y)
+ {
+ tdefl_compress_buffer(pComp, &z, 1, TDEFL_NO_FLUSH);
+ tdefl_compress_buffer(pComp, (mz_uint8 *)pImage + (flip ? (h - 1 - y) : y) * bpl, bpl, TDEFL_NO_FLUSH);
+ }
+ if (tdefl_compress_buffer(pComp, NULL, 0, TDEFL_FINISH) != TDEFL_STATUS_DONE)
+ {
+ MZ_FREE(pComp);
+ MZ_FREE(out_buf.m_pBuf);
+ return NULL;
+ }
+ /* write real header */
+ *pLen_out = out_buf.m_size - 41;
+ {
+ static const mz_uint8 chans[] = { 0x00, 0x00, 0x04, 0x02, 0x06 };
+ mz_uint8 pnghdr[41] = { 0x89, 0x50, 0x4e, 0x47, 0x0d,
+ 0x0a, 0x1a, 0x0a, 0x00, 0x00,
+ 0x00, 0x0d, 0x49, 0x48, 0x44,
+ 0x52, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x08,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x49, 0x44, 0x41,
+ 0x54 };
+ pnghdr[18] = (mz_uint8)(w >> 8);
+ pnghdr[19] = (mz_uint8)w;
+ pnghdr[22] = (mz_uint8)(h >> 8);
+ pnghdr[23] = (mz_uint8)h;
+ pnghdr[25] = chans[num_chans];
+ pnghdr[33] = (mz_uint8)(*pLen_out >> 24);
+ pnghdr[34] = (mz_uint8)(*pLen_out >> 16);
+ pnghdr[35] = (mz_uint8)(*pLen_out >> 8);
+ pnghdr[36] = (mz_uint8)*pLen_out;
+ c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, pnghdr + 12, 17);
+ for (i = 0; i < 4; ++i, c <<= 8)
+ ((mz_uint8 *)(pnghdr + 29))[i] = (mz_uint8)(c >> 24);
+ memcpy(out_buf.m_pBuf, pnghdr, 41);
+ }
+ /* write footer (IDAT CRC-32, followed by IEND chunk) */
+ if (!tdefl_output_buffer_putter("\0\0\0\0\0\0\0\0\x49\x45\x4e\x44\xae\x42\x60\x82", 16, &out_buf))
+ {
+ *pLen_out = 0;
+ MZ_FREE(pComp);
+ MZ_FREE(out_buf.m_pBuf);
+ return NULL;
+ }
+ c = (mz_uint32)mz_crc32(MZ_CRC32_INIT, out_buf.m_pBuf + 41 - 4, *pLen_out + 4);
+ for (i = 0; i < 4; ++i, c <<= 8)
+ (out_buf.m_pBuf + out_buf.m_size - 16)[i] = (mz_uint8)(c >> 24);
+ /* compute final size of file, grab compressed data buffer and return */
+ *pLen_out += 57;
+ MZ_FREE(pComp);
+ return out_buf.m_pBuf;
+ }
+ void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out)
+ {
+ /* Level 6 corresponds to TDEFL_DEFAULT_MAX_PROBES or MZ_DEFAULT_LEVEL (but we can't depend on MZ_DEFAULT_LEVEL being available in case the zlib API's where #defined out) */
+ return tdefl_write_image_to_png_file_in_memory_ex(pImage, w, h, num_chans, pLen_out, 6, MZ_FALSE);
+ }
+
+#ifndef MINIZ_NO_MALLOC
+ /* Allocate the tdefl_compressor and tinfl_decompressor structures in C so that */
+ /* non-C language bindings to tdefL_ and tinfl_ API don't need to worry about */
+ /* structure size and allocation mechanism. */
+ tdefl_compressor *tdefl_compressor_alloc(void)
+ {
+ return (tdefl_compressor *)MZ_MALLOC(sizeof(tdefl_compressor));
+ }
+
+ void tdefl_compressor_free(tdefl_compressor *pComp)
+ {
+ MZ_FREE(pComp);
+ }
+#endif
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
+ /**************************************************************************
+ *
+ * Copyright 2013-2014 RAD Game Tools and Valve Software
+ * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
+ * All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ **************************************************************************/
+
+
+
+#ifndef MINIZ_NO_INFLATE_APIS
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ /* ------------------- Low-level Decompression (completely independent from all compression API's) */
+
+#define TINFL_MEMCPY(d, s, l) memcpy(d, s, l)
+#define TINFL_MEMSET(p, c, l) memset(p, c, l)
+
+#define TINFL_CR_BEGIN \
+ switch (r->m_state) \
+ { \
+ case 0:
+#define TINFL_CR_RETURN(state_index, result) \
+ do \
+ { \
+ status = result; \
+ r->m_state = state_index; \
+ goto common_exit; \
+ case state_index:; \
+ } \
+ MZ_MACRO_END
+#define TINFL_CR_RETURN_FOREVER(state_index, result) \
+ do \
+ { \
+ for (;;) \
+ { \
+ TINFL_CR_RETURN(state_index, result); \
+ } \
+ } \
+ MZ_MACRO_END
+#define TINFL_CR_FINISH }
+
+#define TINFL_GET_BYTE(state_index, c) \
+ do \
+ { \
+ while (pIn_buf_cur >= pIn_buf_end) \
+ { \
+ TINFL_CR_RETURN(state_index, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS); \
+ } \
+ c = *pIn_buf_cur++; \
+ } \
+ MZ_MACRO_END
+
+#define TINFL_NEED_BITS(state_index, n) \
+ do \
+ { \
+ mz_uint c; \
+ TINFL_GET_BYTE(state_index, c); \
+ bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \
+ num_bits += 8; \
+ } while (num_bits < (mz_uint)(n))
+#define TINFL_SKIP_BITS(state_index, n) \
+ do \
+ { \
+ if (num_bits < (mz_uint)(n)) \
+ { \
+ TINFL_NEED_BITS(state_index, n); \
+ } \
+ bit_buf >>= (n); \
+ num_bits -= (n); \
+ } \
+ MZ_MACRO_END
+#define TINFL_GET_BITS(state_index, b, n) \
+ do \
+ { \
+ if (num_bits < (mz_uint)(n)) \
+ { \
+ TINFL_NEED_BITS(state_index, n); \
+ } \
+ b = bit_buf & ((1 << (n)) - 1); \
+ bit_buf >>= (n); \
+ num_bits -= (n); \
+ } \
+ MZ_MACRO_END
+
+/* TINFL_HUFF_BITBUF_FILL() is only used rarely, when the number of bytes remaining in the input buffer falls below 2. */
+/* It reads just enough bytes from the input stream that are needed to decode the next Huffman code (and absolutely no more). It works by trying to fully decode a */
+/* Huffman code by using whatever bits are currently present in the bit buffer. If this fails, it reads another byte, and tries again until it succeeds or until the */
+/* bit buffer contains >=15 bits (deflate's max. Huffman code size). */
+#define TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree) \
+ do \
+ { \
+ temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]; \
+ if (temp >= 0) \
+ { \
+ code_len = temp >> 9; \
+ if ((code_len) && (num_bits >= code_len)) \
+ break; \
+ } \
+ else if (num_bits > TINFL_FAST_LOOKUP_BITS) \
+ { \
+ code_len = TINFL_FAST_LOOKUP_BITS; \
+ do \
+ { \
+ temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \
+ } while ((temp < 0) && (num_bits >= (code_len + 1))); \
+ if (temp >= 0) \
+ break; \
+ } \
+ TINFL_GET_BYTE(state_index, c); \
+ bit_buf |= (((tinfl_bit_buf_t)c) << num_bits); \
+ num_bits += 8; \
+ } while (num_bits < 15);
+
+/* TINFL_HUFF_DECODE() decodes the next Huffman coded symbol. It's more complex than you would initially expect because the zlib API expects the decompressor to never read */
+/* beyond the final byte of the deflate stream. (In other words, when this macro wants to read another byte from the input, it REALLY needs another byte in order to fully */
+/* decode the next Huffman code.) Handling this properly is particularly important on raw deflate (non-zlib) streams, which aren't followed by a byte aligned adler-32. */
+/* The slow path is only executed at the very end of the input buffer. */
+/* v1.16: The original macro handled the case at the very end of the passed-in input buffer, but we also need to handle the case where the user passes in 1+zillion bytes */
+/* following the deflate data and our non-conservative read-ahead path won't kick in here on this code. This is much trickier. */
+#define TINFL_HUFF_DECODE(state_index, sym, pLookUp, pTree) \
+ do \
+ { \
+ int temp; \
+ mz_uint code_len, c; \
+ if (num_bits < 15) \
+ { \
+ if ((pIn_buf_end - pIn_buf_cur) < 2) \
+ { \
+ TINFL_HUFF_BITBUF_FILL(state_index, pLookUp, pTree); \
+ } \
+ else \
+ { \
+ bit_buf |= (((tinfl_bit_buf_t)pIn_buf_cur[0]) << num_bits) | (((tinfl_bit_buf_t)pIn_buf_cur[1]) << (num_bits + 8)); \
+ pIn_buf_cur += 2; \
+ num_bits += 16; \
+ } \
+ } \
+ if ((temp = pLookUp[bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0) \
+ code_len = temp >> 9, temp &= 511; \
+ else \
+ { \
+ code_len = TINFL_FAST_LOOKUP_BITS; \
+ do \
+ { \
+ temp = pTree[~temp + ((bit_buf >> code_len++) & 1)]; \
+ } while (temp < 0); \
+ } \
+ sym = temp; \
+ bit_buf >>= code_len; \
+ num_bits -= code_len; \
+ } \
+ MZ_MACRO_END
+
+ static void tinfl_clear_tree(tinfl_decompressor *r)
+ {
+ if (r->m_type == 0)
+ MZ_CLEAR_ARR(r->m_tree_0);
+ else if (r->m_type == 1)
+ MZ_CLEAR_ARR(r->m_tree_1);
+ else
+ MZ_CLEAR_ARR(r->m_tree_2);
+ }
+
+ tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags)
+ {
+ static const mz_uint16 s_length_base[31] = { 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 };
+ static const mz_uint8 s_length_extra[31] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0 };
+ static const mz_uint16 s_dist_base[32] = { 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 0, 0 };
+ static const mz_uint8 s_dist_extra[32] = { 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13 };
+ static const mz_uint8 s_length_dezigzag[19] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 };
+ static const mz_uint16 s_min_table_sizes[3] = { 257, 1, 4 };
+
+ mz_int16 *pTrees[3];
+ mz_uint8 *pCode_sizes[3];
+
+ tinfl_status status = TINFL_STATUS_FAILED;
+ mz_uint32 num_bits, dist, counter, num_extra;
+ tinfl_bit_buf_t bit_buf;
+ const mz_uint8 *pIn_buf_cur = pIn_buf_next, *const pIn_buf_end = pIn_buf_next + *pIn_buf_size;
+ mz_uint8 *pOut_buf_cur = pOut_buf_next, *const pOut_buf_end = pOut_buf_next ? pOut_buf_next + *pOut_buf_size : NULL;
+ size_t out_buf_size_mask = (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF) ? (size_t)-1 : ((pOut_buf_next - pOut_buf_start) + *pOut_buf_size) - 1, dist_from_out_buf_start;
+
+ /* Ensure the output buffer's size is a power of 2, unless the output buffer is large enough to hold the entire output file (in which case it doesn't matter). */
+ if (((out_buf_size_mask + 1) & out_buf_size_mask) || (pOut_buf_next < pOut_buf_start))
+ {
+ *pIn_buf_size = *pOut_buf_size = 0;
+ return TINFL_STATUS_BAD_PARAM;
+ }
+
+ pTrees[0] = r->m_tree_0;
+ pTrees[1] = r->m_tree_1;
+ pTrees[2] = r->m_tree_2;
+ pCode_sizes[0] = r->m_code_size_0;
+ pCode_sizes[1] = r->m_code_size_1;
+ pCode_sizes[2] = r->m_code_size_2;
+
+ num_bits = r->m_num_bits;
+ bit_buf = r->m_bit_buf;
+ dist = r->m_dist;
+ counter = r->m_counter;
+ num_extra = r->m_num_extra;
+ dist_from_out_buf_start = r->m_dist_from_out_buf_start;
+ TINFL_CR_BEGIN
+
+ bit_buf = num_bits = dist = counter = num_extra = r->m_zhdr0 = r->m_zhdr1 = 0;
+ r->m_z_adler32 = r->m_check_adler32 = 1;
+ if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER)
+ {
+ TINFL_GET_BYTE(1, r->m_zhdr0);
+ TINFL_GET_BYTE(2, r->m_zhdr1);
+ counter = (((r->m_zhdr0 * 256 + r->m_zhdr1) % 31 != 0) || (r->m_zhdr1 & 32) || ((r->m_zhdr0 & 15) != 8));
+ if (!(decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF))
+ counter |= (((1U << (8U + (r->m_zhdr0 >> 4))) > 32768U) || ((out_buf_size_mask + 1) < (size_t)((size_t)1 << (8U + (r->m_zhdr0 >> 4)))));
+ if (counter)
+ {
+ TINFL_CR_RETURN_FOREVER(36, TINFL_STATUS_FAILED);
+ }
+ }
+
+ do
+ {
+ TINFL_GET_BITS(3, r->m_final, 3);
+ r->m_type = r->m_final >> 1;
+ if (r->m_type == 0)
+ {
+ TINFL_SKIP_BITS(5, num_bits & 7);
+ for (counter = 0; counter < 4; ++counter)
+ {
+ if (num_bits)
+ TINFL_GET_BITS(6, r->m_raw_header[counter], 8);
+ else
+ TINFL_GET_BYTE(7, r->m_raw_header[counter]);
+ }
+ if ((counter = (r->m_raw_header[0] | (r->m_raw_header[1] << 8))) != (mz_uint)(0xFFFF ^ (r->m_raw_header[2] | (r->m_raw_header[3] << 8))))
+ {
+ TINFL_CR_RETURN_FOREVER(39, TINFL_STATUS_FAILED);
+ }
+ while ((counter) && (num_bits))
+ {
+ TINFL_GET_BITS(51, dist, 8);
+ while (pOut_buf_cur >= pOut_buf_end)
+ {
+ TINFL_CR_RETURN(52, TINFL_STATUS_HAS_MORE_OUTPUT);
+ }
+ *pOut_buf_cur++ = (mz_uint8)dist;
+ counter--;
+ }
+ while (counter)
+ {
+ size_t n;
+ while (pOut_buf_cur >= pOut_buf_end)
+ {
+ TINFL_CR_RETURN(9, TINFL_STATUS_HAS_MORE_OUTPUT);
+ }
+ while (pIn_buf_cur >= pIn_buf_end)
+ {
+ TINFL_CR_RETURN(38, (decomp_flags & TINFL_FLAG_HAS_MORE_INPUT) ? TINFL_STATUS_NEEDS_MORE_INPUT : TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS);
+ }
+ n = MZ_MIN(MZ_MIN((size_t)(pOut_buf_end - pOut_buf_cur), (size_t)(pIn_buf_end - pIn_buf_cur)), counter);
+ TINFL_MEMCPY(pOut_buf_cur, pIn_buf_cur, n);
+ pIn_buf_cur += n;
+ pOut_buf_cur += n;
+ counter -= (mz_uint)n;
+ }
+ }
+ else if (r->m_type == 3)
+ {
+ TINFL_CR_RETURN_FOREVER(10, TINFL_STATUS_FAILED);
+ }
+ else
+ {
+ if (r->m_type == 1)
+ {
+ mz_uint8 *p = r->m_code_size_0;
+ mz_uint i;
+ r->m_table_sizes[0] = 288;
+ r->m_table_sizes[1] = 32;
+ TINFL_MEMSET(r->m_code_size_1, 5, 32);
+ for (i = 0; i <= 143; ++i)
+ *p++ = 8;
+ for (; i <= 255; ++i)
+ *p++ = 9;
+ for (; i <= 279; ++i)
+ *p++ = 7;
+ for (; i <= 287; ++i)
+ *p++ = 8;
+ }
+ else
+ {
+ for (counter = 0; counter < 3; counter++)
+ {
+ TINFL_GET_BITS(11, r->m_table_sizes[counter], "\05\05\04"[counter]);
+ r->m_table_sizes[counter] += s_min_table_sizes[counter];
+ }
+ MZ_CLEAR_ARR(r->m_code_size_2);
+ for (counter = 0; counter < r->m_table_sizes[2]; counter++)
+ {
+ mz_uint s;
+ TINFL_GET_BITS(14, s, 3);
+ r->m_code_size_2[s_length_dezigzag[counter]] = (mz_uint8)s;
+ }
+ r->m_table_sizes[2] = 19;
+ }
+ for (; (int)r->m_type >= 0; r->m_type--)
+ {
+ int tree_next, tree_cur;
+ mz_int16 *pLookUp;
+ mz_int16 *pTree;
+ mz_uint8 *pCode_size;
+ mz_uint i, j, used_syms, total, sym_index, next_code[17], total_syms[16];
+ pLookUp = r->m_look_up[r->m_type];
+ pTree = pTrees[r->m_type];
+ pCode_size = pCode_sizes[r->m_type];
+ MZ_CLEAR_ARR(total_syms);
+ TINFL_MEMSET(pLookUp, 0, sizeof(r->m_look_up[0]));
+ tinfl_clear_tree(r);
+ for (i = 0; i < r->m_table_sizes[r->m_type]; ++i)
+ total_syms[pCode_size[i]]++;
+ used_syms = 0, total = 0;
+ next_code[0] = next_code[1] = 0;
+ for (i = 1; i <= 15; ++i)
+ {
+ used_syms += total_syms[i];
+ next_code[i + 1] = (total = ((total + total_syms[i]) << 1));
+ }
+ if ((65536 != total) && (used_syms > 1))
+ {
+ TINFL_CR_RETURN_FOREVER(35, TINFL_STATUS_FAILED);
+ }
+ for (tree_next = -1, sym_index = 0; sym_index < r->m_table_sizes[r->m_type]; ++sym_index)
+ {
+ mz_uint rev_code = 0, l, cur_code, code_size = pCode_size[sym_index];
+ if (!code_size)
+ continue;
+ cur_code = next_code[code_size]++;
+ for (l = code_size; l > 0; l--, cur_code >>= 1)
+ rev_code = (rev_code << 1) | (cur_code & 1);
+ if (code_size <= TINFL_FAST_LOOKUP_BITS)
+ {
+ mz_int16 k = (mz_int16)((code_size << 9) | sym_index);
+ while (rev_code < TINFL_FAST_LOOKUP_SIZE)
+ {
+ pLookUp[rev_code] = k;
+ rev_code += (1 << code_size);
+ }
+ continue;
+ }
+ if (0 == (tree_cur = pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)]))
+ {
+ pLookUp[rev_code & (TINFL_FAST_LOOKUP_SIZE - 1)] = (mz_int16)tree_next;
+ tree_cur = tree_next;
+ tree_next -= 2;
+ }
+ rev_code >>= (TINFL_FAST_LOOKUP_BITS - 1);
+ for (j = code_size; j > (TINFL_FAST_LOOKUP_BITS + 1); j--)
+ {
+ tree_cur -= ((rev_code >>= 1) & 1);
+ if (!pTree[-tree_cur - 1])
+ {
+ pTree[-tree_cur - 1] = (mz_int16)tree_next;
+ tree_cur = tree_next;
+ tree_next -= 2;
+ }
+ else
+ tree_cur = pTree[-tree_cur - 1];
+ }
+ tree_cur -= ((rev_code >>= 1) & 1);
+ pTree[-tree_cur - 1] = (mz_int16)sym_index;
+ }
+ if (r->m_type == 2)
+ {
+ for (counter = 0; counter < (r->m_table_sizes[0] + r->m_table_sizes[1]);)
+ {
+ mz_uint s;
+ TINFL_HUFF_DECODE(16, dist, r->m_look_up[2], r->m_tree_2);
+ if (dist < 16)
+ {
+ r->m_len_codes[counter++] = (mz_uint8)dist;
+ continue;
+ }
+ if ((dist == 16) && (!counter))
+ {
+ TINFL_CR_RETURN_FOREVER(17, TINFL_STATUS_FAILED);
+ }
+ num_extra = "\02\03\07"[dist - 16];
+ TINFL_GET_BITS(18, s, num_extra);
+ s += "\03\03\013"[dist - 16];
+ TINFL_MEMSET(r->m_len_codes + counter, (dist == 16) ? r->m_len_codes[counter - 1] : 0, s);
+ counter += s;
+ }
+ if ((r->m_table_sizes[0] + r->m_table_sizes[1]) != counter)
+ {
+ TINFL_CR_RETURN_FOREVER(21, TINFL_STATUS_FAILED);
+ }
+ TINFL_MEMCPY(r->m_code_size_0, r->m_len_codes, r->m_table_sizes[0]);
+ TINFL_MEMCPY(r->m_code_size_1, r->m_len_codes + r->m_table_sizes[0], r->m_table_sizes[1]);
+ }
+ }
+ for (;;)
+ {
+ mz_uint8 *pSrc;
+ for (;;)
+ {
+ if (((pIn_buf_end - pIn_buf_cur) < 4) || ((pOut_buf_end - pOut_buf_cur) < 2))
+ {
+ TINFL_HUFF_DECODE(23, counter, r->m_look_up[0], r->m_tree_0);
+ if (counter >= 256)
+ break;
+ while (pOut_buf_cur >= pOut_buf_end)
+ {
+ TINFL_CR_RETURN(24, TINFL_STATUS_HAS_MORE_OUTPUT);
+ }
+ *pOut_buf_cur++ = (mz_uint8)counter;
+ }
+ else
+ {
+ int sym2;
+ mz_uint code_len;
+#if TINFL_USE_64BIT_BITBUF
+ if (num_bits < 30)
+ {
+ bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE32(pIn_buf_cur)) << num_bits);
+ pIn_buf_cur += 4;
+ num_bits += 32;
+ }
+#else
+ if (num_bits < 15)
+ {
+ bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits);
+ pIn_buf_cur += 2;
+ num_bits += 16;
+ }
+#endif
+ if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0)
+ code_len = sym2 >> 9;
+ else
+ {
+ code_len = TINFL_FAST_LOOKUP_BITS;
+ do
+ {
+ sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)];
+ } while (sym2 < 0);
+ }
+ counter = sym2;
+ bit_buf >>= code_len;
+ num_bits -= code_len;
+ if (code_len == 0)
+ {
+ TINFL_CR_RETURN_FOREVER(40, TINFL_STATUS_FAILED);
+ }
+ if (counter & 256)
+ break;
+
+#if !TINFL_USE_64BIT_BITBUF
+ if (num_bits < 15)
+ {
+ bit_buf |= (((tinfl_bit_buf_t)MZ_READ_LE16(pIn_buf_cur)) << num_bits);
+ pIn_buf_cur += 2;
+ num_bits += 16;
+ }
+#endif
+ if ((sym2 = r->m_look_up[0][bit_buf & (TINFL_FAST_LOOKUP_SIZE - 1)]) >= 0)
+ code_len = sym2 >> 9;
+ else
+ {
+ code_len = TINFL_FAST_LOOKUP_BITS;
+ do
+ {
+ sym2 = r->m_tree_0[~sym2 + ((bit_buf >> code_len++) & 1)];
+ } while (sym2 < 0);
+ }
+ bit_buf >>= code_len;
+ num_bits -= code_len;
+ if (code_len == 0)
+ {
+ TINFL_CR_RETURN_FOREVER(54, TINFL_STATUS_FAILED);
+ }
+
+ pOut_buf_cur[0] = (mz_uint8)counter;
+ if (sym2 & 256)
+ {
+ pOut_buf_cur++;
+ counter = sym2;
+ break;
+ }
+ pOut_buf_cur[1] = (mz_uint8)sym2;
+ pOut_buf_cur += 2;
+ }
+ }
+ if ((counter &= 511) == 256)
+ break;
+
+ num_extra = s_length_extra[counter - 257];
+ counter = s_length_base[counter - 257];
+ if (num_extra)
+ {
+ mz_uint extra_bits;
+ TINFL_GET_BITS(25, extra_bits, num_extra);
+ counter += extra_bits;
+ }
+
+ TINFL_HUFF_DECODE(26, dist, r->m_look_up[1], r->m_tree_1);
+ num_extra = s_dist_extra[dist];
+ dist = s_dist_base[dist];
+ if (num_extra)
+ {
+ mz_uint extra_bits;
+ TINFL_GET_BITS(27, extra_bits, num_extra);
+ dist += extra_bits;
+ }
+
+ dist_from_out_buf_start = pOut_buf_cur - pOut_buf_start;
+ if ((dist == 0 || dist > dist_from_out_buf_start || dist_from_out_buf_start == 0) && (decomp_flags & TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF))
+ {
+ TINFL_CR_RETURN_FOREVER(37, TINFL_STATUS_FAILED);
+ }
+
+ pSrc = pOut_buf_start + ((dist_from_out_buf_start - dist) & out_buf_size_mask);
+
+ if ((MZ_MAX(pOut_buf_cur, pSrc) + counter) > pOut_buf_end)
+ {
+ while (counter--)
+ {
+ while (pOut_buf_cur >= pOut_buf_end)
+ {
+ TINFL_CR_RETURN(53, TINFL_STATUS_HAS_MORE_OUTPUT);
+ }
+ *pOut_buf_cur++ = pOut_buf_start[(dist_from_out_buf_start++ - dist) & out_buf_size_mask];
+ }
+ continue;
+ }
+#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES
+ else if ((counter >= 9) && (counter <= dist))
+ {
+ const mz_uint8 *pSrc_end = pSrc + (counter & ~7);
+ do
+ {
+#ifdef MINIZ_UNALIGNED_USE_MEMCPY
+ memcpy(pOut_buf_cur, pSrc, sizeof(mz_uint32) * 2);
+#else
+ ((mz_uint32 *)pOut_buf_cur)[0] = ((const mz_uint32 *)pSrc)[0];
+ ((mz_uint32 *)pOut_buf_cur)[1] = ((const mz_uint32 *)pSrc)[1];
+#endif
+ pOut_buf_cur += 8;
+ } while ((pSrc += 8) < pSrc_end);
+ if ((counter &= 7) < 3)
+ {
+ if (counter)
+ {
+ pOut_buf_cur[0] = pSrc[0];
+ if (counter > 1)
+ pOut_buf_cur[1] = pSrc[1];
+ pOut_buf_cur += counter;
+ }
+ continue;
+ }
+ }
+#endif
+ while (counter > 2)
+ {
+ pOut_buf_cur[0] = pSrc[0];
+ pOut_buf_cur[1] = pSrc[1];
+ pOut_buf_cur[2] = pSrc[2];
+ pOut_buf_cur += 3;
+ pSrc += 3;
+ counter -= 3;
+ }
+ if (counter > 0)
+ {
+ pOut_buf_cur[0] = pSrc[0];
+ if (counter > 1)
+ pOut_buf_cur[1] = pSrc[1];
+ pOut_buf_cur += counter;
+ }
+ }
+ }
+ } while (!(r->m_final & 1));
+
+ /* Ensure byte alignment and put back any bytes from the bitbuf if we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */
+ /* I'm being super conservative here. A number of simplifications can be made to the byte alignment part, and the Adler32 check shouldn't ever need to worry about reading from the bitbuf now. */
+ TINFL_SKIP_BITS(32, num_bits & 7);
+ while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8))
+ {
+ --pIn_buf_cur;
+ num_bits -= 8;
+ }
+ bit_buf &= ~(~(tinfl_bit_buf_t)0 << num_bits);
+ MZ_ASSERT(!num_bits); /* if this assert fires then we've read beyond the end of non-deflate/zlib streams with following data (such as gzip streams). */
+
+ if (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER)
+ {
+ for (counter = 0; counter < 4; ++counter)
+ {
+ mz_uint s;
+ if (num_bits)
+ TINFL_GET_BITS(41, s, 8);
+ else
+ TINFL_GET_BYTE(42, s);
+ r->m_z_adler32 = (r->m_z_adler32 << 8) | s;
+ }
+ }
+ TINFL_CR_RETURN_FOREVER(34, TINFL_STATUS_DONE);
+
+ TINFL_CR_FINISH
+
+ common_exit:
+ /* As long as we aren't telling the caller that we NEED more input to make forward progress: */
+ /* Put back any bytes from the bitbuf in case we've looked ahead too far on gzip, or other Deflate streams followed by arbitrary data. */
+ /* We need to be very careful here to NOT push back any bytes we definitely know we need to make forward progress, though, or we'll lock the caller up into an inf loop. */
+ if ((status != TINFL_STATUS_NEEDS_MORE_INPUT) && (status != TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS))
+ {
+ while ((pIn_buf_cur > pIn_buf_next) && (num_bits >= 8))
+ {
+ --pIn_buf_cur;
+ num_bits -= 8;
+ }
+ }
+ r->m_num_bits = num_bits;
+ r->m_bit_buf = bit_buf & ~(~(tinfl_bit_buf_t)0 << num_bits);
+ r->m_dist = dist;
+ r->m_counter = counter;
+ r->m_num_extra = num_extra;
+ r->m_dist_from_out_buf_start = dist_from_out_buf_start;
+ *pIn_buf_size = pIn_buf_cur - pIn_buf_next;
+ *pOut_buf_size = pOut_buf_cur - pOut_buf_next;
+ if ((decomp_flags & (TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_COMPUTE_ADLER32)) && (status >= 0))
+ {
+ const mz_uint8 *ptr = pOut_buf_next;
+ size_t buf_len = *pOut_buf_size;
+ mz_uint32 i, s1 = r->m_check_adler32 & 0xffff, s2 = r->m_check_adler32 >> 16;
+ size_t block_len = buf_len % 5552;
+ while (buf_len)
+ {
+ for (i = 0; i + 7 < block_len; i += 8, ptr += 8)
+ {
+ s1 += ptr[0], s2 += s1;
+ s1 += ptr[1], s2 += s1;
+ s1 += ptr[2], s2 += s1;
+ s1 += ptr[3], s2 += s1;
+ s1 += ptr[4], s2 += s1;
+ s1 += ptr[5], s2 += s1;
+ s1 += ptr[6], s2 += s1;
+ s1 += ptr[7], s2 += s1;
+ }
+ for (; i < block_len; ++i)
+ s1 += *ptr++, s2 += s1;
+ s1 %= 65521U, s2 %= 65521U;
+ buf_len -= block_len;
+ block_len = 5552;
+ }
+ r->m_check_adler32 = (s2 << 16) + s1;
+ if ((status == TINFL_STATUS_DONE) && (decomp_flags & TINFL_FLAG_PARSE_ZLIB_HEADER) && (r->m_check_adler32 != r->m_z_adler32))
+ status = TINFL_STATUS_ADLER32_MISMATCH;
+ }
+ return status;
+ }
+
+ /* Higher level helper functions. */
+ void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags)
+ {
+ tinfl_decompressor decomp;
+ void *pBuf = NULL, *pNew_buf;
+ size_t src_buf_ofs = 0, out_buf_capacity = 0;
+ *pOut_len = 0;
+ tinfl_init(&decomp);
+ for (;;)
+ {
+ size_t src_buf_size = src_buf_len - src_buf_ofs, dst_buf_size = out_buf_capacity - *pOut_len, new_out_buf_capacity;
+ tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf + src_buf_ofs, &src_buf_size, (mz_uint8 *)pBuf, pBuf ? (mz_uint8 *)pBuf + *pOut_len : NULL, &dst_buf_size,
+ (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
+ if ((status < 0) || (status == TINFL_STATUS_NEEDS_MORE_INPUT))
+ {
+ MZ_FREE(pBuf);
+ *pOut_len = 0;
+ return NULL;
+ }
+ src_buf_ofs += src_buf_size;
+ *pOut_len += dst_buf_size;
+ if (status == TINFL_STATUS_DONE)
+ break;
+ new_out_buf_capacity = out_buf_capacity * 2;
+ if (new_out_buf_capacity < 128)
+ new_out_buf_capacity = 128;
+ pNew_buf = MZ_REALLOC(pBuf, new_out_buf_capacity);
+ if (!pNew_buf)
+ {
+ MZ_FREE(pBuf);
+ *pOut_len = 0;
+ return NULL;
+ }
+ pBuf = pNew_buf;
+ out_buf_capacity = new_out_buf_capacity;
+ }
+ return pBuf;
+ }
+
+ size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags)
+ {
+ tinfl_decompressor decomp;
+ tinfl_status status;
+ tinfl_init(&decomp);
+ status = tinfl_decompress(&decomp, (const mz_uint8 *)pSrc_buf, &src_buf_len, (mz_uint8 *)pOut_buf, (mz_uint8 *)pOut_buf, &out_buf_len, (flags & ~TINFL_FLAG_HAS_MORE_INPUT) | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
+ return (status != TINFL_STATUS_DONE) ? TINFL_DECOMPRESS_MEM_TO_MEM_FAILED : out_buf_len;
+ }
+
+ int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags)
+ {
+ int result = 0;
+ tinfl_decompressor decomp;
+ mz_uint8 *pDict = (mz_uint8 *)MZ_MALLOC(TINFL_LZ_DICT_SIZE);
+ size_t in_buf_ofs = 0, dict_ofs = 0;
+ if (!pDict)
+ return TINFL_STATUS_FAILED;
+ memset(pDict, 0, TINFL_LZ_DICT_SIZE);
+ tinfl_init(&decomp);
+ for (;;)
+ {
+ size_t in_buf_size = *pIn_buf_size - in_buf_ofs, dst_buf_size = TINFL_LZ_DICT_SIZE - dict_ofs;
+ tinfl_status status = tinfl_decompress(&decomp, (const mz_uint8 *)pIn_buf + in_buf_ofs, &in_buf_size, pDict, pDict + dict_ofs, &dst_buf_size,
+ (flags & ~(TINFL_FLAG_HAS_MORE_INPUT | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF)));
+ in_buf_ofs += in_buf_size;
+ if ((dst_buf_size) && (!(*pPut_buf_func)(pDict + dict_ofs, (int)dst_buf_size, pPut_buf_user)))
+ break;
+ if (status != TINFL_STATUS_HAS_MORE_OUTPUT)
+ {
+ result = (status == TINFL_STATUS_DONE);
+ break;
+ }
+ dict_ofs = (dict_ofs + dst_buf_size) & (TINFL_LZ_DICT_SIZE - 1);
+ }
+ MZ_FREE(pDict);
+ *pIn_buf_size = in_buf_ofs;
+ return result;
+ }
+
+#ifndef MINIZ_NO_MALLOC
+ tinfl_decompressor *tinfl_decompressor_alloc(void)
+ {
+ tinfl_decompressor *pDecomp = (tinfl_decompressor *)MZ_MALLOC(sizeof(tinfl_decompressor));
+ if (pDecomp)
+ tinfl_init(pDecomp);
+ return pDecomp;
+ }
+
+ void tinfl_decompressor_free(tinfl_decompressor *pDecomp)
+ {
+ MZ_FREE(pDecomp);
+ }
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
+ /**************************************************************************
+ *
+ * Copyright 2013-2014 RAD Game Tools and Valve Software
+ * Copyright 2010-2014 Rich Geldreich and Tenacious Software LLC
+ * Copyright 2016 Martin Raiber
+ * All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ **************************************************************************/
+
+
+#ifndef MINIZ_NO_ARCHIVE_APIS
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ /* ------------------- .ZIP archive reading */
+
+#ifdef MINIZ_NO_STDIO
+#define MZ_FILE void *
+#else
+#include
+
+#if defined(_MSC_VER) || defined(__MINGW64__) || defined(__MINGW32__)
+
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#ifndef __cplusplus
+#define MICROSOFT_WINDOWS_WINBASE_H_DEFINE_INTERLOCKED_CPLUSPLUS_OVERLOADS 0
+#endif
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+#include
+
+static WCHAR *mz_utf8z_to_widechar(const char *str)
+{
+ int reqChars = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
+ WCHAR *wStr = (WCHAR *)malloc(reqChars * sizeof(WCHAR));
+ MultiByteToWideChar(CP_UTF8, 0, str, -1, wStr, reqChars);
+ return wStr;
+}
+
+static FILE *mz_fopen(const char *pFilename, const char *pMode)
+{
+ WCHAR *wFilename = mz_utf8z_to_widechar(pFilename);
+ WCHAR *wMode = mz_utf8z_to_widechar(pMode);
+ FILE *pFile = NULL;
+ errno_t err = _wfopen_s(&pFile, wFilename, wMode);
+ free(wFilename);
+ free(wMode);
+ return err ? NULL : pFile;
+}
+
+static FILE *mz_freopen(const char *pPath, const char *pMode, FILE *pStream)
+{
+ WCHAR *wPath = mz_utf8z_to_widechar(pPath);
+ WCHAR *wMode = mz_utf8z_to_widechar(pMode);
+ FILE *pFile = NULL;
+ errno_t err = _wfreopen_s(&pFile, wPath, wMode, pStream);
+ free(wPath);
+ free(wMode);
+ return err ? NULL : pFile;
+}
+
+#if defined(__MINGW32__)
+static int mz_stat(const char *path, struct _stat *buffer)
+{
+ WCHAR *wPath = mz_utf8z_to_widechar(path);
+ int res = _wstat(wPath, buffer);
+ free(wPath);
+ return res;
+}
+#else
+static int mz_stat64(const char *path, struct __stat64 *buffer)
+{
+ WCHAR *wPath = mz_utf8z_to_widechar(path);
+ int res = _wstat64(wPath, buffer);
+ free(wPath);
+ return res;
+}
+#endif
+
+#ifndef MINIZ_NO_TIME
+#include
+#endif
+#define MZ_FOPEN mz_fopen
+#define MZ_FCLOSE fclose
+#define MZ_FREAD fread
+#define MZ_FWRITE fwrite
+#define MZ_FTELL64 _ftelli64
+#define MZ_FSEEK64 _fseeki64
+#if defined(__MINGW32__)
+#define MZ_FILE_STAT_STRUCT _stat
+#define MZ_FILE_STAT mz_stat
+#else
+#define MZ_FILE_STAT_STRUCT _stat64
+#define MZ_FILE_STAT mz_stat64
+#endif
+#define MZ_FFLUSH fflush
+#define MZ_FREOPEN mz_freopen
+#define MZ_DELETE_FILE remove
+
+#elif defined(__WATCOMC__)
+#ifndef MINIZ_NO_TIME
+#include
+#endif
+#define MZ_FOPEN(f, m) fopen(f, m)
+#define MZ_FCLOSE fclose
+#define MZ_FREAD fread
+#define MZ_FWRITE fwrite
+#define MZ_FTELL64 _ftelli64
+#define MZ_FSEEK64 _fseeki64
+#define MZ_FILE_STAT_STRUCT stat
+#define MZ_FILE_STAT stat
+#define MZ_FFLUSH fflush
+#define MZ_FREOPEN(f, m, s) freopen(f, m, s)
+#define MZ_DELETE_FILE remove
+
+#elif defined(__TINYC__)
+#ifndef MINIZ_NO_TIME
+#include
+#endif
+#define MZ_FOPEN(f, m) fopen(f, m)
+#define MZ_FCLOSE fclose
+#define MZ_FREAD fread
+#define MZ_FWRITE fwrite
+#define MZ_FTELL64 ftell
+#define MZ_FSEEK64 fseek
+#define MZ_FILE_STAT_STRUCT stat
+#define MZ_FILE_STAT stat
+#define MZ_FFLUSH fflush
+#define MZ_FREOPEN(f, m, s) freopen(f, m, s)
+#define MZ_DELETE_FILE remove
+
+#elif defined(__USE_LARGEFILE64) /* gcc, clang */
+#ifndef MINIZ_NO_TIME
+#include
+#endif
+#define MZ_FOPEN(f, m) fopen64(f, m)
+#define MZ_FCLOSE fclose
+#define MZ_FREAD fread
+#define MZ_FWRITE fwrite
+#define MZ_FTELL64 ftello64
+#define MZ_FSEEK64 fseeko64
+#define MZ_FILE_STAT_STRUCT stat64
+#define MZ_FILE_STAT stat64
+#define MZ_FFLUSH fflush
+#define MZ_FREOPEN(p, m, s) freopen64(p, m, s)
+#define MZ_DELETE_FILE remove
+
+#elif defined(__APPLE__) || defined(__FreeBSD__) || (defined(__linux__) && defined(__x86_64__))
+#ifndef MINIZ_NO_TIME
+#include
+#endif
+#define MZ_FOPEN(f, m) fopen(f, m)
+#define MZ_FCLOSE fclose
+#define MZ_FREAD fread
+#define MZ_FWRITE fwrite
+#define MZ_FTELL64 ftello
+#define MZ_FSEEK64 fseeko
+#define MZ_FILE_STAT_STRUCT stat
+#define MZ_FILE_STAT stat
+#define MZ_FFLUSH fflush
+#define MZ_FREOPEN(p, m, s) freopen(p, m, s)
+#define MZ_DELETE_FILE remove
+
+#else
+#pragma message("Using fopen, ftello, fseeko, stat() etc. path for file I/O - this path may not support large files.")
+#ifndef MINIZ_NO_TIME
+#include
+#endif
+#define MZ_FOPEN(f, m) fopen(f, m)
+#define MZ_FCLOSE fclose
+#define MZ_FREAD fread
+#define MZ_FWRITE fwrite
+#ifdef __STRICT_ANSI__
+#define MZ_FTELL64 ftell
+#define MZ_FSEEK64 fseek
+#else
+#define MZ_FTELL64 ftello
+#define MZ_FSEEK64 fseeko
+#endif
+#define MZ_FILE_STAT_STRUCT stat
+#define MZ_FILE_STAT stat
+#define MZ_FFLUSH fflush
+#define MZ_FREOPEN(f, m, s) freopen(f, m, s)
+#define MZ_DELETE_FILE remove
+#endif /* #ifdef _MSC_VER */
+#endif /* #ifdef MINIZ_NO_STDIO */
+
+#define MZ_TOLOWER(c) ((((c) >= 'A') && ((c) <= 'Z')) ? ((c) - 'A' + 'a') : (c))
+
+ /* Various ZIP archive enums. To completely avoid cross platform compiler alignment and platform endian issues, miniz.c doesn't use structs for any of this stuff. */
+ enum
+ {
+ /* ZIP archive identifiers and record sizes */
+ MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06054b50,
+ MZ_ZIP_CENTRAL_DIR_HEADER_SIG = 0x02014b50,
+ MZ_ZIP_LOCAL_DIR_HEADER_SIG = 0x04034b50,
+ MZ_ZIP_LOCAL_DIR_HEADER_SIZE = 30,
+ MZ_ZIP_CENTRAL_DIR_HEADER_SIZE = 46,
+ MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE = 22,
+
+ /* ZIP64 archive identifier and record sizes */
+ MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG = 0x06064b50,
+ MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG = 0x07064b50,
+ MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE = 56,
+ MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE = 20,
+ MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID = 0x0001,
+ MZ_ZIP_DATA_DESCRIPTOR_ID = 0x08074b50,
+ MZ_ZIP_DATA_DESCRIPTER_SIZE64 = 24,
+ MZ_ZIP_DATA_DESCRIPTER_SIZE32 = 16,
+
+ /* Central directory header record offsets */
+ MZ_ZIP_CDH_SIG_OFS = 0,
+ MZ_ZIP_CDH_VERSION_MADE_BY_OFS = 4,
+ MZ_ZIP_CDH_VERSION_NEEDED_OFS = 6,
+ MZ_ZIP_CDH_BIT_FLAG_OFS = 8,
+ MZ_ZIP_CDH_METHOD_OFS = 10,
+ MZ_ZIP_CDH_FILE_TIME_OFS = 12,
+ MZ_ZIP_CDH_FILE_DATE_OFS = 14,
+ MZ_ZIP_CDH_CRC32_OFS = 16,
+ MZ_ZIP_CDH_COMPRESSED_SIZE_OFS = 20,
+ MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS = 24,
+ MZ_ZIP_CDH_FILENAME_LEN_OFS = 28,
+ MZ_ZIP_CDH_EXTRA_LEN_OFS = 30,
+ MZ_ZIP_CDH_COMMENT_LEN_OFS = 32,
+ MZ_ZIP_CDH_DISK_START_OFS = 34,
+ MZ_ZIP_CDH_INTERNAL_ATTR_OFS = 36,
+ MZ_ZIP_CDH_EXTERNAL_ATTR_OFS = 38,
+ MZ_ZIP_CDH_LOCAL_HEADER_OFS = 42,
+
+ /* Local directory header offsets */
+ MZ_ZIP_LDH_SIG_OFS = 0,
+ MZ_ZIP_LDH_VERSION_NEEDED_OFS = 4,
+ MZ_ZIP_LDH_BIT_FLAG_OFS = 6,
+ MZ_ZIP_LDH_METHOD_OFS = 8,
+ MZ_ZIP_LDH_FILE_TIME_OFS = 10,
+ MZ_ZIP_LDH_FILE_DATE_OFS = 12,
+ MZ_ZIP_LDH_CRC32_OFS = 14,
+ MZ_ZIP_LDH_COMPRESSED_SIZE_OFS = 18,
+ MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS = 22,
+ MZ_ZIP_LDH_FILENAME_LEN_OFS = 26,
+ MZ_ZIP_LDH_EXTRA_LEN_OFS = 28,
+ MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR = 1 << 3,
+
+ /* End of central directory offsets */
+ MZ_ZIP_ECDH_SIG_OFS = 0,
+ MZ_ZIP_ECDH_NUM_THIS_DISK_OFS = 4,
+ MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS = 6,
+ MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 8,
+ MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS = 10,
+ MZ_ZIP_ECDH_CDIR_SIZE_OFS = 12,
+ MZ_ZIP_ECDH_CDIR_OFS_OFS = 16,
+ MZ_ZIP_ECDH_COMMENT_SIZE_OFS = 20,
+
+ /* ZIP64 End of central directory locator offsets */
+ MZ_ZIP64_ECDL_SIG_OFS = 0, /* 4 bytes */
+ MZ_ZIP64_ECDL_NUM_DISK_CDIR_OFS = 4, /* 4 bytes */
+ MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS = 8, /* 8 bytes */
+ MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS = 16, /* 4 bytes */
+
+ /* ZIP64 End of central directory header offsets */
+ MZ_ZIP64_ECDH_SIG_OFS = 0, /* 4 bytes */
+ MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS = 4, /* 8 bytes */
+ MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS = 12, /* 2 bytes */
+ MZ_ZIP64_ECDH_VERSION_NEEDED_OFS = 14, /* 2 bytes */
+ MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS = 16, /* 4 bytes */
+ MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS = 20, /* 4 bytes */
+ MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS = 24, /* 8 bytes */
+ MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS = 32, /* 8 bytes */
+ MZ_ZIP64_ECDH_CDIR_SIZE_OFS = 40, /* 8 bytes */
+ MZ_ZIP64_ECDH_CDIR_OFS_OFS = 48, /* 8 bytes */
+ MZ_ZIP_VERSION_MADE_BY_DOS_FILESYSTEM_ID = 0,
+ MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG = 0x10,
+ MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED = 1,
+ MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG = 32,
+ MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION = 64,
+ MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED = 8192,
+ MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8 = 1 << 11
+ };
+
+ typedef struct
+ {
+ void *m_p;
+ size_t m_size, m_capacity;
+ mz_uint m_element_size;
+ } mz_zip_array;
+
+ struct mz_zip_internal_state_tag
+ {
+ mz_zip_array m_central_dir;
+ mz_zip_array m_central_dir_offsets;
+ mz_zip_array m_sorted_central_dir_offsets;
+
+ /* The flags passed in when the archive is initially opened. */
+ mz_uint32 m_init_flags;
+
+ /* MZ_TRUE if the archive has a zip64 end of central directory headers, etc. */
+ mz_bool m_zip64;
+
+ /* MZ_TRUE if we found zip64 extended info in the central directory (m_zip64 will also be slammed to true too, even if we didn't find a zip64 end of central dir header, etc.) */
+ mz_bool m_zip64_has_extended_info_fields;
+
+ /* These fields are used by the file, FILE, memory, and memory/heap read/write helpers. */
+ MZ_FILE *m_pFile;
+ mz_uint64 m_file_archive_start_ofs;
+
+ void *m_pMem;
+ size_t m_mem_size;
+ size_t m_mem_capacity;
+ };
+
+#define MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(array_ptr, element_size) (array_ptr)->m_element_size = element_size
+
+#if defined(DEBUG) || defined(_DEBUG)
+ static MZ_FORCEINLINE mz_uint mz_zip_array_range_check(const mz_zip_array *pArray, mz_uint index)
+ {
+ MZ_ASSERT(index < pArray->m_size);
+ return index;
+ }
+#define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[mz_zip_array_range_check(array_ptr, index)]
+#else
+#define MZ_ZIP_ARRAY_ELEMENT(array_ptr, element_type, index) ((element_type *)((array_ptr)->m_p))[index]
+#endif
+
+ static MZ_FORCEINLINE void mz_zip_array_init(mz_zip_array *pArray, mz_uint32 element_size)
+ {
+ memset(pArray, 0, sizeof(mz_zip_array));
+ pArray->m_element_size = element_size;
+ }
+
+ static MZ_FORCEINLINE void mz_zip_array_clear(mz_zip_archive *pZip, mz_zip_array *pArray)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pArray->m_p);
+ memset(pArray, 0, sizeof(mz_zip_array));
+ }
+
+ static mz_bool mz_zip_array_ensure_capacity(mz_zip_archive *pZip, mz_zip_array *pArray, size_t min_new_capacity, mz_uint growing)
+ {
+ void *pNew_p;
+ size_t new_capacity = min_new_capacity;
+ MZ_ASSERT(pArray->m_element_size);
+ if (pArray->m_capacity >= min_new_capacity)
+ return MZ_TRUE;
+ if (growing)
+ {
+ new_capacity = MZ_MAX(1, pArray->m_capacity);
+ while (new_capacity < min_new_capacity)
+ new_capacity *= 2;
+ }
+ if (NULL == (pNew_p = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pArray->m_p, pArray->m_element_size, new_capacity)))
+ return MZ_FALSE;
+ pArray->m_p = pNew_p;
+ pArray->m_capacity = new_capacity;
+ return MZ_TRUE;
+ }
+
+ static MZ_FORCEINLINE mz_bool mz_zip_array_reserve(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_capacity, mz_uint growing)
+ {
+ if (new_capacity > pArray->m_capacity)
+ {
+ if (!mz_zip_array_ensure_capacity(pZip, pArray, new_capacity, growing))
+ return MZ_FALSE;
+ }
+ return MZ_TRUE;
+ }
+
+ static MZ_FORCEINLINE mz_bool mz_zip_array_resize(mz_zip_archive *pZip, mz_zip_array *pArray, size_t new_size, mz_uint growing)
+ {
+ if (new_size > pArray->m_capacity)
+ {
+ if (!mz_zip_array_ensure_capacity(pZip, pArray, new_size, growing))
+ return MZ_FALSE;
+ }
+ pArray->m_size = new_size;
+ return MZ_TRUE;
+ }
+
+ static MZ_FORCEINLINE mz_bool mz_zip_array_ensure_room(mz_zip_archive *pZip, mz_zip_array *pArray, size_t n)
+ {
+ return mz_zip_array_reserve(pZip, pArray, pArray->m_size + n, MZ_TRUE);
+ }
+
+ static MZ_FORCEINLINE mz_bool mz_zip_array_push_back(mz_zip_archive *pZip, mz_zip_array *pArray, const void *pElements, size_t n)
+ {
+ size_t orig_size = pArray->m_size;
+ if (!mz_zip_array_resize(pZip, pArray, orig_size + n, MZ_TRUE))
+ return MZ_FALSE;
+ if (n > 0)
+ memcpy((mz_uint8 *)pArray->m_p + orig_size * pArray->m_element_size, pElements, n * pArray->m_element_size);
+ return MZ_TRUE;
+ }
+
+#ifndef MINIZ_NO_TIME
+ static MZ_TIME_T mz_zip_dos_to_time_t(int dos_time, int dos_date)
+ {
+ struct tm tm;
+ memset(&tm, 0, sizeof(tm));
+ tm.tm_isdst = -1;
+ tm.tm_year = ((dos_date >> 9) & 127) + 1980 - 1900;
+ tm.tm_mon = ((dos_date >> 5) & 15) - 1;
+ tm.tm_mday = dos_date & 31;
+ tm.tm_hour = (dos_time >> 11) & 31;
+ tm.tm_min = (dos_time >> 5) & 63;
+ tm.tm_sec = (dos_time << 1) & 62;
+ return mktime(&tm);
+ }
+
+#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS
+ static void mz_zip_time_t_to_dos_time(MZ_TIME_T time, mz_uint16 *pDOS_time, mz_uint16 *pDOS_date)
+ {
+#ifdef _MSC_VER
+ struct tm tm_struct;
+ struct tm *tm = &tm_struct;
+ errno_t err = localtime_s(tm, &time);
+ if (err)
+ {
+ *pDOS_date = 0;
+ *pDOS_time = 0;
+ return;
+ }
+#else
+ struct tm *tm = localtime(&time);
+#endif /* #ifdef _MSC_VER */
+
+ *pDOS_time = (mz_uint16)(((tm->tm_hour) << 11) + ((tm->tm_min) << 5) + ((tm->tm_sec) >> 1));
+ *pDOS_date = (mz_uint16)(((tm->tm_year + 1900 - 1980) << 9) + ((tm->tm_mon + 1) << 5) + tm->tm_mday);
+ }
+#endif /* MINIZ_NO_ARCHIVE_WRITING_APIS */
+
+#ifndef MINIZ_NO_STDIO
+#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS
+ static mz_bool mz_zip_get_file_modified_time(const char *pFilename, MZ_TIME_T *pTime)
+ {
+ struct MZ_FILE_STAT_STRUCT file_stat;
+
+ /* On Linux with x86 glibc, this call will fail on large files (I think >= 0x80000000 bytes) unless you compiled with _LARGEFILE64_SOURCE. Argh. */
+ if (MZ_FILE_STAT(pFilename, &file_stat) != 0)
+ return MZ_FALSE;
+
+ *pTime = file_stat.st_mtime;
+
+ return MZ_TRUE;
+ }
+#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS*/
+
+ static mz_bool mz_zip_set_file_times(const char *pFilename, MZ_TIME_T access_time, MZ_TIME_T modified_time)
+ {
+ struct utimbuf t;
+
+ memset(&t, 0, sizeof(t));
+ t.actime = access_time;
+ t.modtime = modified_time;
+
+ return !utime(pFilename, &t);
+ }
+#endif /* #ifndef MINIZ_NO_STDIO */
+#endif /* #ifndef MINIZ_NO_TIME */
+
+ static MZ_FORCEINLINE mz_bool mz_zip_set_error(mz_zip_archive *pZip, mz_zip_error err_num)
+ {
+ if (pZip)
+ pZip->m_last_error = err_num;
+ return MZ_FALSE;
+ }
+
+ static mz_bool mz_zip_reader_init_internal(mz_zip_archive *pZip, mz_uint flags)
+ {
+ (void)flags;
+ if ((!pZip) || (pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (!pZip->m_pAlloc)
+ pZip->m_pAlloc = miniz_def_alloc_func;
+ if (!pZip->m_pFree)
+ pZip->m_pFree = miniz_def_free_func;
+ if (!pZip->m_pRealloc)
+ pZip->m_pRealloc = miniz_def_realloc_func;
+
+ pZip->m_archive_size = 0;
+ pZip->m_central_directory_file_ofs = 0;
+ pZip->m_total_files = 0;
+ pZip->m_last_error = MZ_ZIP_NO_ERROR;
+
+ if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state))))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state));
+ MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8));
+ MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32));
+ MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32));
+ pZip->m_pState->m_init_flags = flags;
+ pZip->m_pState->m_zip64 = MZ_FALSE;
+ pZip->m_pState->m_zip64_has_extended_info_fields = MZ_FALSE;
+
+ pZip->m_zip_mode = MZ_ZIP_MODE_READING;
+
+ return MZ_TRUE;
+ }
+
+ static MZ_FORCEINLINE mz_bool mz_zip_reader_filename_less(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, mz_uint r_index)
+ {
+ const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE;
+ const mz_uint8 *pR = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, r_index));
+ mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS), r_len = MZ_READ_LE16(pR + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ mz_uint8 l = 0, r = 0;
+ pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE;
+ pR += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE;
+ pE = pL + MZ_MIN(l_len, r_len);
+ while (pL < pE)
+ {
+ if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR)))
+ break;
+ pL++;
+ pR++;
+ }
+ return (pL == pE) ? (l_len < r_len) : (l < r);
+ }
+
+#define MZ_SWAP_UINT32(a, b) \
+ do \
+ { \
+ mz_uint32 t = a; \
+ a = b; \
+ b = t; \
+ } \
+ MZ_MACRO_END
+
+ /* Heap sort of lowercased filenames, used to help accelerate plain central directory searches by mz_zip_reader_locate_file(). (Could also use qsort(), but it could allocate memory.) */
+ static void mz_zip_reader_sort_central_dir_offsets_by_filename(mz_zip_archive *pZip)
+ {
+ mz_zip_internal_state *pState = pZip->m_pState;
+ const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets;
+ const mz_zip_array *pCentral_dir = &pState->m_central_dir;
+ mz_uint32 *pIndices;
+ mz_uint32 start, end;
+ const mz_uint32 size = pZip->m_total_files;
+
+ if (size <= 1U)
+ return;
+
+ pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0);
+
+ start = (size - 2U) >> 1U;
+ for (;;)
+ {
+ mz_uint64 child, root = start;
+ for (;;)
+ {
+ if ((child = (root << 1U) + 1U) >= size)
+ break;
+ child += (((child + 1U) < size) && (mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U])));
+ if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child]))
+ break;
+ MZ_SWAP_UINT32(pIndices[root], pIndices[child]);
+ root = child;
+ }
+ if (!start)
+ break;
+ start--;
+ }
+
+ end = size - 1;
+ while (end > 0)
+ {
+ mz_uint64 child, root = 0;
+ MZ_SWAP_UINT32(pIndices[end], pIndices[0]);
+ for (;;)
+ {
+ if ((child = (root << 1U) + 1U) >= end)
+ break;
+ child += (((child + 1U) < end) && mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[child], pIndices[child + 1U]));
+ if (!mz_zip_reader_filename_less(pCentral_dir, pCentral_dir_offsets, pIndices[root], pIndices[child]))
+ break;
+ MZ_SWAP_UINT32(pIndices[root], pIndices[child]);
+ root = child;
+ }
+ end--;
+ }
+ }
+
+ static mz_bool mz_zip_reader_locate_header_sig(mz_zip_archive *pZip, mz_uint32 record_sig, mz_uint32 record_size, mz_int64 *pOfs)
+ {
+ mz_int64 cur_file_ofs;
+ mz_uint32 buf_u32[4096 / sizeof(mz_uint32)];
+ mz_uint8 *pBuf = (mz_uint8 *)buf_u32;
+
+ /* Basic sanity checks - reject files which are too small */
+ if (pZip->m_archive_size < record_size)
+ return MZ_FALSE;
+
+ /* Find the record by scanning the file from the end towards the beginning. */
+ cur_file_ofs = MZ_MAX((mz_int64)pZip->m_archive_size - (mz_int64)sizeof(buf_u32), 0);
+ for (;;)
+ {
+ int i, n = (int)MZ_MIN(sizeof(buf_u32), pZip->m_archive_size - cur_file_ofs);
+
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, n) != (mz_uint)n)
+ return MZ_FALSE;
+
+ for (i = n - 4; i >= 0; --i)
+ {
+ mz_uint s = MZ_READ_LE32(pBuf + i);
+ if (s == record_sig)
+ {
+ if ((pZip->m_archive_size - (cur_file_ofs + i)) >= record_size)
+ break;
+ }
+ }
+
+ if (i >= 0)
+ {
+ cur_file_ofs += i;
+ break;
+ }
+
+ /* Give up if we've searched the entire file, or we've gone back "too far" (~64kb) */
+ if ((!cur_file_ofs) || ((pZip->m_archive_size - cur_file_ofs) >= ((mz_uint64)(MZ_UINT16_MAX) + record_size)))
+ return MZ_FALSE;
+
+ cur_file_ofs = MZ_MAX(cur_file_ofs - (sizeof(buf_u32) - 3), 0);
+ }
+
+ *pOfs = cur_file_ofs;
+ return MZ_TRUE;
+ }
+
+ static mz_bool mz_zip_reader_eocd64_valid(mz_zip_archive *pZip, uint64_t offset, uint8_t *buf)
+ {
+ if (pZip->m_pRead(pZip->m_pIO_opaque, offset, buf, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ {
+ if (MZ_READ_LE32(buf + MZ_ZIP64_ECDH_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG)
+ {
+ return MZ_TRUE;
+ }
+ }
+
+ return MZ_FALSE;
+ }
+
+ static mz_bool mz_zip_reader_read_central_dir(mz_zip_archive *pZip, mz_uint flags)
+ {
+ mz_uint cdir_size = 0, cdir_entries_on_this_disk = 0, num_this_disk = 0, cdir_disk_index = 0;
+ mz_uint64 cdir_ofs = 0, eocd_ofs = 0, archive_ofs = 0;
+ mz_int64 cur_file_ofs = 0;
+ const mz_uint8 *p;
+
+ mz_uint32 buf_u32[4096 / sizeof(mz_uint32)];
+ mz_uint8 *pBuf = (mz_uint8 *)buf_u32;
+ mz_bool sort_central_dir = ((flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0);
+ mz_uint32 zip64_end_of_central_dir_locator_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)];
+ mz_uint8 *pZip64_locator = (mz_uint8 *)zip64_end_of_central_dir_locator_u32;
+
+ mz_uint32 zip64_end_of_central_dir_header_u32[(MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)];
+ mz_uint8 *pZip64_end_of_central_dir = (mz_uint8 *)zip64_end_of_central_dir_header_u32;
+
+ mz_uint64 zip64_end_of_central_dir_ofs = 0;
+
+ /* Basic sanity checks - reject files which are too small, and check the first 4 bytes of the file to make sure a local header is there. */
+ if (pZip->m_archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+
+ if (!mz_zip_reader_locate_header_sig(pZip, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE, &cur_file_ofs))
+ return mz_zip_set_error(pZip, MZ_ZIP_FAILED_FINDING_CENTRAL_DIR);
+
+ eocd_ofs = cur_file_ofs;
+ /* Read and verify the end of central directory record. */
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+
+ if (MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_SIG_OFS) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG)
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+
+ if (cur_file_ofs >= (MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE + MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE))
+ {
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs - MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE, pZip64_locator, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE)
+ {
+ if (MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_SIG_OFS) == MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG)
+ {
+ pZip->m_pState->m_zip64 = MZ_TRUE;
+ }
+ }
+ }
+
+ if (pZip->m_pState->m_zip64)
+ {
+ /* Try locating the EOCD64 right before the EOCD64 locator. This works even
+ * when the effective start of the zip header is not yet known. */
+ if (cur_file_ofs < MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE +
+ MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+
+ zip64_end_of_central_dir_ofs = cur_file_ofs -
+ MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE -
+ MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE;
+
+ if (!mz_zip_reader_eocd64_valid(pZip, zip64_end_of_central_dir_ofs,
+ pZip64_end_of_central_dir))
+ {
+ /* That failed, try reading where the locator tells us to. */
+ zip64_end_of_central_dir_ofs = MZ_READ_LE64(
+ pZip64_locator + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS);
+
+ if (zip64_end_of_central_dir_ofs >
+ (pZip->m_archive_size - MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE))
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+
+ if (!mz_zip_reader_eocd64_valid(pZip, zip64_end_of_central_dir_ofs,
+ pZip64_end_of_central_dir))
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+ }
+ }
+
+ pZip->m_total_files = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS);
+ cdir_entries_on_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS);
+ num_this_disk = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_THIS_DISK_OFS);
+ cdir_disk_index = MZ_READ_LE16(pBuf + MZ_ZIP_ECDH_NUM_DISK_CDIR_OFS);
+ cdir_size = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_SIZE_OFS);
+ cdir_ofs = MZ_READ_LE32(pBuf + MZ_ZIP_ECDH_CDIR_OFS_OFS);
+
+ if (pZip->m_pState->m_zip64)
+ {
+ mz_uint32 zip64_total_num_of_disks = MZ_READ_LE32(pZip64_locator + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS);
+ mz_uint64 zip64_cdir_total_entries = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS);
+ mz_uint64 zip64_cdir_total_entries_on_this_disk = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS);
+ mz_uint64 zip64_size_of_end_of_central_dir_record = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS);
+ mz_uint64 zip64_size_of_central_directory = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_SIZE_OFS);
+
+ if (zip64_size_of_end_of_central_dir_record < (MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - 12))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if (zip64_total_num_of_disks != 1U)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK);
+
+ /* Check for miniz's practical limits */
+ if (zip64_cdir_total_entries > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+
+ pZip->m_total_files = (mz_uint32)zip64_cdir_total_entries;
+
+ if (zip64_cdir_total_entries_on_this_disk > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+
+ cdir_entries_on_this_disk = (mz_uint32)zip64_cdir_total_entries_on_this_disk;
+
+ /* Check for miniz's current practical limits (sorry, this should be enough for millions of files) */
+ if (zip64_size_of_central_directory > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE);
+
+ cdir_size = (mz_uint32)zip64_size_of_central_directory;
+
+ num_this_disk = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_THIS_DISK_OFS);
+
+ cdir_disk_index = MZ_READ_LE32(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_NUM_DISK_CDIR_OFS);
+
+ cdir_ofs = MZ_READ_LE64(pZip64_end_of_central_dir + MZ_ZIP64_ECDH_CDIR_OFS_OFS);
+ }
+
+ if (pZip->m_total_files != cdir_entries_on_this_disk)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK);
+
+ if (((num_this_disk | cdir_disk_index) != 0) && ((num_this_disk != 1) || (cdir_disk_index != 1)))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK);
+
+ if (cdir_size < (mz_uint64)pZip->m_total_files * MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if (cdir_size> pZip->m_archive_size || cdir_ofs > pZip->m_archive_size - cdir_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if (eocd_ofs < cdir_ofs + cdir_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ /* The end of central dir follows the central dir, unless the zip file has
+ * some trailing data (e.g. it is appended to an executable file). */
+ archive_ofs = eocd_ofs - (cdir_ofs + cdir_size);
+ if (pZip->m_pState->m_zip64)
+ {
+ if (archive_ofs < MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE +
+ MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ archive_ofs -= MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE +
+ MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE;
+ }
+
+ /* Update the archive start position, but only if not specified. */
+ if ((pZip->m_zip_type == MZ_ZIP_TYPE_FILE || pZip->m_zip_type == MZ_ZIP_TYPE_CFILE ||
+ pZip->m_zip_type == MZ_ZIP_TYPE_USER) && pZip->m_pState->m_file_archive_start_ofs == 0)
+ {
+ pZip->m_pState->m_file_archive_start_ofs = archive_ofs;
+ pZip->m_archive_size -= archive_ofs;
+ }
+
+ pZip->m_central_directory_file_ofs = cdir_ofs;
+
+ if (pZip->m_total_files)
+ {
+ mz_uint i, n;
+ /* Read the entire central directory into a heap block, and allocate another heap block to hold the unsorted central dir file record offsets, and possibly another to hold the sorted indices. */
+ if ((!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir, cdir_size, MZ_FALSE)) ||
+ (!mz_zip_array_resize(pZip, &pZip->m_pState->m_central_dir_offsets, pZip->m_total_files, MZ_FALSE)))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ if (sort_central_dir)
+ {
+ if (!mz_zip_array_resize(pZip, &pZip->m_pState->m_sorted_central_dir_offsets, pZip->m_total_files, MZ_FALSE))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs, pZip->m_pState->m_central_dir.m_p, cdir_size) != cdir_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+
+ /* Now create an index into the central directory file records, do some basic sanity checking on each record */
+ p = (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p;
+ for (n = cdir_size, i = 0; i < pZip->m_total_files; ++i)
+ {
+ mz_uint total_header_size, disk_index, bit_flags, filename_size, ext_data_size;
+ mz_uint64 comp_size, decomp_size, local_header_ofs;
+
+ if ((n < MZ_ZIP_CENTRAL_DIR_HEADER_SIZE) || (MZ_READ_LE32(p) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, i) = (mz_uint32)(p - (const mz_uint8 *)pZip->m_pState->m_central_dir.m_p);
+
+ if (sort_central_dir)
+ MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_sorted_central_dir_offsets, mz_uint32, i) = i;
+
+ comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS);
+ decomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS);
+ local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS);
+ filename_size = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ ext_data_size = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS);
+
+ if ((!pZip->m_pState->m_zip64_has_extended_info_fields) &&
+ (ext_data_size) &&
+ (MZ_MAX(MZ_MAX(comp_size, decomp_size), local_header_ofs) == MZ_UINT32_MAX))
+ {
+ /* Attempt to find zip64 extended information field in the entry's extra data */
+ mz_uint32 extra_size_remaining = ext_data_size;
+
+ if (extra_size_remaining)
+ {
+ const mz_uint8 *pExtra_data;
+ void *buf = NULL;
+
+ if (MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + ext_data_size > n)
+ {
+ buf = MZ_MALLOC(ext_data_size);
+ if (buf == NULL)
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cdir_ofs + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size, buf, ext_data_size) != ext_data_size)
+ {
+ MZ_FREE(buf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ }
+
+ pExtra_data = (mz_uint8 *)buf;
+ }
+ else
+ {
+ pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size;
+ }
+
+ do
+ {
+ mz_uint32 field_id;
+ mz_uint32 field_data_size;
+
+ if (extra_size_remaining < (sizeof(mz_uint16) * 2))
+ {
+ MZ_FREE(buf);
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ }
+
+ field_id = MZ_READ_LE16(pExtra_data);
+ field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16));
+
+ if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining)
+ {
+ MZ_FREE(buf);
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ }
+
+ if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID)
+ {
+ /* Ok, the archive didn't have any zip64 headers but it uses a zip64 extended information field so mark it as zip64 anyway (this can occur with infozip's zip util when it reads compresses files from stdin). */
+ pZip->m_pState->m_zip64 = MZ_TRUE;
+ pZip->m_pState->m_zip64_has_extended_info_fields = MZ_TRUE;
+ break;
+ }
+
+ pExtra_data += sizeof(mz_uint16) * 2 + field_data_size;
+ extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size;
+ } while (extra_size_remaining);
+
+ MZ_FREE(buf);
+ }
+ }
+
+ /* I've seen archives that aren't marked as zip64 that uses zip64 ext data, argh */
+ if ((comp_size != MZ_UINT32_MAX) && (decomp_size != MZ_UINT32_MAX))
+ {
+ if (((!MZ_READ_LE32(p + MZ_ZIP_CDH_METHOD_OFS)) && (decomp_size != comp_size)) || (decomp_size && !comp_size))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ }
+
+ disk_index = MZ_READ_LE16(p + MZ_ZIP_CDH_DISK_START_OFS);
+ if ((disk_index == MZ_UINT16_MAX) || ((disk_index != num_this_disk) && (disk_index != 1)))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_MULTIDISK);
+
+ if (comp_size != MZ_UINT32_MAX)
+ {
+ if (((mz_uint64)MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS) + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + comp_size) > pZip->m_archive_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ }
+
+ bit_flags = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS);
+ if (bit_flags & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_LOCAL_DIR_IS_MASKED)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION);
+
+ if ((total_header_size = MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS)) > n)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ n -= total_header_size;
+ p += total_header_size;
+ }
+ }
+
+ if (sort_central_dir)
+ mz_zip_reader_sort_central_dir_offsets_by_filename(pZip);
+
+ return MZ_TRUE;
+ }
+
+ void mz_zip_zero_struct(mz_zip_archive *pZip)
+ {
+ if (pZip)
+ MZ_CLEAR_PTR(pZip);
+ }
+
+ static mz_bool mz_zip_reader_end_internal(mz_zip_archive *pZip, mz_bool set_last_error)
+ {
+ mz_bool status = MZ_TRUE;
+
+ if (!pZip)
+ return MZ_FALSE;
+
+ if ((!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING))
+ {
+ if (set_last_error)
+ pZip->m_last_error = MZ_ZIP_INVALID_PARAMETER;
+
+ return MZ_FALSE;
+ }
+
+ if (pZip->m_pState)
+ {
+ mz_zip_internal_state *pState = pZip->m_pState;
+ pZip->m_pState = NULL;
+
+ mz_zip_array_clear(pZip, &pState->m_central_dir);
+ mz_zip_array_clear(pZip, &pState->m_central_dir_offsets);
+ mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets);
+
+#ifndef MINIZ_NO_STDIO
+ if (pState->m_pFile)
+ {
+ if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE)
+ {
+ if (MZ_FCLOSE(pState->m_pFile) == EOF)
+ {
+ if (set_last_error)
+ pZip->m_last_error = MZ_ZIP_FILE_CLOSE_FAILED;
+ status = MZ_FALSE;
+ }
+ }
+ pState->m_pFile = NULL;
+ }
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ }
+ pZip->m_zip_mode = MZ_ZIP_MODE_INVALID;
+
+ return status;
+ }
+
+ mz_bool mz_zip_reader_end(mz_zip_archive *pZip)
+ {
+ return mz_zip_reader_end_internal(pZip, MZ_TRUE);
+ }
+ mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags)
+ {
+ if ((!pZip) || (!pZip->m_pRead))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (!mz_zip_reader_init_internal(pZip, flags))
+ return MZ_FALSE;
+
+ pZip->m_zip_type = MZ_ZIP_TYPE_USER;
+ pZip->m_archive_size = size;
+
+ if (!mz_zip_reader_read_central_dir(pZip, flags))
+ {
+ mz_zip_reader_end_internal(pZip, MZ_FALSE);
+ return MZ_FALSE;
+ }
+
+ return MZ_TRUE;
+ }
+
+ static size_t mz_zip_mem_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n)
+ {
+ mz_zip_archive *pZip = (mz_zip_archive *)pOpaque;
+ size_t s = (file_ofs >= pZip->m_archive_size) ? 0 : (size_t)MZ_MIN(pZip->m_archive_size - file_ofs, n);
+ memcpy(pBuf, (const mz_uint8 *)pZip->m_pState->m_pMem + file_ofs, s);
+ return s;
+ }
+
+ mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags)
+ {
+ if (!pMem)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+
+ if (!mz_zip_reader_init_internal(pZip, flags))
+ return MZ_FALSE;
+
+ pZip->m_zip_type = MZ_ZIP_TYPE_MEMORY;
+ pZip->m_archive_size = size;
+ pZip->m_pRead = mz_zip_mem_read_func;
+ pZip->m_pIO_opaque = pZip;
+ pZip->m_pNeeds_keepalive = NULL;
+
+#ifdef __cplusplus
+ pZip->m_pState->m_pMem = const_cast(pMem);
+#else
+ pZip->m_pState->m_pMem = (void *)pMem;
+#endif
+
+ pZip->m_pState->m_mem_size = size;
+
+ if (!mz_zip_reader_read_central_dir(pZip, flags))
+ {
+ mz_zip_reader_end_internal(pZip, MZ_FALSE);
+ return MZ_FALSE;
+ }
+
+ return MZ_TRUE;
+ }
+
+#ifndef MINIZ_NO_STDIO
+ static size_t mz_zip_file_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n)
+ {
+ mz_zip_archive *pZip = (mz_zip_archive *)pOpaque;
+ mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile);
+
+ file_ofs += pZip->m_pState->m_file_archive_start_ofs;
+
+ if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET))))
+ return 0;
+
+ return MZ_FREAD(pBuf, 1, n, pZip->m_pState->m_pFile);
+ }
+
+ mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags)
+ {
+ return mz_zip_reader_init_file_v2(pZip, pFilename, flags, 0, 0);
+ }
+
+ mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size)
+ {
+ mz_uint64 file_size;
+ MZ_FILE *pFile;
+
+ if ((!pZip) || (!pFilename) || ((archive_size) && (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE)))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pFile = MZ_FOPEN(pFilename, (flags & MZ_ZIP_FLAG_READ_ALLOW_WRITING ) ? "r+b" : "rb");
+ if (!pFile)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED);
+
+ file_size = archive_size;
+ if (!file_size)
+ {
+ if (MZ_FSEEK64(pFile, 0, SEEK_END))
+ {
+ MZ_FCLOSE(pFile);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED);
+ }
+
+ file_size = MZ_FTELL64(pFile);
+ }
+
+ /* TODO: Better sanity check archive_size and the # of actual remaining bytes */
+
+ if (file_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ {
+ MZ_FCLOSE(pFile);
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+ }
+
+ if (!mz_zip_reader_init_internal(pZip, flags))
+ {
+ MZ_FCLOSE(pFile);
+ return MZ_FALSE;
+ }
+
+ pZip->m_zip_type = MZ_ZIP_TYPE_FILE;
+ pZip->m_pRead = mz_zip_file_read_func;
+ pZip->m_pIO_opaque = pZip;
+ pZip->m_pState->m_pFile = pFile;
+ pZip->m_archive_size = file_size;
+ pZip->m_pState->m_file_archive_start_ofs = file_start_ofs;
+
+ if (!mz_zip_reader_read_central_dir(pZip, flags))
+ {
+ mz_zip_reader_end_internal(pZip, MZ_FALSE);
+ return MZ_FALSE;
+ }
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags)
+ {
+ mz_uint64 cur_file_ofs;
+
+ if ((!pZip) || (!pFile))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED);
+
+ cur_file_ofs = MZ_FTELL64(pFile);
+
+ if (!archive_size)
+ {
+ if (MZ_FSEEK64(pFile, 0, SEEK_END))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED);
+
+ archive_size = MZ_FTELL64(pFile) - cur_file_ofs;
+
+ if (archive_size < MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_NOT_AN_ARCHIVE);
+ }
+
+ if (!mz_zip_reader_init_internal(pZip, flags))
+ return MZ_FALSE;
+
+ pZip->m_zip_type = MZ_ZIP_TYPE_CFILE;
+ pZip->m_pRead = mz_zip_file_read_func;
+
+ pZip->m_pIO_opaque = pZip;
+ pZip->m_pState->m_pFile = pFile;
+ pZip->m_archive_size = archive_size;
+ pZip->m_pState->m_file_archive_start_ofs = cur_file_ofs;
+
+ if (!mz_zip_reader_read_central_dir(pZip, flags))
+ {
+ mz_zip_reader_end_internal(pZip, MZ_FALSE);
+ return MZ_FALSE;
+ }
+
+ return MZ_TRUE;
+ }
+
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ static MZ_FORCEINLINE const mz_uint8 *mz_zip_get_cdh(mz_zip_archive *pZip, mz_uint file_index)
+ {
+ if ((!pZip) || (!pZip->m_pState) || (file_index >= pZip->m_total_files))
+ return NULL;
+ return &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index));
+ }
+
+ mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index)
+ {
+ mz_uint m_bit_flag;
+ const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index);
+ if (!p)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ return MZ_FALSE;
+ }
+
+ m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS);
+ return (m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION)) != 0;
+ }
+
+ mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index)
+ {
+ mz_uint bit_flag;
+ mz_uint method;
+
+ const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index);
+ if (!p)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ return MZ_FALSE;
+ }
+
+ method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS);
+ bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS);
+
+ if ((method != 0) && (method != MZ_DEFLATED))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD);
+ return MZ_FALSE;
+ }
+
+ if (bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION);
+ return MZ_FALSE;
+ }
+
+ if (bit_flag & MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE);
+ return MZ_FALSE;
+ }
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index)
+ {
+ mz_uint filename_len, attribute_mapping_id, external_attr;
+ const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index);
+ if (!p)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ return MZ_FALSE;
+ }
+
+ filename_len = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ if (filename_len)
+ {
+ if (*(p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_len - 1) == '/')
+ return MZ_TRUE;
+ }
+
+ /* Bugfix: This code was also checking if the internal attribute was non-zero, which wasn't correct. */
+ /* Most/all zip writers (hopefully) set DOS file/directory attributes in the low 16-bits, so check for the DOS directory flag and ignore the source OS ID in the created by field. */
+ /* FIXME: Remove this check? Is it necessary - we already check the filename. */
+ attribute_mapping_id = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS) >> 8;
+ (void)attribute_mapping_id;
+
+ external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS);
+ if ((external_attr & MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG) != 0)
+ {
+ return MZ_TRUE;
+ }
+
+ return MZ_FALSE;
+ }
+
+ static mz_bool mz_zip_file_stat_internal(mz_zip_archive *pZip, mz_uint file_index, const mz_uint8 *pCentral_dir_header, mz_zip_archive_file_stat *pStat, mz_bool *pFound_zip64_extra_data)
+ {
+ mz_uint n;
+ const mz_uint8 *p = pCentral_dir_header;
+
+ if (pFound_zip64_extra_data)
+ *pFound_zip64_extra_data = MZ_FALSE;
+
+ if ((!p) || (!pStat))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ /* Extract fields from the central directory record. */
+ pStat->m_file_index = file_index;
+ pStat->m_central_dir_ofs = MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index);
+ pStat->m_version_made_by = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_MADE_BY_OFS);
+ pStat->m_version_needed = MZ_READ_LE16(p + MZ_ZIP_CDH_VERSION_NEEDED_OFS);
+ pStat->m_bit_flag = MZ_READ_LE16(p + MZ_ZIP_CDH_BIT_FLAG_OFS);
+ pStat->m_method = MZ_READ_LE16(p + MZ_ZIP_CDH_METHOD_OFS);
+#ifndef MINIZ_NO_TIME
+ pStat->m_time = mz_zip_dos_to_time_t(MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_TIME_OFS), MZ_READ_LE16(p + MZ_ZIP_CDH_FILE_DATE_OFS));
+#endif
+ pStat->m_crc32 = MZ_READ_LE32(p + MZ_ZIP_CDH_CRC32_OFS);
+ pStat->m_comp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS);
+ pStat->m_uncomp_size = MZ_READ_LE32(p + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS);
+ pStat->m_internal_attr = MZ_READ_LE16(p + MZ_ZIP_CDH_INTERNAL_ATTR_OFS);
+ pStat->m_external_attr = MZ_READ_LE32(p + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS);
+ pStat->m_local_header_ofs = MZ_READ_LE32(p + MZ_ZIP_CDH_LOCAL_HEADER_OFS);
+
+ /* Copy as much of the filename and comment as possible. */
+ n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE - 1);
+ memcpy(pStat->m_filename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n);
+ pStat->m_filename[n] = '\0';
+
+ n = MZ_READ_LE16(p + MZ_ZIP_CDH_COMMENT_LEN_OFS);
+ n = MZ_MIN(n, MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE - 1);
+ pStat->m_comment_size = n;
+ memcpy(pStat->m_comment, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS) + MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS), n);
+ pStat->m_comment[n] = '\0';
+
+ /* Set some flags for convienance */
+ pStat->m_is_directory = mz_zip_reader_is_file_a_directory(pZip, file_index);
+ pStat->m_is_encrypted = mz_zip_reader_is_file_encrypted(pZip, file_index);
+ pStat->m_is_supported = mz_zip_reader_is_file_supported(pZip, file_index);
+
+ /* See if we need to read any zip64 extended information fields. */
+ /* Confusingly, these zip64 fields can be present even on non-zip64 archives (Debian zip on a huge files from stdin piped to stdout creates them). */
+ if (MZ_MAX(MZ_MAX(pStat->m_comp_size, pStat->m_uncomp_size), pStat->m_local_header_ofs) == MZ_UINT32_MAX)
+ {
+ /* Attempt to find zip64 extended information field in the entry's extra data */
+ mz_uint32 extra_size_remaining = MZ_READ_LE16(p + MZ_ZIP_CDH_EXTRA_LEN_OFS);
+
+ if (extra_size_remaining)
+ {
+ const mz_uint8 *pExtra_data = p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+
+ do
+ {
+ mz_uint32 field_id;
+ mz_uint32 field_data_size;
+
+ if (extra_size_remaining < (sizeof(mz_uint16) * 2))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ field_id = MZ_READ_LE16(pExtra_data);
+ field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16));
+
+ if ((field_data_size + sizeof(mz_uint16) * 2) > extra_size_remaining)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID)
+ {
+ const mz_uint8 *pField_data = pExtra_data + sizeof(mz_uint16) * 2;
+ mz_uint32 field_data_remaining = field_data_size;
+
+ if (pFound_zip64_extra_data)
+ *pFound_zip64_extra_data = MZ_TRUE;
+
+ if (pStat->m_uncomp_size == MZ_UINT32_MAX)
+ {
+ if (field_data_remaining < sizeof(mz_uint64))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ pStat->m_uncomp_size = MZ_READ_LE64(pField_data);
+ pField_data += sizeof(mz_uint64);
+ field_data_remaining -= sizeof(mz_uint64);
+ }
+
+ if (pStat->m_comp_size == MZ_UINT32_MAX)
+ {
+ if (field_data_remaining < sizeof(mz_uint64))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ pStat->m_comp_size = MZ_READ_LE64(pField_data);
+ pField_data += sizeof(mz_uint64);
+ field_data_remaining -= sizeof(mz_uint64);
+ }
+
+ if (pStat->m_local_header_ofs == MZ_UINT32_MAX)
+ {
+ if (field_data_remaining < sizeof(mz_uint64))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ pStat->m_local_header_ofs = MZ_READ_LE64(pField_data);
+ pField_data += sizeof(mz_uint64);
+ field_data_remaining -= sizeof(mz_uint64);
+ }
+
+ break;
+ }
+
+ pExtra_data += sizeof(mz_uint16) * 2 + field_data_size;
+ extra_size_remaining = extra_size_remaining - sizeof(mz_uint16) * 2 - field_data_size;
+ } while (extra_size_remaining);
+ }
+ }
+
+ return MZ_TRUE;
+ }
+
+ static MZ_FORCEINLINE mz_bool mz_zip_string_equal(const char *pA, const char *pB, mz_uint len, mz_uint flags)
+ {
+ mz_uint i;
+ if (flags & MZ_ZIP_FLAG_CASE_SENSITIVE)
+ return 0 == memcmp(pA, pB, len);
+ for (i = 0; i < len; ++i)
+ if (MZ_TOLOWER(pA[i]) != MZ_TOLOWER(pB[i]))
+ return MZ_FALSE;
+ return MZ_TRUE;
+ }
+
+ static MZ_FORCEINLINE int mz_zip_filename_compare(const mz_zip_array *pCentral_dir_array, const mz_zip_array *pCentral_dir_offsets, mz_uint l_index, const char *pR, mz_uint r_len)
+ {
+ const mz_uint8 *pL = &MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_array, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(pCentral_dir_offsets, mz_uint32, l_index)), *pE;
+ mz_uint l_len = MZ_READ_LE16(pL + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ mz_uint8 l = 0, r = 0;
+ pL += MZ_ZIP_CENTRAL_DIR_HEADER_SIZE;
+ pE = pL + MZ_MIN(l_len, r_len);
+ while (pL < pE)
+ {
+ if ((l = MZ_TOLOWER(*pL)) != (r = MZ_TOLOWER(*pR)))
+ break;
+ pL++;
+ pR++;
+ }
+ return (pL == pE) ? (int)(l_len - r_len) : (l - r);
+ }
+
+ static mz_bool mz_zip_locate_file_binary_search(mz_zip_archive *pZip, const char *pFilename, mz_uint32 *pIndex)
+ {
+ mz_zip_internal_state *pState = pZip->m_pState;
+ const mz_zip_array *pCentral_dir_offsets = &pState->m_central_dir_offsets;
+ const mz_zip_array *pCentral_dir = &pState->m_central_dir;
+ mz_uint32 *pIndices = &MZ_ZIP_ARRAY_ELEMENT(&pState->m_sorted_central_dir_offsets, mz_uint32, 0);
+ const mz_uint32 size = pZip->m_total_files;
+ const mz_uint filename_len = (mz_uint)strlen(pFilename);
+
+ if (pIndex)
+ *pIndex = 0;
+
+ if (size)
+ {
+ /* yes I could use uint32_t's, but then we would have to add some special case checks in the loop, argh, and */
+ /* honestly the major expense here on 32-bit CPU's will still be the filename compare */
+ mz_int64 l = 0, h = (mz_int64)size - 1;
+
+ while (l <= h)
+ {
+ mz_int64 m = l + ((h - l) >> 1);
+ mz_uint32 file_index = pIndices[(mz_uint32)m];
+
+ int comp = mz_zip_filename_compare(pCentral_dir, pCentral_dir_offsets, file_index, pFilename, filename_len);
+ if (!comp)
+ {
+ if (pIndex)
+ *pIndex = file_index;
+ return MZ_TRUE;
+ }
+ else if (comp < 0)
+ l = m + 1;
+ else
+ h = m - 1;
+ }
+ }
+
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND);
+ }
+
+ int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags)
+ {
+ mz_uint32 index;
+ if (!mz_zip_reader_locate_file_v2(pZip, pName, pComment, flags, &index))
+ return -1;
+ else
+ return (int)index;
+ }
+
+ mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *pIndex)
+ {
+ mz_uint file_index;
+ size_t name_len, comment_len;
+
+ if (pIndex)
+ *pIndex = 0;
+
+ if ((!pZip) || (!pZip->m_pState) || (!pName))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ /* See if we can use a binary search */
+ if (((pZip->m_pState->m_init_flags & MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY) == 0) &&
+ (pZip->m_zip_mode == MZ_ZIP_MODE_READING) &&
+ ((flags & (MZ_ZIP_FLAG_IGNORE_PATH | MZ_ZIP_FLAG_CASE_SENSITIVE)) == 0) && (!pComment) && (pZip->m_pState->m_sorted_central_dir_offsets.m_size))
+ {
+ return mz_zip_locate_file_binary_search(pZip, pName, pIndex);
+ }
+
+ /* Locate the entry by scanning the entire central directory */
+ name_len = strlen(pName);
+ if (name_len > MZ_UINT16_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ comment_len = pComment ? strlen(pComment) : 0;
+ if (comment_len > MZ_UINT16_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ for (file_index = 0; file_index < pZip->m_total_files; file_index++)
+ {
+ const mz_uint8 *pHeader = &MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir, mz_uint8, MZ_ZIP_ARRAY_ELEMENT(&pZip->m_pState->m_central_dir_offsets, mz_uint32, file_index));
+ mz_uint filename_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ const char *pFilename = (const char *)pHeader + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE;
+ if (filename_len < name_len)
+ continue;
+ if (comment_len)
+ {
+ mz_uint file_extra_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_EXTRA_LEN_OFS), file_comment_len = MZ_READ_LE16(pHeader + MZ_ZIP_CDH_COMMENT_LEN_OFS);
+ const char *pFile_comment = pFilename + filename_len + file_extra_len;
+ if ((file_comment_len != comment_len) || (!mz_zip_string_equal(pComment, pFile_comment, file_comment_len, flags)))
+ continue;
+ }
+ if ((flags & MZ_ZIP_FLAG_IGNORE_PATH) && (filename_len))
+ {
+ int ofs = filename_len - 1;
+ do
+ {
+ if ((pFilename[ofs] == '/') || (pFilename[ofs] == '\\') || (pFilename[ofs] == ':'))
+ break;
+ } while (--ofs >= 0);
+ ofs++;
+ pFilename += ofs;
+ filename_len -= ofs;
+ }
+ if ((filename_len == name_len) && (mz_zip_string_equal(pName, pFilename, filename_len, flags)))
+ {
+ if (pIndex)
+ *pIndex = file_index;
+ return MZ_TRUE;
+ }
+ }
+
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_NOT_FOUND);
+ }
+
+ static mz_bool mz_zip_reader_extract_to_mem_no_alloc1(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size, const mz_zip_archive_file_stat *st)
+ {
+ int status = TINFL_STATUS_DONE;
+ mz_uint64 needed_size, cur_file_ofs, comp_remaining, out_buf_ofs = 0, read_buf_size, read_buf_ofs = 0, read_buf_avail;
+ mz_zip_archive_file_stat file_stat;
+ void *pRead_buf;
+ mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)];
+ mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32;
+ tinfl_decompressor inflator;
+
+ if ((!pZip) || (!pZip->m_pState) || ((buf_size) && (!pBuf)) || ((user_read_buf_size) && (!pUser_read_buf)) || (!pZip->m_pRead))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (st)
+ {
+ file_stat = *st;
+ }
+ else if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat))
+ return MZ_FALSE;
+
+ /* A directory or zero length file */
+ if ((file_stat.m_is_directory) || (!file_stat.m_comp_size))
+ return MZ_TRUE;
+
+ /* Encryption and patch files are not supported. */
+ if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION);
+
+ /* This function only supports decompressing stored and deflate. */
+ if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD);
+
+ /* Ensure supplied output buffer is large enough. */
+ needed_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size;
+ if (buf_size < needed_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_BUF_TOO_SMALL);
+
+ /* Read and parse the local directory entry. */
+ cur_file_ofs = file_stat.m_local_header_ofs;
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+
+ if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ cur_file_ofs += (mz_uint64)(MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS);
+ if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method))
+ {
+ /* The file is stored or the caller has requested the compressed data. */
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pBuf, (size_t)needed_size) != needed_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) == 0)
+ {
+ if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32)
+ return mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED);
+ }
+#endif
+
+ return MZ_TRUE;
+ }
+
+ /* Decompress the file either directly from memory or from a file input buffer. */
+ tinfl_init(&inflator);
+
+ if (pZip->m_pState->m_pMem)
+ {
+ /* Read directly from the archive in memory. */
+ pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs;
+ read_buf_size = read_buf_avail = file_stat.m_comp_size;
+ comp_remaining = 0;
+ }
+ else if (pUser_read_buf)
+ {
+ /* Use a user provided read buffer. */
+ if (!user_read_buf_size)
+ return MZ_FALSE;
+ pRead_buf = (mz_uint8 *)pUser_read_buf;
+ read_buf_size = user_read_buf_size;
+ read_buf_avail = 0;
+ comp_remaining = file_stat.m_comp_size;
+ }
+ else
+ {
+ /* Temporarily allocate a read buffer. */
+ read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE);
+ if (((sizeof(size_t) == sizeof(mz_uint32))) && (read_buf_size > 0x7FFFFFFF))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size)))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ read_buf_avail = 0;
+ comp_remaining = file_stat.m_comp_size;
+ }
+
+ do
+ {
+ /* The size_t cast here should be OK because we've verified that the output buffer is >= file_stat.m_uncomp_size above */
+ size_t in_buf_size, out_buf_size = (size_t)(file_stat.m_uncomp_size - out_buf_ofs);
+ if ((!read_buf_avail) && (!pZip->m_pState->m_pMem))
+ {
+ read_buf_avail = MZ_MIN(read_buf_size, comp_remaining);
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail)
+ {
+ status = TINFL_STATUS_FAILED;
+ mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED);
+ break;
+ }
+ cur_file_ofs += read_buf_avail;
+ comp_remaining -= read_buf_avail;
+ read_buf_ofs = 0;
+ }
+ in_buf_size = (size_t)read_buf_avail;
+ status = tinfl_decompress(&inflator, (mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pBuf, (mz_uint8 *)pBuf + out_buf_ofs, &out_buf_size, TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF | (comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0));
+ read_buf_avail -= in_buf_size;
+ read_buf_ofs += in_buf_size;
+ out_buf_ofs += out_buf_size;
+ } while (status == TINFL_STATUS_NEEDS_MORE_INPUT);
+
+ if (status == TINFL_STATUS_DONE)
+ {
+ /* Make sure the entire file was decompressed, and check its CRC. */
+ if (out_buf_ofs != file_stat.m_uncomp_size)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE);
+ status = TINFL_STATUS_FAILED;
+ }
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ else if (mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, (size_t)file_stat.m_uncomp_size) != file_stat.m_crc32)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_CRC_CHECK_FAILED);
+ status = TINFL_STATUS_FAILED;
+ }
+#endif
+ }
+
+ if ((!pZip->m_pState->m_pMem) && (!pUser_read_buf))
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+
+ return status == TINFL_STATUS_DONE;
+ }
+
+ mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size)
+ {
+ return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL);
+ }
+
+ mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size)
+ {
+ mz_uint32 file_index;
+ if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index))
+ return MZ_FALSE;
+ return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, pUser_read_buf, user_read_buf_size, NULL);
+ }
+
+ mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags)
+ {
+ return mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, buf_size, flags, NULL, 0, NULL);
+ }
+
+ mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags)
+ {
+ return mz_zip_reader_extract_file_to_mem_no_alloc(pZip, pFilename, pBuf, buf_size, flags, NULL, 0);
+ }
+
+ void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags)
+ {
+ mz_zip_archive_file_stat file_stat;
+ mz_uint64 alloc_size;
+ void *pBuf;
+
+ if (pSize)
+ *pSize = 0;
+
+ if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat))
+ return NULL;
+
+ alloc_size = (flags & MZ_ZIP_FLAG_COMPRESSED_DATA) ? file_stat.m_comp_size : file_stat.m_uncomp_size;
+ if (((sizeof(size_t) == sizeof(mz_uint32))) && (alloc_size > 0x7FFFFFFF))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+ return NULL;
+ }
+
+ if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)alloc_size)))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ return NULL;
+ }
+
+ if (!mz_zip_reader_extract_to_mem_no_alloc1(pZip, file_index, pBuf, (size_t)alloc_size, flags, NULL, 0, &file_stat))
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf);
+ return NULL;
+ }
+
+ if (pSize)
+ *pSize = (size_t)alloc_size;
+ return pBuf;
+ }
+
+ void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags)
+ {
+ mz_uint32 file_index;
+ if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index))
+ {
+ if (pSize)
+ *pSize = 0;
+ return MZ_FALSE;
+ }
+ return mz_zip_reader_extract_to_heap(pZip, file_index, pSize, flags);
+ }
+
+ mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags)
+ {
+ int status = TINFL_STATUS_DONE;
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ mz_uint file_crc32 = MZ_CRC32_INIT;
+#endif
+ mz_uint64 read_buf_size, read_buf_ofs = 0, read_buf_avail, comp_remaining, out_buf_ofs = 0, cur_file_ofs;
+ mz_zip_archive_file_stat file_stat;
+ void *pRead_buf = NULL;
+ void *pWrite_buf = NULL;
+ mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)];
+ mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32;
+
+ if ((!pZip) || (!pZip->m_pState) || (!pCallback) || (!pZip->m_pRead))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat))
+ return MZ_FALSE;
+
+ /* A directory or zero length file */
+ if ((file_stat.m_is_directory) || (!file_stat.m_comp_size))
+ return MZ_TRUE;
+
+ /* Encryption and patch files are not supported. */
+ if (file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION);
+
+ /* This function only supports decompressing stored and deflate. */
+ if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD);
+
+ /* Read and do some minimal validation of the local directory entry (this doesn't crack the zip64 stuff, which we already have from the central dir) */
+ cur_file_ofs = file_stat.m_local_header_ofs;
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+
+ if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ cur_file_ofs += (mz_uint64)(MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS);
+ if ((cur_file_ofs + file_stat.m_comp_size) > pZip->m_archive_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ /* Decompress the file either directly from memory or from a file input buffer. */
+ if (pZip->m_pState->m_pMem)
+ {
+ pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + cur_file_ofs;
+ read_buf_size = read_buf_avail = file_stat.m_comp_size;
+ comp_remaining = 0;
+ }
+ else
+ {
+ read_buf_size = MZ_MIN(file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE);
+ if (NULL == (pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)read_buf_size)))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ read_buf_avail = 0;
+ comp_remaining = file_stat.m_comp_size;
+ }
+
+ if ((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!file_stat.m_method))
+ {
+ /* The file is stored or the caller has requested the compressed data. */
+ if (pZip->m_pState->m_pMem)
+ {
+ if (((sizeof(size_t) == sizeof(mz_uint32))) && (file_stat.m_comp_size > MZ_UINT32_MAX))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)file_stat.m_comp_size) != file_stat.m_comp_size)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED);
+ status = TINFL_STATUS_FAILED;
+ }
+ else if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA))
+ {
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)file_stat.m_comp_size);
+#endif
+ }
+
+ cur_file_ofs += file_stat.m_comp_size;
+ out_buf_ofs += file_stat.m_comp_size;
+ comp_remaining = 0;
+ }
+ else
+ {
+ while (comp_remaining)
+ {
+ read_buf_avail = MZ_MIN(read_buf_size, comp_remaining);
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ status = TINFL_STATUS_FAILED;
+ break;
+ }
+
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ if (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA))
+ {
+ file_crc32 = (mz_uint32)mz_crc32(file_crc32, (const mz_uint8 *)pRead_buf, (size_t)read_buf_avail);
+ }
+#endif
+
+ if (pCallback(pOpaque, out_buf_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED);
+ status = TINFL_STATUS_FAILED;
+ break;
+ }
+
+ cur_file_ofs += read_buf_avail;
+ out_buf_ofs += read_buf_avail;
+ comp_remaining -= read_buf_avail;
+ }
+ }
+ }
+ else
+ {
+ tinfl_decompressor inflator;
+ tinfl_init(&inflator);
+
+ if (NULL == (pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE)))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ status = TINFL_STATUS_FAILED;
+ }
+ else
+ {
+ do
+ {
+ mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pWrite_buf + (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1));
+ size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1));
+ if ((!read_buf_avail) && (!pZip->m_pState->m_pMem))
+ {
+ read_buf_avail = MZ_MIN(read_buf_size, comp_remaining);
+ if (pZip->m_pRead(pZip->m_pIO_opaque, cur_file_ofs, pRead_buf, (size_t)read_buf_avail) != read_buf_avail)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ status = TINFL_STATUS_FAILED;
+ break;
+ }
+ cur_file_ofs += read_buf_avail;
+ comp_remaining -= read_buf_avail;
+ read_buf_ofs = 0;
+ }
+
+ in_buf_size = (size_t)read_buf_avail;
+ status = tinfl_decompress(&inflator, (const mz_uint8 *)pRead_buf + read_buf_ofs, &in_buf_size, (mz_uint8 *)pWrite_buf, pWrite_buf_cur, &out_buf_size, comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0);
+ read_buf_avail -= in_buf_size;
+ read_buf_ofs += in_buf_size;
+
+ if (out_buf_size)
+ {
+ if (pCallback(pOpaque, out_buf_ofs, pWrite_buf_cur, out_buf_size) != out_buf_size)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_WRITE_CALLBACK_FAILED);
+ status = TINFL_STATUS_FAILED;
+ break;
+ }
+
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ file_crc32 = (mz_uint32)mz_crc32(file_crc32, pWrite_buf_cur, out_buf_size);
+#endif
+ if ((out_buf_ofs += out_buf_size) > file_stat.m_uncomp_size)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED);
+ status = TINFL_STATUS_FAILED;
+ break;
+ }
+ }
+ } while ((status == TINFL_STATUS_NEEDS_MORE_INPUT) || (status == TINFL_STATUS_HAS_MORE_OUTPUT));
+ }
+ }
+
+ if ((status == TINFL_STATUS_DONE) && (!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)))
+ {
+ /* Make sure the entire file was decompressed, and check its CRC. */
+ if (out_buf_ofs != file_stat.m_uncomp_size)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE);
+ status = TINFL_STATUS_FAILED;
+ }
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ else if (file_crc32 != file_stat.m_crc32)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_DECOMPRESSION_FAILED);
+ status = TINFL_STATUS_FAILED;
+ }
+#endif
+ }
+
+ if (!pZip->m_pState->m_pMem)
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+
+ if (pWrite_buf)
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pWrite_buf);
+
+ return status == TINFL_STATUS_DONE;
+ }
+
+ mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags)
+ {
+ mz_uint32 file_index;
+ if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index))
+ return MZ_FALSE;
+
+ return mz_zip_reader_extract_to_callback(pZip, file_index, pCallback, pOpaque, flags);
+ }
+
+ mz_zip_reader_extract_iter_state *mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags)
+ {
+ mz_zip_reader_extract_iter_state *pState;
+ mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)];
+ mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32;
+
+ /* Argument sanity check */
+ if ((!pZip) || (!pZip->m_pState))
+ return NULL;
+
+ /* Allocate an iterator status structure */
+ pState = (mz_zip_reader_extract_iter_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_reader_extract_iter_state));
+ if (!pState)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ return NULL;
+ }
+
+ /* Fetch file details */
+ if (!mz_zip_reader_file_stat(pZip, file_index, &pState->file_stat))
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+
+ /* Encryption and patch files are not supported. */
+ if (pState->file_stat.m_bit_flag & (MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_IS_ENCRYPTED | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_USES_STRONG_ENCRYPTION | MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_COMPRESSED_PATCH_FLAG))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+
+ /* This function only supports decompressing stored and deflate. */
+ if ((!(flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (pState->file_stat.m_method != 0) && (pState->file_stat.m_method != MZ_DEFLATED))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+
+ /* Init state - save args */
+ pState->pZip = pZip;
+ pState->flags = flags;
+
+ /* Init state - reset variables to defaults */
+ pState->status = TINFL_STATUS_DONE;
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ pState->file_crc32 = MZ_CRC32_INIT;
+#endif
+ pState->read_buf_ofs = 0;
+ pState->out_buf_ofs = 0;
+ pState->pRead_buf = NULL;
+ pState->pWrite_buf = NULL;
+ pState->out_blk_remain = 0;
+
+ /* Read and parse the local directory entry. */
+ pState->cur_file_ofs = pState->file_stat.m_local_header_ofs;
+ if (pZip->m_pRead(pZip->m_pIO_opaque, pState->cur_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+
+ if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+
+ pState->cur_file_ofs += (mz_uint64)(MZ_ZIP_LOCAL_DIR_HEADER_SIZE) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS) + MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS);
+ if ((pState->cur_file_ofs + pState->file_stat.m_comp_size) > pZip->m_archive_size)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+
+ /* Decompress the file either directly from memory or from a file input buffer. */
+ if (pZip->m_pState->m_pMem)
+ {
+ pState->pRead_buf = (mz_uint8 *)pZip->m_pState->m_pMem + pState->cur_file_ofs;
+ pState->read_buf_size = pState->read_buf_avail = pState->file_stat.m_comp_size;
+ pState->comp_remaining = pState->file_stat.m_comp_size;
+ }
+ else
+ {
+ if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method)))
+ {
+ /* Decompression required, therefore intermediate read buffer required */
+ pState->read_buf_size = MZ_MIN(pState->file_stat.m_comp_size, (mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE);
+ if (NULL == (pState->pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)pState->read_buf_size)))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+ }
+ else
+ {
+ /* Decompression not required - we will be reading directly into user buffer, no temp buf required */
+ pState->read_buf_size = 0;
+ }
+ pState->read_buf_avail = 0;
+ pState->comp_remaining = pState->file_stat.m_comp_size;
+ }
+
+ if (!((flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method)))
+ {
+ /* Decompression required, init decompressor */
+ tinfl_init(&pState->inflator);
+
+ /* Allocate write buffer */
+ if (NULL == (pState->pWrite_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, TINFL_LZ_DICT_SIZE)))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ if (pState->pRead_buf)
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState->pRead_buf);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ return NULL;
+ }
+ }
+
+ return pState;
+ }
+
+ mz_zip_reader_extract_iter_state *mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags)
+ {
+ mz_uint32 file_index;
+
+ /* Locate file index by name */
+ if (!mz_zip_reader_locate_file_v2(pZip, pFilename, NULL, flags, &file_index))
+ return NULL;
+
+ /* Construct iterator */
+ return mz_zip_reader_extract_iter_new(pZip, file_index, flags);
+ }
+
+ size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state *pState, void *pvBuf, size_t buf_size)
+ {
+ size_t copied_to_caller = 0;
+
+ /* Argument sanity check */
+ if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState) || (!pvBuf))
+ return 0;
+
+ if ((pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA) || (!pState->file_stat.m_method))
+ {
+ /* The file is stored or the caller has requested the compressed data, calc amount to return. */
+ copied_to_caller = (size_t)MZ_MIN(buf_size, pState->comp_remaining);
+
+ /* Zip is in memory....or requires reading from a file? */
+ if (pState->pZip->m_pState->m_pMem)
+ {
+ /* Copy data to caller's buffer */
+ memcpy(pvBuf, pState->pRead_buf, copied_to_caller);
+ pState->pRead_buf = ((mz_uint8 *)pState->pRead_buf) + copied_to_caller;
+ }
+ else
+ {
+ /* Read directly into caller's buffer */
+ if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pvBuf, copied_to_caller) != copied_to_caller)
+ {
+ /* Failed to read all that was asked for, flag failure and alert user */
+ mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED);
+ pState->status = TINFL_STATUS_FAILED;
+ copied_to_caller = 0;
+ }
+ }
+
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ /* Compute CRC if not returning compressed data only */
+ if (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA))
+ pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, (const mz_uint8 *)pvBuf, copied_to_caller);
+#endif
+
+ /* Advance offsets, dec counters */
+ pState->cur_file_ofs += copied_to_caller;
+ pState->out_buf_ofs += copied_to_caller;
+ pState->comp_remaining -= copied_to_caller;
+ }
+ else
+ {
+ do
+ {
+ /* Calc ptr to write buffer - given current output pos and block size */
+ mz_uint8 *pWrite_buf_cur = (mz_uint8 *)pState->pWrite_buf + (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1));
+
+ /* Calc max output size - given current output pos and block size */
+ size_t in_buf_size, out_buf_size = TINFL_LZ_DICT_SIZE - (pState->out_buf_ofs & (TINFL_LZ_DICT_SIZE - 1));
+
+ if (!pState->out_blk_remain)
+ {
+ /* Read more data from file if none available (and reading from file) */
+ if ((!pState->read_buf_avail) && (!pState->pZip->m_pState->m_pMem))
+ {
+ /* Calc read size */
+ pState->read_buf_avail = MZ_MIN(pState->read_buf_size, pState->comp_remaining);
+ if (pState->pZip->m_pRead(pState->pZip->m_pIO_opaque, pState->cur_file_ofs, pState->pRead_buf, (size_t)pState->read_buf_avail) != pState->read_buf_avail)
+ {
+ mz_zip_set_error(pState->pZip, MZ_ZIP_FILE_READ_FAILED);
+ pState->status = TINFL_STATUS_FAILED;
+ break;
+ }
+
+ /* Advance offsets, dec counters */
+ pState->cur_file_ofs += pState->read_buf_avail;
+ pState->comp_remaining -= pState->read_buf_avail;
+ pState->read_buf_ofs = 0;
+ }
+
+ /* Perform decompression */
+ in_buf_size = (size_t)pState->read_buf_avail;
+ pState->status = tinfl_decompress(&pState->inflator, (const mz_uint8 *)pState->pRead_buf + pState->read_buf_ofs, &in_buf_size, (mz_uint8 *)pState->pWrite_buf, pWrite_buf_cur, &out_buf_size, pState->comp_remaining ? TINFL_FLAG_HAS_MORE_INPUT : 0);
+ pState->read_buf_avail -= in_buf_size;
+ pState->read_buf_ofs += in_buf_size;
+
+ /* Update current output block size remaining */
+ pState->out_blk_remain = out_buf_size;
+ }
+
+ if (pState->out_blk_remain)
+ {
+ /* Calc amount to return. */
+ size_t to_copy = MZ_MIN((buf_size - copied_to_caller), pState->out_blk_remain);
+
+ /* Copy data to caller's buffer */
+ memcpy((mz_uint8 *)pvBuf + copied_to_caller, pWrite_buf_cur, to_copy);
+
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ /* Perform CRC */
+ pState->file_crc32 = (mz_uint32)mz_crc32(pState->file_crc32, pWrite_buf_cur, to_copy);
+#endif
+
+ /* Decrement data consumed from block */
+ pState->out_blk_remain -= to_copy;
+
+ /* Inc output offset, while performing sanity check */
+ if ((pState->out_buf_ofs += to_copy) > pState->file_stat.m_uncomp_size)
+ {
+ mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED);
+ pState->status = TINFL_STATUS_FAILED;
+ break;
+ }
+
+ /* Increment counter of data copied to caller */
+ copied_to_caller += to_copy;
+ }
+ } while ((copied_to_caller < buf_size) && ((pState->status == TINFL_STATUS_NEEDS_MORE_INPUT) || (pState->status == TINFL_STATUS_HAS_MORE_OUTPUT)));
+ }
+
+ /* Return how many bytes were copied into user buffer */
+ return copied_to_caller;
+ }
+
+ mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state *pState)
+ {
+ int status;
+
+ /* Argument sanity check */
+ if ((!pState) || (!pState->pZip) || (!pState->pZip->m_pState))
+ return MZ_FALSE;
+
+ /* Was decompression completed and requested? */
+ if ((pState->status == TINFL_STATUS_DONE) && (!(pState->flags & MZ_ZIP_FLAG_COMPRESSED_DATA)))
+ {
+ /* Make sure the entire file was decompressed, and check its CRC. */
+ if (pState->out_buf_ofs != pState->file_stat.m_uncomp_size)
+ {
+ mz_zip_set_error(pState->pZip, MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE);
+ pState->status = TINFL_STATUS_FAILED;
+ }
+#ifndef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ else if (pState->file_crc32 != pState->file_stat.m_crc32)
+ {
+ mz_zip_set_error(pState->pZip, MZ_ZIP_DECOMPRESSION_FAILED);
+ pState->status = TINFL_STATUS_FAILED;
+ }
+#endif
+ }
+
+ /* Free buffers */
+ if (!pState->pZip->m_pState->m_pMem)
+ pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pRead_buf);
+ if (pState->pWrite_buf)
+ pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState->pWrite_buf);
+
+ /* Save status */
+ status = pState->status;
+
+ /* Free context */
+ pState->pZip->m_pFree(pState->pZip->m_pAlloc_opaque, pState);
+
+ return status == TINFL_STATUS_DONE;
+ }
+
+#ifndef MINIZ_NO_STDIO
+ static size_t mz_zip_file_write_callback(void *pOpaque, mz_uint64 ofs, const void *pBuf, size_t n)
+ {
+ (void)ofs;
+
+ return MZ_FWRITE(pBuf, 1, n, (MZ_FILE *)pOpaque);
+ }
+
+ mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags)
+ {
+ mz_bool status;
+ mz_zip_archive_file_stat file_stat;
+ MZ_FILE *pFile;
+
+ if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat))
+ return MZ_FALSE;
+
+ if ((file_stat.m_is_directory) || (!file_stat.m_is_supported))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE);
+
+ pFile = MZ_FOPEN(pDst_filename, "wb");
+ if (!pFile)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED);
+
+ status = mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags);
+
+ if (MZ_FCLOSE(pFile) == EOF)
+ {
+ if (status)
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED);
+
+ status = MZ_FALSE;
+ }
+
+#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO)
+ if (status)
+ mz_zip_set_file_times(pDst_filename, file_stat.m_time, file_stat.m_time);
+#endif
+
+ return status;
+ }
+
+ mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags)
+ {
+ mz_uint32 file_index;
+ if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index))
+ return MZ_FALSE;
+
+ return mz_zip_reader_extract_to_file(pZip, file_index, pDst_filename, flags);
+ }
+
+ mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *pFile, mz_uint flags)
+ {
+ mz_zip_archive_file_stat file_stat;
+
+ if (!mz_zip_reader_file_stat(pZip, file_index, &file_stat))
+ return MZ_FALSE;
+
+ if ((file_stat.m_is_directory) || (!file_stat.m_is_supported))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE);
+
+ return mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_file_write_callback, pFile, flags);
+ }
+
+ mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags)
+ {
+ mz_uint32 file_index;
+ if (!mz_zip_reader_locate_file_v2(pZip, pArchive_filename, NULL, flags, &file_index))
+ return MZ_FALSE;
+
+ return mz_zip_reader_extract_to_cfile(pZip, file_index, pFile, flags);
+ }
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ static size_t mz_zip_compute_crc32_callback(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n)
+ {
+ mz_uint32 *p = (mz_uint32 *)pOpaque;
+ (void)file_ofs;
+ *p = (mz_uint32)mz_crc32(*p, (const mz_uint8 *)pBuf, n);
+ return n;
+ }
+
+ mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags)
+ {
+ mz_zip_archive_file_stat file_stat;
+ mz_zip_internal_state *pState;
+ const mz_uint8 *pCentral_dir_header;
+ mz_bool found_zip64_ext_data_in_cdir = MZ_FALSE;
+ mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE;
+ mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)];
+ mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32;
+ mz_uint64 local_header_ofs = 0;
+ mz_uint32 local_header_filename_len, local_header_extra_len, local_header_crc32;
+ mz_uint64 local_header_comp_size, local_header_uncomp_size;
+ mz_uint32 uncomp_crc32 = MZ_CRC32_INIT;
+ mz_bool has_data_descriptor;
+ mz_uint32 local_header_bit_flags;
+
+ mz_zip_array file_data_array;
+ mz_zip_array_init(&file_data_array, 1);
+
+ if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (file_index > pZip->m_total_files)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pState = pZip->m_pState;
+
+ pCentral_dir_header = mz_zip_get_cdh(pZip, file_index);
+
+ if (!mz_zip_file_stat_internal(pZip, file_index, pCentral_dir_header, &file_stat, &found_zip64_ext_data_in_cdir))
+ return MZ_FALSE;
+
+ /* A directory or zero length file */
+ if ((file_stat.m_is_directory) || (!file_stat.m_uncomp_size))
+ return MZ_TRUE;
+
+ /* Encryption and patch files are not supported. */
+ if (file_stat.m_is_encrypted)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_ENCRYPTION);
+
+ /* This function only supports stored and deflate. */
+ if ((file_stat.m_method != 0) && (file_stat.m_method != MZ_DEFLATED))
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_METHOD);
+
+ if (!file_stat.m_is_supported)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_FEATURE);
+
+ /* Read and parse the local directory entry. */
+ local_header_ofs = file_stat.m_local_header_ofs;
+ if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+
+ if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ local_header_filename_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS);
+ local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS);
+ local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS);
+ local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS);
+ local_header_crc32 = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_CRC32_OFS);
+ local_header_bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS);
+ has_data_descriptor = (local_header_bit_flags & 8) != 0;
+
+ if (local_header_filename_len != strlen(file_stat.m_filename))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if ((local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size) > pZip->m_archive_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if (!mz_zip_array_resize(pZip, &file_data_array, MZ_MAX(local_header_filename_len, local_header_extra_len), MZ_FALSE))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ goto handle_failure;
+ }
+
+ if (local_header_filename_len)
+ {
+ if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE, file_data_array.m_p, local_header_filename_len) != local_header_filename_len)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ goto handle_failure;
+ }
+
+ /* I've seen 1 archive that had the same pathname, but used backslashes in the local dir and forward slashes in the central dir. Do we care about this? For now, this case will fail validation. */
+ if (memcmp(file_stat.m_filename, file_data_array.m_p, local_header_filename_len) != 0)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED);
+ goto handle_failure;
+ }
+ }
+
+ if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX)))
+ {
+ mz_uint32 extra_size_remaining = local_header_extra_len;
+ const mz_uint8 *pExtra_data = (const mz_uint8 *)file_data_array.m_p;
+
+ if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len, file_data_array.m_p, local_header_extra_len) != local_header_extra_len)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ goto handle_failure;
+ }
+
+ do
+ {
+ mz_uint32 field_id, field_data_size, field_total_size;
+
+ if (extra_size_remaining < (sizeof(mz_uint16) * 2))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ goto handle_failure;
+ }
+
+ field_id = MZ_READ_LE16(pExtra_data);
+ field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16));
+ field_total_size = field_data_size + sizeof(mz_uint16) * 2;
+
+ if (field_total_size > extra_size_remaining)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ goto handle_failure;
+ }
+
+ if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID)
+ {
+ const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32);
+
+ if (field_data_size < sizeof(mz_uint64) * 2)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ goto handle_failure;
+ }
+
+ local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data);
+ local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64));
+
+ found_zip64_ext_data_in_ldir = MZ_TRUE;
+ break;
+ }
+
+ pExtra_data += field_total_size;
+ extra_size_remaining -= field_total_size;
+ } while (extra_size_remaining);
+ }
+
+ /* TODO: parse local header extra data when local_header_comp_size is 0xFFFFFFFF! (big_descriptor.zip) */
+ /* I've seen zips in the wild with the data descriptor bit set, but proper local header values and bogus data descriptors */
+ if ((has_data_descriptor) && (!local_header_comp_size) && (!local_header_crc32))
+ {
+ mz_uint8 descriptor_buf[32];
+ mz_bool has_id;
+ const mz_uint8 *pSrc;
+ mz_uint32 file_crc32;
+ mz_uint64 comp_size = 0, uncomp_size = 0;
+
+ mz_uint32 num_descriptor_uint32s = ((pState->m_zip64) || (found_zip64_ext_data_in_ldir)) ? 6 : 4;
+
+ if (pZip->m_pRead(pZip->m_pIO_opaque, local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_len + local_header_extra_len + file_stat.m_comp_size, descriptor_buf, sizeof(mz_uint32) * num_descriptor_uint32s) != (sizeof(mz_uint32) * num_descriptor_uint32s))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ goto handle_failure;
+ }
+
+ has_id = (MZ_READ_LE32(descriptor_buf) == MZ_ZIP_DATA_DESCRIPTOR_ID);
+ pSrc = has_id ? (descriptor_buf + sizeof(mz_uint32)) : descriptor_buf;
+
+ file_crc32 = MZ_READ_LE32(pSrc);
+
+ if ((pState->m_zip64) || (found_zip64_ext_data_in_ldir))
+ {
+ comp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32));
+ uncomp_size = MZ_READ_LE64(pSrc + sizeof(mz_uint32) + sizeof(mz_uint64));
+ }
+ else
+ {
+ comp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32));
+ uncomp_size = MZ_READ_LE32(pSrc + sizeof(mz_uint32) + sizeof(mz_uint32));
+ }
+
+ if ((file_crc32 != file_stat.m_crc32) || (comp_size != file_stat.m_comp_size) || (uncomp_size != file_stat.m_uncomp_size))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED);
+ goto handle_failure;
+ }
+ }
+ else
+ {
+ if ((local_header_crc32 != file_stat.m_crc32) || (local_header_comp_size != file_stat.m_comp_size) || (local_header_uncomp_size != file_stat.m_uncomp_size))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED);
+ goto handle_failure;
+ }
+ }
+
+ mz_zip_array_clear(pZip, &file_data_array);
+
+ if ((flags & MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY) == 0)
+ {
+ if (!mz_zip_reader_extract_to_callback(pZip, file_index, mz_zip_compute_crc32_callback, &uncomp_crc32, 0))
+ return MZ_FALSE;
+
+ /* 1 more check to be sure, although the extract checks too. */
+ if (uncomp_crc32 != file_stat.m_crc32)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED);
+ return MZ_FALSE;
+ }
+ }
+
+ return MZ_TRUE;
+
+ handle_failure:
+ mz_zip_array_clear(pZip, &file_data_array);
+ return MZ_FALSE;
+ }
+
+ mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags)
+ {
+ mz_zip_internal_state *pState;
+ mz_uint32 i;
+
+ if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || (!pZip->m_pRead))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pState = pZip->m_pState;
+
+ /* Basic sanity checks */
+ if (!pState->m_zip64)
+ {
+ if (pZip->m_total_files > MZ_UINT16_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+
+ if (pZip->m_archive_size > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+ }
+ else
+ {
+ if (pState->m_central_dir.m_size >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+ }
+
+ for (i = 0; i < pZip->m_total_files; i++)
+ {
+ if (MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG & flags)
+ {
+ mz_uint32 found_index;
+ mz_zip_archive_file_stat stat;
+
+ if (!mz_zip_reader_file_stat(pZip, i, &stat))
+ return MZ_FALSE;
+
+ if (!mz_zip_reader_locate_file_v2(pZip, stat.m_filename, NULL, 0, &found_index))
+ return MZ_FALSE;
+
+ /* This check can fail if there are duplicate filenames in the archive (which we don't check for when writing - that's up to the user) */
+ if (found_index != i)
+ return mz_zip_set_error(pZip, MZ_ZIP_VALIDATION_FAILED);
+ }
+
+ if (!mz_zip_validate_file(pZip, i, flags))
+ return MZ_FALSE;
+ }
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr)
+ {
+ mz_bool success = MZ_TRUE;
+ mz_zip_archive zip;
+ mz_zip_error actual_err = MZ_ZIP_NO_ERROR;
+
+ if ((!pMem) || (!size))
+ {
+ if (pErr)
+ *pErr = MZ_ZIP_INVALID_PARAMETER;
+ return MZ_FALSE;
+ }
+
+ mz_zip_zero_struct(&zip);
+
+ if (!mz_zip_reader_init_mem(&zip, pMem, size, flags))
+ {
+ if (pErr)
+ *pErr = zip.m_last_error;
+ return MZ_FALSE;
+ }
+
+ if (!mz_zip_validate_archive(&zip, flags))
+ {
+ actual_err = zip.m_last_error;
+ success = MZ_FALSE;
+ }
+
+ if (!mz_zip_reader_end_internal(&zip, success))
+ {
+ if (!actual_err)
+ actual_err = zip.m_last_error;
+ success = MZ_FALSE;
+ }
+
+ if (pErr)
+ *pErr = actual_err;
+
+ return success;
+ }
+
+#ifndef MINIZ_NO_STDIO
+ mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr)
+ {
+ mz_bool success = MZ_TRUE;
+ mz_zip_archive zip;
+ mz_zip_error actual_err = MZ_ZIP_NO_ERROR;
+
+ if (!pFilename)
+ {
+ if (pErr)
+ *pErr = MZ_ZIP_INVALID_PARAMETER;
+ return MZ_FALSE;
+ }
+
+ mz_zip_zero_struct(&zip);
+
+ if (!mz_zip_reader_init_file_v2(&zip, pFilename, flags, 0, 0))
+ {
+ if (pErr)
+ *pErr = zip.m_last_error;
+ return MZ_FALSE;
+ }
+
+ if (!mz_zip_validate_archive(&zip, flags))
+ {
+ actual_err = zip.m_last_error;
+ success = MZ_FALSE;
+ }
+
+ if (!mz_zip_reader_end_internal(&zip, success))
+ {
+ if (!actual_err)
+ actual_err = zip.m_last_error;
+ success = MZ_FALSE;
+ }
+
+ if (pErr)
+ *pErr = actual_err;
+
+ return success;
+ }
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ /* ------------------- .ZIP archive writing */
+
+#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS
+
+ static MZ_FORCEINLINE void mz_write_le16(mz_uint8 *p, mz_uint16 v)
+ {
+ p[0] = (mz_uint8)v;
+ p[1] = (mz_uint8)(v >> 8);
+ }
+ static MZ_FORCEINLINE void mz_write_le32(mz_uint8 *p, mz_uint32 v)
+ {
+ p[0] = (mz_uint8)v;
+ p[1] = (mz_uint8)(v >> 8);
+ p[2] = (mz_uint8)(v >> 16);
+ p[3] = (mz_uint8)(v >> 24);
+ }
+ static MZ_FORCEINLINE void mz_write_le64(mz_uint8 *p, mz_uint64 v)
+ {
+ mz_write_le32(p, (mz_uint32)v);
+ mz_write_le32(p + sizeof(mz_uint32), (mz_uint32)(v >> 32));
+ }
+
+#define MZ_WRITE_LE16(p, v) mz_write_le16((mz_uint8 *)(p), (mz_uint16)(v))
+#define MZ_WRITE_LE32(p, v) mz_write_le32((mz_uint8 *)(p), (mz_uint32)(v))
+#define MZ_WRITE_LE64(p, v) mz_write_le64((mz_uint8 *)(p), (mz_uint64)(v))
+
+ static size_t mz_zip_heap_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n)
+ {
+ mz_zip_archive *pZip = (mz_zip_archive *)pOpaque;
+ mz_zip_internal_state *pState = pZip->m_pState;
+ mz_uint64 new_size = MZ_MAX(file_ofs + n, pState->m_mem_size);
+
+ if (!n)
+ return 0;
+
+ /* An allocation this big is likely to just fail on 32-bit systems, so don't even go there. */
+ if ((sizeof(size_t) == sizeof(mz_uint32)) && (new_size > 0x7FFFFFFF))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE);
+ return 0;
+ }
+
+ if (new_size > pState->m_mem_capacity)
+ {
+ void *pNew_block;
+ size_t new_capacity = MZ_MAX(64, pState->m_mem_capacity);
+
+ while (new_capacity < new_size)
+ new_capacity *= 2;
+
+ if (NULL == (pNew_block = pZip->m_pRealloc(pZip->m_pAlloc_opaque, pState->m_pMem, 1, new_capacity)))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ return 0;
+ }
+
+ pState->m_pMem = pNew_block;
+ pState->m_mem_capacity = new_capacity;
+ }
+ memcpy((mz_uint8 *)pState->m_pMem + file_ofs, pBuf, n);
+ pState->m_mem_size = (size_t)new_size;
+ return n;
+ }
+
+ static mz_bool mz_zip_writer_end_internal(mz_zip_archive *pZip, mz_bool set_last_error)
+ {
+ mz_zip_internal_state *pState;
+ mz_bool status = MZ_TRUE;
+
+ if ((!pZip) || (!pZip->m_pState) || (!pZip->m_pAlloc) || (!pZip->m_pFree) || ((pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) && (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED)))
+ {
+ if (set_last_error)
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ return MZ_FALSE;
+ }
+
+ pState = pZip->m_pState;
+ pZip->m_pState = NULL;
+ mz_zip_array_clear(pZip, &pState->m_central_dir);
+ mz_zip_array_clear(pZip, &pState->m_central_dir_offsets);
+ mz_zip_array_clear(pZip, &pState->m_sorted_central_dir_offsets);
+
+#ifndef MINIZ_NO_STDIO
+ if (pState->m_pFile)
+ {
+ if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE)
+ {
+ if (MZ_FCLOSE(pState->m_pFile) == EOF)
+ {
+ if (set_last_error)
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED);
+ status = MZ_FALSE;
+ }
+ }
+
+ pState->m_pFile = NULL;
+ }
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ if ((pZip->m_pWrite == mz_zip_heap_write_func) && (pState->m_pMem))
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState->m_pMem);
+ pState->m_pMem = NULL;
+ }
+
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pState);
+ pZip->m_zip_mode = MZ_ZIP_MODE_INVALID;
+ return status;
+ }
+
+ mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags)
+ {
+ mz_bool zip64 = (flags & MZ_ZIP_FLAG_WRITE_ZIP64) != 0;
+
+ if ((!pZip) || (pZip->m_pState) || (!pZip->m_pWrite) || (pZip->m_zip_mode != MZ_ZIP_MODE_INVALID))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING)
+ {
+ if (!pZip->m_pRead)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ }
+
+ if (pZip->m_file_offset_alignment)
+ {
+ /* Ensure user specified file offset alignment is a power of 2. */
+ if (pZip->m_file_offset_alignment & (pZip->m_file_offset_alignment - 1))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ }
+
+ if (!pZip->m_pAlloc)
+ pZip->m_pAlloc = miniz_def_alloc_func;
+ if (!pZip->m_pFree)
+ pZip->m_pFree = miniz_def_free_func;
+ if (!pZip->m_pRealloc)
+ pZip->m_pRealloc = miniz_def_realloc_func;
+
+ pZip->m_archive_size = existing_size;
+ pZip->m_central_directory_file_ofs = 0;
+ pZip->m_total_files = 0;
+
+ if (NULL == (pZip->m_pState = (mz_zip_internal_state *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(mz_zip_internal_state))))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ memset(pZip->m_pState, 0, sizeof(mz_zip_internal_state));
+
+ MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir, sizeof(mz_uint8));
+ MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_central_dir_offsets, sizeof(mz_uint32));
+ MZ_ZIP_ARRAY_SET_ELEMENT_SIZE(&pZip->m_pState->m_sorted_central_dir_offsets, sizeof(mz_uint32));
+
+ pZip->m_pState->m_zip64 = zip64;
+ pZip->m_pState->m_zip64_has_extended_info_fields = zip64;
+
+ pZip->m_zip_type = MZ_ZIP_TYPE_USER;
+ pZip->m_zip_mode = MZ_ZIP_MODE_WRITING;
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size)
+ {
+ return mz_zip_writer_init_v2(pZip, existing_size, 0);
+ }
+
+ mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags)
+ {
+ pZip->m_pWrite = mz_zip_heap_write_func;
+ pZip->m_pNeeds_keepalive = NULL;
+
+ if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING)
+ pZip->m_pRead = mz_zip_mem_read_func;
+
+ pZip->m_pIO_opaque = pZip;
+
+ if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags))
+ return MZ_FALSE;
+
+ pZip->m_zip_type = MZ_ZIP_TYPE_HEAP;
+
+ if (0 != (initial_allocation_size = MZ_MAX(initial_allocation_size, size_to_reserve_at_beginning)))
+ {
+ if (NULL == (pZip->m_pState->m_pMem = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, initial_allocation_size)))
+ {
+ mz_zip_writer_end_internal(pZip, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+ pZip->m_pState->m_mem_capacity = initial_allocation_size;
+ }
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size)
+ {
+ return mz_zip_writer_init_heap_v2(pZip, size_to_reserve_at_beginning, initial_allocation_size, 0);
+ }
+
+#ifndef MINIZ_NO_STDIO
+ static size_t mz_zip_file_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n)
+ {
+ mz_zip_archive *pZip = (mz_zip_archive *)pOpaque;
+ mz_int64 cur_ofs = MZ_FTELL64(pZip->m_pState->m_pFile);
+
+ file_ofs += pZip->m_pState->m_file_archive_start_ofs;
+
+ if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pZip->m_pState->m_pFile, (mz_int64)file_ofs, SEEK_SET))))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_SEEK_FAILED);
+ return 0;
+ }
+
+ return MZ_FWRITE(pBuf, 1, n, pZip->m_pState->m_pFile);
+ }
+
+ mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning)
+ {
+ return mz_zip_writer_init_file_v2(pZip, pFilename, size_to_reserve_at_beginning, 0);
+ }
+
+ mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags)
+ {
+ MZ_FILE *pFile;
+
+ pZip->m_pWrite = mz_zip_file_write_func;
+ pZip->m_pNeeds_keepalive = NULL;
+
+ if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING)
+ pZip->m_pRead = mz_zip_file_read_func;
+
+ pZip->m_pIO_opaque = pZip;
+
+ if (!mz_zip_writer_init_v2(pZip, size_to_reserve_at_beginning, flags))
+ return MZ_FALSE;
+
+ if (NULL == (pFile = MZ_FOPEN(pFilename, (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING) ? "w+b" : "wb")))
+ {
+ mz_zip_writer_end(pZip);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED);
+ }
+
+ pZip->m_pState->m_pFile = pFile;
+ pZip->m_zip_type = MZ_ZIP_TYPE_FILE;
+
+ if (size_to_reserve_at_beginning)
+ {
+ mz_uint64 cur_ofs = 0;
+ char buf[4096];
+
+ MZ_CLEAR_ARR(buf);
+
+ do
+ {
+ size_t n = (size_t)MZ_MIN(sizeof(buf), size_to_reserve_at_beginning);
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_ofs, buf, n) != n)
+ {
+ mz_zip_writer_end(pZip);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+ cur_ofs += n;
+ size_to_reserve_at_beginning -= n;
+ } while (size_to_reserve_at_beginning);
+ }
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags)
+ {
+ pZip->m_pWrite = mz_zip_file_write_func;
+ pZip->m_pNeeds_keepalive = NULL;
+
+ if (flags & MZ_ZIP_FLAG_WRITE_ALLOW_READING)
+ pZip->m_pRead = mz_zip_file_read_func;
+
+ pZip->m_pIO_opaque = pZip;
+
+ if (!mz_zip_writer_init_v2(pZip, 0, flags))
+ return MZ_FALSE;
+
+ pZip->m_pState->m_pFile = pFile;
+ pZip->m_pState->m_file_archive_start_ofs = MZ_FTELL64(pZip->m_pState->m_pFile);
+ pZip->m_zip_type = MZ_ZIP_TYPE_CFILE;
+
+ return MZ_TRUE;
+ }
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags)
+ {
+ mz_zip_internal_state *pState;
+
+ if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_READING))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (flags & MZ_ZIP_FLAG_WRITE_ZIP64)
+ {
+ /* We don't support converting a non-zip64 file to zip64 - this seems like more trouble than it's worth. (What about the existing 32-bit data descriptors that could follow the compressed data?) */
+ if (!pZip->m_pState->m_zip64)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ }
+
+ /* No sense in trying to write to an archive that's already at the support max size */
+ if (pZip->m_pState->m_zip64)
+ {
+ if (pZip->m_total_files == MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+ }
+ else
+ {
+ if (pZip->m_total_files == MZ_UINT16_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+
+ if ((pZip->m_archive_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + MZ_ZIP_LOCAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE);
+ }
+
+ pState = pZip->m_pState;
+
+ if (pState->m_pFile)
+ {
+#ifdef MINIZ_NO_STDIO
+ (void)pFilename;
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+#else
+ if (pZip->m_pIO_opaque != pZip)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (pZip->m_zip_type == MZ_ZIP_TYPE_FILE &&
+ !(flags & MZ_ZIP_FLAG_READ_ALLOW_WRITING) )
+ {
+ if (!pFilename)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ /* Archive is being read from stdio and was originally opened only for reading. Try to reopen as writable. */
+ if (NULL == (pState->m_pFile = MZ_FREOPEN(pFilename, "r+b", pState->m_pFile)))
+ {
+ /* The mz_zip_archive is now in a bogus state because pState->m_pFile is NULL, so just close it. */
+ mz_zip_reader_end_internal(pZip, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED);
+ }
+ }
+
+ pZip->m_pWrite = mz_zip_file_write_func;
+ pZip->m_pNeeds_keepalive = NULL;
+#endif /* #ifdef MINIZ_NO_STDIO */
+ }
+ else if (pState->m_pMem)
+ {
+ /* Archive lives in a memory block. Assume it's from the heap that we can resize using the realloc callback. */
+ if (pZip->m_pIO_opaque != pZip)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pState->m_mem_capacity = pState->m_mem_size;
+ pZip->m_pWrite = mz_zip_heap_write_func;
+ pZip->m_pNeeds_keepalive = NULL;
+ }
+ /* Archive is being read via a user provided read function - make sure the user has specified a write function too. */
+ else if (!pZip->m_pWrite)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ /* Start writing new files at the archive's current central directory location. */
+ /* TODO: We could add a flag that lets the user start writing immediately AFTER the existing central dir - this would be safer. */
+ pZip->m_archive_size = pZip->m_central_directory_file_ofs;
+ pZip->m_central_directory_file_ofs = 0;
+
+ /* Clear the sorted central dir offsets, they aren't useful or maintained now. */
+ /* Even though we're now in write mode, files can still be extracted and verified, but file locates will be slow. */
+ /* TODO: We could easily maintain the sorted central directory offsets. */
+ mz_zip_array_clear(pZip, &pZip->m_pState->m_sorted_central_dir_offsets);
+
+ pZip->m_zip_mode = MZ_ZIP_MODE_WRITING;
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename)
+ {
+ return mz_zip_writer_init_from_reader_v2(pZip, pFilename, 0);
+ }
+
+ /* TODO: pArchive_name is a terrible name here! */
+ mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags)
+ {
+ return mz_zip_writer_add_mem_ex(pZip, pArchive_name, pBuf, buf_size, NULL, 0, level_and_flags, 0, 0);
+ }
+
+ typedef struct
+ {
+ mz_zip_archive *m_pZip;
+ mz_uint64 m_cur_archive_file_ofs;
+ mz_uint64 m_comp_size;
+ } mz_zip_writer_add_state;
+
+ static mz_bool mz_zip_writer_add_put_buf_callback(const void *pBuf, int len, void *pUser)
+ {
+ mz_zip_writer_add_state *pState = (mz_zip_writer_add_state *)pUser;
+ if ((int)pState->m_pZip->m_pWrite(pState->m_pZip->m_pIO_opaque, pState->m_cur_archive_file_ofs, pBuf, len) != len)
+ return MZ_FALSE;
+
+ pState->m_cur_archive_file_ofs += len;
+ pState->m_comp_size += len;
+ return MZ_TRUE;
+ }
+
+#define MZ_ZIP64_MAX_LOCAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 2)
+#define MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE (sizeof(mz_uint16) * 2 + sizeof(mz_uint64) * 3)
+ static mz_uint32 mz_zip_writer_create_zip64_extra_data(mz_uint8 *pBuf, mz_uint64 *pUncomp_size, mz_uint64 *pComp_size, mz_uint64 *pLocal_header_ofs)
+ {
+ mz_uint8 *pDst = pBuf;
+ mz_uint32 field_size = 0;
+
+ MZ_WRITE_LE16(pDst + 0, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID);
+ MZ_WRITE_LE16(pDst + 2, 0);
+ pDst += sizeof(mz_uint16) * 2;
+
+ if (pUncomp_size)
+ {
+ MZ_WRITE_LE64(pDst, *pUncomp_size);
+ pDst += sizeof(mz_uint64);
+ field_size += sizeof(mz_uint64);
+ }
+
+ if (pComp_size)
+ {
+ MZ_WRITE_LE64(pDst, *pComp_size);
+ pDst += sizeof(mz_uint64);
+ field_size += sizeof(mz_uint64);
+ }
+
+ if (pLocal_header_ofs)
+ {
+ MZ_WRITE_LE64(pDst, *pLocal_header_ofs);
+ pDst += sizeof(mz_uint64);
+ field_size += sizeof(mz_uint64);
+ }
+
+ MZ_WRITE_LE16(pBuf + 2, field_size);
+
+ return (mz_uint32)(pDst - pBuf);
+ }
+
+ static mz_bool mz_zip_writer_create_local_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst, mz_uint16 filename_size, mz_uint16 extra_size, mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32, mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date)
+ {
+ (void)pZip;
+ memset(pDst, 0, MZ_ZIP_LOCAL_DIR_HEADER_SIZE);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_SIG_OFS, MZ_ZIP_LOCAL_DIR_HEADER_SIG);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_VERSION_NEEDED_OFS, method ? 20 : 0);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_BIT_FLAG_OFS, bit_flags);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_METHOD_OFS, method);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_TIME_OFS, dos_time);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILE_DATE_OFS, dos_date);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_CRC32_OFS, uncomp_crc32);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX));
+ MZ_WRITE_LE32(pDst + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX));
+ MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_FILENAME_LEN_OFS, filename_size);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_LDH_EXTRA_LEN_OFS, extra_size);
+ return MZ_TRUE;
+ }
+
+ static mz_bool mz_zip_writer_create_central_dir_header(mz_zip_archive *pZip, mz_uint8 *pDst,
+ mz_uint16 filename_size, mz_uint16 extra_size, mz_uint16 comment_size,
+ mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32,
+ mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date,
+ mz_uint64 local_header_ofs, mz_uint32 ext_attributes)
+ {
+ (void)pZip;
+ memset(pDst, 0, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_SIG_OFS, MZ_ZIP_CENTRAL_DIR_HEADER_SIG);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_VERSION_NEEDED_OFS, method ? 20 : 0);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_BIT_FLAG_OFS, bit_flags);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_METHOD_OFS, method);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_TIME_OFS, dos_time);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILE_DATE_OFS, dos_date);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_CRC32_OFS, uncomp_crc32);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_MIN(comp_size, MZ_UINT32_MAX));
+ MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_MIN(uncomp_size, MZ_UINT32_MAX));
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_FILENAME_LEN_OFS, filename_size);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_EXTRA_LEN_OFS, extra_size);
+ MZ_WRITE_LE16(pDst + MZ_ZIP_CDH_COMMENT_LEN_OFS, comment_size);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_EXTERNAL_ATTR_OFS, ext_attributes);
+ MZ_WRITE_LE32(pDst + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_MIN(local_header_ofs, MZ_UINT32_MAX));
+ return MZ_TRUE;
+ }
+
+ static mz_bool mz_zip_writer_add_to_central_dir(mz_zip_archive *pZip, const char *pFilename, mz_uint16 filename_size,
+ const void *pExtra, mz_uint16 extra_size, const void *pComment, mz_uint16 comment_size,
+ mz_uint64 uncomp_size, mz_uint64 comp_size, mz_uint32 uncomp_crc32,
+ mz_uint16 method, mz_uint16 bit_flags, mz_uint16 dos_time, mz_uint16 dos_date,
+ mz_uint64 local_header_ofs, mz_uint32 ext_attributes,
+ const char *user_extra_data, mz_uint user_extra_data_len)
+ {
+ mz_zip_internal_state *pState = pZip->m_pState;
+ mz_uint32 central_dir_ofs = (mz_uint32)pState->m_central_dir.m_size;
+ size_t orig_central_dir_size = pState->m_central_dir.m_size;
+ mz_uint8 central_dir_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE];
+
+ if (!pZip->m_pState->m_zip64)
+ {
+ if (local_header_ofs > 0xFFFFFFFF)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_TOO_LARGE);
+ }
+
+ /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */
+ if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + filename_size + extra_size + user_extra_data_len + comment_size) >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE);
+
+ if (!mz_zip_writer_create_central_dir_header(pZip, central_dir_header, filename_size, (mz_uint16)(extra_size + user_extra_data_len), comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_header_ofs, ext_attributes))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ if ((!mz_zip_array_push_back(pZip, &pState->m_central_dir, central_dir_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE)) ||
+ (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pFilename, filename_size)) ||
+ (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pExtra, extra_size)) ||
+ (!mz_zip_array_push_back(pZip, &pState->m_central_dir, user_extra_data, user_extra_data_len)) ||
+ (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pComment, comment_size)) ||
+ (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, ¢ral_dir_ofs, 1)))
+ {
+ /* Try to resize the central directory array back into its original state. */
+ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ return MZ_TRUE;
+ }
+
+ static mz_bool mz_zip_writer_validate_archive_name(const char *pArchive_name)
+ {
+ /* Basic ZIP archive filename validity checks: Valid filenames cannot start with a forward slash, cannot contain a drive letter, and cannot use DOS-style backward slashes. */
+ if (*pArchive_name == '/')
+ return MZ_FALSE;
+
+ /* Making sure the name does not contain drive letters or DOS style backward slashes is the responsibility of the program using miniz*/
+
+ return MZ_TRUE;
+ }
+
+ static mz_uint mz_zip_writer_compute_padding_needed_for_file_alignment(mz_zip_archive *pZip)
+ {
+ mz_uint32 n;
+ if (!pZip->m_file_offset_alignment)
+ return 0;
+ n = (mz_uint32)(pZip->m_archive_size & (pZip->m_file_offset_alignment - 1));
+ return (mz_uint)((pZip->m_file_offset_alignment - n) & (pZip->m_file_offset_alignment - 1));
+ }
+
+ static mz_bool mz_zip_writer_write_zeros(mz_zip_archive *pZip, mz_uint64 cur_file_ofs, mz_uint32 n)
+ {
+ char buf[4096];
+ memset(buf, 0, MZ_MIN(sizeof(buf), n));
+ while (n)
+ {
+ mz_uint32 s = MZ_MIN(sizeof(buf), n);
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_file_ofs, buf, s) != s)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_file_ofs += s;
+ n -= s;
+ }
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags,
+ mz_uint64 uncomp_size, mz_uint32 uncomp_crc32)
+ {
+ return mz_zip_writer_add_mem_ex_v2(pZip, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, uncomp_size, uncomp_crc32, NULL, NULL, 0, NULL, 0);
+ }
+
+ mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size,
+ mz_uint level_and_flags, mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified,
+ const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len)
+ {
+ mz_uint16 method = 0, dos_time = 0, dos_date = 0;
+ mz_uint level, ext_attributes = 0, num_alignment_padding_bytes;
+ mz_uint64 local_dir_header_ofs = pZip->m_archive_size, cur_archive_file_ofs = pZip->m_archive_size, comp_size = 0;
+ size_t archive_name_size;
+ mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE];
+ tdefl_compressor *pComp = NULL;
+ mz_bool store_data_uncompressed;
+ mz_zip_internal_state *pState;
+ mz_uint8 *pExtra_data = NULL;
+ mz_uint32 extra_size = 0;
+ mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE];
+ mz_uint16 bit_flags = 0;
+
+ if ((int)level_and_flags < 0)
+ level_and_flags = MZ_DEFAULT_LEVEL;
+
+ if (uncomp_size || (buf_size && !(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)))
+ bit_flags |= MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR;
+
+ if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME))
+ bit_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8;
+
+ level = level_and_flags & 0xF;
+ store_data_uncompressed = ((!level) || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA));
+
+ if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || ((buf_size) && (!pBuf)) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pState = pZip->m_pState;
+
+ if (pState->m_zip64)
+ {
+ if (pZip->m_total_files == MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+ }
+ else
+ {
+ if (pZip->m_total_files == MZ_UINT16_MAX)
+ {
+ pState->m_zip64 = MZ_TRUE;
+ /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */
+ }
+ if (((mz_uint64)buf_size > 0xFFFFFFFF) || (uncomp_size > 0xFFFFFFFF))
+ {
+ pState->m_zip64 = MZ_TRUE;
+ /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */
+ }
+ }
+
+ if ((!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)) && (uncomp_size))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (!mz_zip_writer_validate_archive_name(pArchive_name))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME);
+
+#ifndef MINIZ_NO_TIME
+ if (last_modified != NULL)
+ {
+ mz_zip_time_t_to_dos_time(*last_modified, &dos_time, &dos_date);
+ }
+ else
+ {
+ MZ_TIME_T cur_time;
+ time(&cur_time);
+ mz_zip_time_t_to_dos_time(cur_time, &dos_time, &dos_date);
+ }
+#else
+ (void)last_modified;
+#endif /* #ifndef MINIZ_NO_TIME */
+
+ if (!(level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA))
+ {
+ uncomp_crc32 = (mz_uint32)mz_crc32(MZ_CRC32_INIT, (const mz_uint8 *)pBuf, buf_size);
+ uncomp_size = buf_size;
+ if (uncomp_size <= 3)
+ {
+ level = 0;
+ store_data_uncompressed = MZ_TRUE;
+ }
+ }
+
+ archive_name_size = strlen(pArchive_name);
+ if (archive_name_size > MZ_UINT16_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME);
+
+ num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip);
+
+ /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */
+ if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE);
+
+ if (!pState->m_zip64)
+ {
+ /* Bail early if the archive would obviously become too large */
+ if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + user_extra_data_len +
+ pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + user_extra_data_central_len + MZ_ZIP_DATA_DESCRIPTER_SIZE32) > 0xFFFFFFFF)
+ {
+ pState->m_zip64 = MZ_TRUE;
+ /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */
+ }
+ }
+
+ if ((archive_name_size) && (pArchive_name[archive_name_size - 1] == '/'))
+ {
+ /* Set DOS Subdirectory attribute bit. */
+ ext_attributes |= MZ_ZIP_DOS_DIR_ATTRIBUTE_BITFLAG;
+
+ /* Subdirectories cannot contain data. */
+ if ((buf_size) || (uncomp_size))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ }
+
+ /* Try to do any allocations before writing to the archive, so if an allocation fails the file remains unmodified. (A good idea if we're doing an in-place modification.) */
+ if ((!mz_zip_array_ensure_room(pZip, &pState->m_central_dir, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + (pState->m_zip64 ? MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE : 0))) || (!mz_zip_array_ensure_room(pZip, &pState->m_central_dir_offsets, 1)))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ if ((!store_data_uncompressed) && (buf_size))
+ {
+ if (NULL == (pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor))))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes))
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+ return MZ_FALSE;
+ }
+
+ local_dir_header_ofs += num_alignment_padding_bytes;
+ if (pZip->m_file_offset_alignment)
+ {
+ MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0);
+ }
+ cur_archive_file_ofs += num_alignment_padding_bytes;
+
+ MZ_CLEAR_ARR(local_dir_header);
+
+ if (!store_data_uncompressed || (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA))
+ {
+ method = MZ_DEFLATED;
+ }
+
+ if (pState->m_zip64)
+ {
+ if (uncomp_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX)
+ {
+ pExtra_data = extra_data;
+ extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL,
+ (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL);
+ }
+
+ if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, bit_flags, dos_time, dos_date))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += sizeof(local_dir_header);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+ cur_archive_file_ofs += archive_name_size;
+
+ if (pExtra_data != NULL)
+ {
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += extra_size;
+ }
+ }
+ else
+ {
+ if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX))
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+ if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, bit_flags, dos_time, dos_date))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, local_dir_header_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += sizeof(local_dir_header);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+ cur_archive_file_ofs += archive_name_size;
+ }
+
+ if (user_extra_data_len > 0)
+ {
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += user_extra_data_len;
+ }
+
+ if (store_data_uncompressed)
+ {
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pBuf, buf_size) != buf_size)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+
+ cur_archive_file_ofs += buf_size;
+ comp_size = buf_size;
+ }
+ else if (buf_size)
+ {
+ mz_zip_writer_add_state state;
+
+ state.m_pZip = pZip;
+ state.m_cur_archive_file_ofs = cur_archive_file_ofs;
+ state.m_comp_size = 0;
+
+ if ((tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY) ||
+ (tdefl_compress_buffer(pComp, pBuf, buf_size, TDEFL_FINISH) != TDEFL_STATUS_DONE))
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+ return mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED);
+ }
+
+ comp_size = state.m_comp_size;
+ cur_archive_file_ofs = state.m_cur_archive_file_ofs;
+ }
+
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+ pComp = NULL;
+
+ if (uncomp_size)
+ {
+ mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64];
+ mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32;
+
+ MZ_ASSERT(bit_flags & MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR);
+
+ MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID);
+ MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32);
+ if (pExtra_data == NULL)
+ {
+ if (comp_size > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+
+ MZ_WRITE_LE32(local_dir_footer + 8, comp_size);
+ MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size);
+ }
+ else
+ {
+ MZ_WRITE_LE64(local_dir_footer + 8, comp_size);
+ MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size);
+ local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64;
+ }
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size)
+ return MZ_FALSE;
+
+ cur_archive_file_ofs += local_dir_footer_size;
+ }
+
+ if (pExtra_data != NULL)
+ {
+ extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL,
+ (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL);
+ }
+
+ if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment,
+ comment_size, uncomp_size, comp_size, uncomp_crc32, method, bit_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes,
+ user_extra_data_central, user_extra_data_central_len))
+ return MZ_FALSE;
+
+ pZip->m_total_files++;
+ pZip->m_archive_size = cur_archive_file_ofs;
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void *callback_opaque, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags,
+ const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len)
+ {
+ mz_uint16 gen_flags;
+ mz_uint uncomp_crc32 = MZ_CRC32_INIT, level, num_alignment_padding_bytes;
+ mz_uint16 method = 0, dos_time = 0, dos_date = 0, ext_attributes = 0;
+ mz_uint64 local_dir_header_ofs, cur_archive_file_ofs = pZip->m_archive_size, uncomp_size = 0, comp_size = 0;
+ size_t archive_name_size;
+ mz_uint8 local_dir_header[MZ_ZIP_LOCAL_DIR_HEADER_SIZE];
+ mz_uint8 *pExtra_data = NULL;
+ mz_uint32 extra_size = 0;
+ mz_uint8 extra_data[MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE];
+ mz_zip_internal_state *pState;
+ mz_uint64 file_ofs = 0, cur_archive_header_file_ofs;
+
+ if ((int)level_and_flags < 0)
+ level_and_flags = MZ_DEFAULT_LEVEL;
+ level = level_and_flags & 0xF;
+
+ gen_flags = (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE) ? 0 : MZ_ZIP_LDH_BIT_FLAG_HAS_LOCATOR;
+
+ if (!(level_and_flags & MZ_ZIP_FLAG_ASCII_FILENAME))
+ gen_flags |= MZ_ZIP_GENERAL_PURPOSE_BIT_FLAG_UTF8;
+
+ /* Sanity checks */
+ if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pArchive_name) || ((comment_size) && (!pComment)) || (level > MZ_UBER_COMPRESSION))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pState = pZip->m_pState;
+
+ if ((!pState->m_zip64) && (max_size > MZ_UINT32_MAX))
+ {
+ /* Source file is too large for non-zip64 */
+ /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */
+ pState->m_zip64 = MZ_TRUE;
+ }
+
+ /* We could support this, but why? */
+ if (level_and_flags & MZ_ZIP_FLAG_COMPRESSED_DATA)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (!mz_zip_writer_validate_archive_name(pArchive_name))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME);
+
+ if (pState->m_zip64)
+ {
+ if (pZip->m_total_files == MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+ }
+ else
+ {
+ if (pZip->m_total_files == MZ_UINT16_MAX)
+ {
+ pState->m_zip64 = MZ_TRUE;
+ /*return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES); */
+ }
+ }
+
+ archive_name_size = strlen(pArchive_name);
+ if (archive_name_size > MZ_UINT16_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_FILENAME);
+
+ num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip);
+
+ /* miniz doesn't support central dirs >= MZ_UINT32_MAX bytes yet */
+ if (((mz_uint64)pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP64_MAX_CENTRAL_EXTRA_FIELD_SIZE + comment_size) >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE);
+
+ if (!pState->m_zip64)
+ {
+ /* Bail early if the archive would obviously become too large */
+ if ((pZip->m_archive_size + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + archive_name_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + archive_name_size + comment_size + user_extra_data_len + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 1024 + MZ_ZIP_DATA_DESCRIPTER_SIZE32 + user_extra_data_central_len) > 0xFFFFFFFF)
+ {
+ pState->m_zip64 = MZ_TRUE;
+ /*return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE); */
+ }
+ }
+
+#ifndef MINIZ_NO_TIME
+ if (pFile_time)
+ {
+ mz_zip_time_t_to_dos_time(*pFile_time, &dos_time, &dos_date);
+ }
+#else
+ (void)pFile_time;
+#endif
+
+ if (max_size <= 3)
+ level = 0;
+
+ if (!mz_zip_writer_write_zeros(pZip, cur_archive_file_ofs, num_alignment_padding_bytes))
+ {
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+
+ cur_archive_file_ofs += num_alignment_padding_bytes;
+ local_dir_header_ofs = cur_archive_file_ofs;
+
+ if (pZip->m_file_offset_alignment)
+ {
+ MZ_ASSERT((cur_archive_file_ofs & (pZip->m_file_offset_alignment - 1)) == 0);
+ }
+
+ if (max_size && level)
+ {
+ method = MZ_DEFLATED;
+ }
+
+ MZ_CLEAR_ARR(local_dir_header);
+ if (pState->m_zip64)
+ {
+ if (max_size >= MZ_UINT32_MAX || local_dir_header_ofs >= MZ_UINT32_MAX)
+ {
+ pExtra_data = extra_data;
+ if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE)
+ extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL,
+ (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL,
+ (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL);
+ else
+ extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, NULL,
+ NULL,
+ (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL);
+ }
+
+ if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len), 0, 0, 0, method, gen_flags, dos_time, dos_date))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += sizeof(local_dir_header);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size)
+ {
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+
+ cur_archive_file_ofs += archive_name_size;
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, extra_data, extra_size) != extra_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += extra_size;
+ }
+ else
+ {
+ if ((comp_size > MZ_UINT32_MAX) || (cur_archive_file_ofs > MZ_UINT32_MAX))
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+ if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header, (mz_uint16)archive_name_size, (mz_uint16)user_extra_data_len, 0, 0, 0, method, gen_flags, dos_time, dos_date))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += sizeof(local_dir_header);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pArchive_name, archive_name_size) != archive_name_size)
+ {
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+
+ cur_archive_file_ofs += archive_name_size;
+ }
+
+ if (user_extra_data_len > 0)
+ {
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, user_extra_data, user_extra_data_len) != user_extra_data_len)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_file_ofs += user_extra_data_len;
+ }
+
+ if (max_size)
+ {
+ void *pRead_buf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, MZ_ZIP_MAX_IO_BUF_SIZE);
+ if (!pRead_buf)
+ {
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if (!level)
+ {
+ while (1)
+ {
+ size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE);
+ if (n == 0)
+ break;
+
+ if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size))
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ }
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, pRead_buf, n) != n)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+ file_ofs += n;
+ uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n);
+ cur_archive_file_ofs += n;
+ }
+ uncomp_size = file_ofs;
+ comp_size = uncomp_size;
+ }
+ else
+ {
+ mz_bool result = MZ_FALSE;
+ mz_zip_writer_add_state state;
+ tdefl_compressor *pComp = (tdefl_compressor *)pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, sizeof(tdefl_compressor));
+ if (!pComp)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ state.m_pZip = pZip;
+ state.m_cur_archive_file_ofs = cur_archive_file_ofs;
+ state.m_comp_size = 0;
+
+ if (tdefl_init(pComp, mz_zip_writer_add_put_buf_callback, &state, tdefl_create_comp_flags_from_zip_params(level, -15, MZ_DEFAULT_STRATEGY)) != TDEFL_STATUS_OKAY)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+ }
+
+ for (;;)
+ {
+ tdefl_status status;
+ tdefl_flush flush = TDEFL_NO_FLUSH;
+
+ size_t n = read_callback(callback_opaque, file_ofs, pRead_buf, MZ_ZIP_MAX_IO_BUF_SIZE);
+ if ((n > MZ_ZIP_MAX_IO_BUF_SIZE) || (file_ofs + n > max_size))
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ break;
+ }
+
+ file_ofs += n;
+ uncomp_crc32 = (mz_uint32)mz_crc32(uncomp_crc32, (const mz_uint8 *)pRead_buf, n);
+
+ if (pZip->m_pNeeds_keepalive != NULL && pZip->m_pNeeds_keepalive(pZip->m_pIO_opaque))
+ flush = TDEFL_FULL_FLUSH;
+
+ if (n == 0)
+ flush = TDEFL_FINISH;
+
+ status = tdefl_compress_buffer(pComp, pRead_buf, n, flush);
+ if (status == TDEFL_STATUS_DONE)
+ {
+ result = MZ_TRUE;
+ break;
+ }
+ else if (status != TDEFL_STATUS_OKAY)
+ {
+ mz_zip_set_error(pZip, MZ_ZIP_COMPRESSION_FAILED);
+ break;
+ }
+ }
+
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pComp);
+
+ if (!result)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+ return MZ_FALSE;
+ }
+
+ uncomp_size = file_ofs;
+ comp_size = state.m_comp_size;
+ cur_archive_file_ofs = state.m_cur_archive_file_ofs;
+ }
+
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pRead_buf);
+ }
+
+ if (!(level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE))
+ {
+ mz_uint8 local_dir_footer[MZ_ZIP_DATA_DESCRIPTER_SIZE64];
+ mz_uint32 local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE32;
+
+ MZ_WRITE_LE32(local_dir_footer + 0, MZ_ZIP_DATA_DESCRIPTOR_ID);
+ MZ_WRITE_LE32(local_dir_footer + 4, uncomp_crc32);
+ if (pExtra_data == NULL)
+ {
+ if (comp_size > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+
+ MZ_WRITE_LE32(local_dir_footer + 8, comp_size);
+ MZ_WRITE_LE32(local_dir_footer + 12, uncomp_size);
+ }
+ else
+ {
+ MZ_WRITE_LE64(local_dir_footer + 8, comp_size);
+ MZ_WRITE_LE64(local_dir_footer + 16, uncomp_size);
+ local_dir_footer_size = MZ_ZIP_DATA_DESCRIPTER_SIZE64;
+ }
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_file_ofs, local_dir_footer, local_dir_footer_size) != local_dir_footer_size)
+ return MZ_FALSE;
+
+ cur_archive_file_ofs += local_dir_footer_size;
+ }
+
+ if (level_and_flags & MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE)
+ {
+ if (pExtra_data != NULL)
+ {
+ extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (max_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL,
+ (max_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL);
+ }
+
+ if (!mz_zip_writer_create_local_dir_header(pZip, local_dir_header,
+ (mz_uint16)archive_name_size, (mz_uint16)(extra_size + user_extra_data_len),
+ (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : uncomp_size,
+ (max_size >= MZ_UINT32_MAX) ? MZ_UINT32_MAX : comp_size,
+ uncomp_crc32, method, gen_flags, dos_time, dos_date))
+ return mz_zip_set_error(pZip, MZ_ZIP_INTERNAL_ERROR);
+
+ cur_archive_header_file_ofs = local_dir_header_ofs;
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, local_dir_header, sizeof(local_dir_header)) != sizeof(local_dir_header))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ if (pExtra_data != NULL)
+ {
+ cur_archive_header_file_ofs += sizeof(local_dir_header);
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, pArchive_name, archive_name_size) != archive_name_size)
+ {
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+
+ cur_archive_header_file_ofs += archive_name_size;
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_archive_header_file_ofs, extra_data, extra_size) != extra_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_archive_header_file_ofs += extra_size;
+ }
+ }
+
+ if (pExtra_data != NULL)
+ {
+ extra_size = mz_zip_writer_create_zip64_extra_data(extra_data, (uncomp_size >= MZ_UINT32_MAX) ? &uncomp_size : NULL,
+ (uncomp_size >= MZ_UINT32_MAX) ? &comp_size : NULL, (local_dir_header_ofs >= MZ_UINT32_MAX) ? &local_dir_header_ofs : NULL);
+ }
+
+ if (!mz_zip_writer_add_to_central_dir(pZip, pArchive_name, (mz_uint16)archive_name_size, pExtra_data, (mz_uint16)extra_size, pComment, comment_size,
+ uncomp_size, comp_size, uncomp_crc32, method, gen_flags, dos_time, dos_date, local_dir_header_ofs, ext_attributes,
+ user_extra_data_central, user_extra_data_central_len))
+ return MZ_FALSE;
+
+ pZip->m_total_files++;
+ pZip->m_archive_size = cur_archive_file_ofs;
+
+ return MZ_TRUE;
+ }
+
+#ifndef MINIZ_NO_STDIO
+
+ static size_t mz_file_read_func_stdio(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n)
+ {
+ MZ_FILE *pSrc_file = (MZ_FILE *)pOpaque;
+ mz_int64 cur_ofs = MZ_FTELL64(pSrc_file);
+
+ if (((mz_int64)file_ofs < 0) || (((cur_ofs != (mz_int64)file_ofs)) && (MZ_FSEEK64(pSrc_file, (mz_int64)file_ofs, SEEK_SET))))
+ return 0;
+
+ return MZ_FREAD(pBuf, 1, n, pSrc_file);
+ }
+
+ mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size, const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags,
+ const char *user_extra_data, mz_uint user_extra_data_len, const char *user_extra_data_central, mz_uint user_extra_data_central_len)
+ {
+ return mz_zip_writer_add_read_buf_callback(pZip, pArchive_name, mz_file_read_func_stdio, pSrc_file, max_size, pFile_time, pComment, comment_size, level_and_flags,
+ user_extra_data, user_extra_data_len, user_extra_data_central, user_extra_data_central_len);
+ }
+
+ mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags)
+ {
+ MZ_FILE *pSrc_file = NULL;
+ mz_uint64 uncomp_size = 0;
+ MZ_TIME_T file_modified_time;
+ MZ_TIME_T *pFile_time = NULL;
+ mz_bool status;
+
+ memset(&file_modified_time, 0, sizeof(file_modified_time));
+
+#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_STDIO)
+ pFile_time = &file_modified_time;
+ if (!mz_zip_get_file_modified_time(pSrc_filename, &file_modified_time))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_STAT_FAILED);
+#endif
+
+ pSrc_file = MZ_FOPEN(pSrc_filename, "rb");
+ if (!pSrc_file)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_OPEN_FAILED);
+
+ MZ_FSEEK64(pSrc_file, 0, SEEK_END);
+ uncomp_size = MZ_FTELL64(pSrc_file);
+ MZ_FSEEK64(pSrc_file, 0, SEEK_SET);
+
+ status = mz_zip_writer_add_cfile(pZip, pArchive_name, pSrc_file, uncomp_size, pFile_time, pComment, comment_size, level_and_flags, NULL, 0, NULL, 0);
+
+ MZ_FCLOSE(pSrc_file);
+
+ return status;
+ }
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ static mz_bool mz_zip_writer_update_zip64_extension_block(mz_zip_array *pNew_ext, mz_zip_archive *pZip, const mz_uint8 *pExt, mz_uint32 ext_len, mz_uint64 *pComp_size, mz_uint64 *pUncomp_size, mz_uint64 *pLocal_header_ofs, mz_uint32 *pDisk_start)
+ {
+ /* + 64 should be enough for any new zip64 data */
+ if (!mz_zip_array_reserve(pZip, pNew_ext, ext_len + 64, MZ_FALSE))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ mz_zip_array_resize(pZip, pNew_ext, 0, MZ_FALSE);
+
+ if ((pUncomp_size) || (pComp_size) || (pLocal_header_ofs) || (pDisk_start))
+ {
+ mz_uint8 new_ext_block[64];
+ mz_uint8 *pDst = new_ext_block;
+ mz_write_le16(pDst, MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID);
+ mz_write_le16(pDst + sizeof(mz_uint16), 0);
+ pDst += sizeof(mz_uint16) * 2;
+
+ if (pUncomp_size)
+ {
+ mz_write_le64(pDst, *pUncomp_size);
+ pDst += sizeof(mz_uint64);
+ }
+
+ if (pComp_size)
+ {
+ mz_write_le64(pDst, *pComp_size);
+ pDst += sizeof(mz_uint64);
+ }
+
+ if (pLocal_header_ofs)
+ {
+ mz_write_le64(pDst, *pLocal_header_ofs);
+ pDst += sizeof(mz_uint64);
+ }
+
+ if (pDisk_start)
+ {
+ mz_write_le32(pDst, *pDisk_start);
+ pDst += sizeof(mz_uint32);
+ }
+
+ mz_write_le16(new_ext_block + sizeof(mz_uint16), (mz_uint16)((pDst - new_ext_block) - sizeof(mz_uint16) * 2));
+
+ if (!mz_zip_array_push_back(pZip, pNew_ext, new_ext_block, pDst - new_ext_block))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if ((pExt) && (ext_len))
+ {
+ mz_uint32 extra_size_remaining = ext_len;
+ const mz_uint8 *pExtra_data = pExt;
+
+ do
+ {
+ mz_uint32 field_id, field_data_size, field_total_size;
+
+ if (extra_size_remaining < (sizeof(mz_uint16) * 2))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ field_id = MZ_READ_LE16(pExtra_data);
+ field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16));
+ field_total_size = field_data_size + sizeof(mz_uint16) * 2;
+
+ if (field_total_size > extra_size_remaining)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ if (field_id != MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID)
+ {
+ if (!mz_zip_array_push_back(pZip, pNew_ext, pExtra_data, field_total_size))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ pExtra_data += field_total_size;
+ extra_size_remaining -= field_total_size;
+ } while (extra_size_remaining);
+ }
+
+ return MZ_TRUE;
+ }
+
+ /* TODO: This func is now pretty freakin complex due to zip64, split it up? */
+ mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index)
+ {
+ mz_uint n, bit_flags, num_alignment_padding_bytes, src_central_dir_following_data_size;
+ mz_uint64 src_archive_bytes_remaining, local_dir_header_ofs;
+ mz_uint64 cur_src_file_ofs, cur_dst_file_ofs;
+ mz_uint32 local_header_u32[(MZ_ZIP_LOCAL_DIR_HEADER_SIZE + sizeof(mz_uint32) - 1) / sizeof(mz_uint32)];
+ mz_uint8 *pLocal_header = (mz_uint8 *)local_header_u32;
+ mz_uint8 new_central_header[MZ_ZIP_CENTRAL_DIR_HEADER_SIZE];
+ size_t orig_central_dir_size;
+ mz_zip_internal_state *pState;
+ void *pBuf;
+ const mz_uint8 *pSrc_central_header;
+ mz_zip_archive_file_stat src_file_stat;
+ mz_uint32 src_filename_len, src_comment_len, src_ext_len;
+ mz_uint32 local_header_filename_size, local_header_extra_len;
+ mz_uint64 local_header_comp_size, local_header_uncomp_size;
+ mz_bool found_zip64_ext_data_in_ldir = MZ_FALSE;
+
+ /* Sanity checks */
+ if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING) || (!pSource_zip->m_pRead))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pState = pZip->m_pState;
+
+ /* Don't support copying files from zip64 archives to non-zip64, even though in some cases this is possible */
+ if ((pSource_zip->m_pState->m_zip64) && (!pZip->m_pState->m_zip64))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ /* Get pointer to the source central dir header and crack it */
+ if (NULL == (pSrc_central_header = mz_zip_get_cdh(pSource_zip, src_file_index)))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (MZ_READ_LE32(pSrc_central_header + MZ_ZIP_CDH_SIG_OFS) != MZ_ZIP_CENTRAL_DIR_HEADER_SIG)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ src_filename_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ src_comment_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_COMMENT_LEN_OFS);
+ src_ext_len = MZ_READ_LE16(pSrc_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS);
+ src_central_dir_following_data_size = src_filename_len + src_ext_len + src_comment_len;
+
+ /* TODO: We don't support central dir's >= MZ_UINT32_MAX bytes right now (+32 fudge factor in case we need to add more extra data) */
+ if ((pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + 32) >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE);
+
+ num_alignment_padding_bytes = mz_zip_writer_compute_padding_needed_for_file_alignment(pZip);
+
+ if (!pState->m_zip64)
+ {
+ if (pZip->m_total_files == MZ_UINT16_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+ }
+ else
+ {
+ /* TODO: Our zip64 support still has some 32-bit limits that may not be worth fixing. */
+ if (pZip->m_total_files == MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+ }
+
+ if (!mz_zip_file_stat_internal(pSource_zip, src_file_index, pSrc_central_header, &src_file_stat, NULL))
+ return MZ_FALSE;
+
+ cur_src_file_ofs = src_file_stat.m_local_header_ofs;
+ cur_dst_file_ofs = pZip->m_archive_size;
+
+ /* Read the source archive's local dir header */
+ if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+
+ if (MZ_READ_LE32(pLocal_header) != MZ_ZIP_LOCAL_DIR_HEADER_SIG)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+
+ cur_src_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE;
+
+ /* Compute the total size we need to copy (filename+extra data+compressed data) */
+ local_header_filename_size = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_FILENAME_LEN_OFS);
+ local_header_extra_len = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_EXTRA_LEN_OFS);
+ local_header_comp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_COMPRESSED_SIZE_OFS);
+ local_header_uncomp_size = MZ_READ_LE32(pLocal_header + MZ_ZIP_LDH_DECOMPRESSED_SIZE_OFS);
+ src_archive_bytes_remaining = src_file_stat.m_comp_size + local_header_filename_size + local_header_extra_len;
+
+ /* Try to find a zip64 extended information field */
+ if ((local_header_extra_len) && ((local_header_comp_size == MZ_UINT32_MAX) || (local_header_uncomp_size == MZ_UINT32_MAX)))
+ {
+ mz_zip_array file_data_array;
+ const mz_uint8 *pExtra_data;
+ mz_uint32 extra_size_remaining = local_header_extra_len;
+
+ mz_zip_array_init(&file_data_array, 1);
+ if (!mz_zip_array_resize(pZip, &file_data_array, local_header_extra_len, MZ_FALSE))
+ {
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, src_file_stat.m_local_header_ofs + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + local_header_filename_size, file_data_array.m_p, local_header_extra_len) != local_header_extra_len)
+ {
+ mz_zip_array_clear(pZip, &file_data_array);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ }
+
+ pExtra_data = (const mz_uint8 *)file_data_array.m_p;
+
+ do
+ {
+ mz_uint32 field_id, field_data_size, field_total_size;
+
+ if (extra_size_remaining < (sizeof(mz_uint16) * 2))
+ {
+ mz_zip_array_clear(pZip, &file_data_array);
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ }
+
+ field_id = MZ_READ_LE16(pExtra_data);
+ field_data_size = MZ_READ_LE16(pExtra_data + sizeof(mz_uint16));
+ field_total_size = field_data_size + sizeof(mz_uint16) * 2;
+
+ if (field_total_size > extra_size_remaining)
+ {
+ mz_zip_array_clear(pZip, &file_data_array);
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ }
+
+ if (field_id == MZ_ZIP64_EXTENDED_INFORMATION_FIELD_HEADER_ID)
+ {
+ const mz_uint8 *pSrc_field_data = pExtra_data + sizeof(mz_uint32);
+
+ if (field_data_size < sizeof(mz_uint64) * 2)
+ {
+ mz_zip_array_clear(pZip, &file_data_array);
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_HEADER_OR_CORRUPTED);
+ }
+
+ local_header_uncomp_size = MZ_READ_LE64(pSrc_field_data);
+ local_header_comp_size = MZ_READ_LE64(pSrc_field_data + sizeof(mz_uint64)); /* may be 0 if there's a descriptor */
+
+ found_zip64_ext_data_in_ldir = MZ_TRUE;
+ break;
+ }
+
+ pExtra_data += field_total_size;
+ extra_size_remaining -= field_total_size;
+ } while (extra_size_remaining);
+
+ mz_zip_array_clear(pZip, &file_data_array);
+ }
+
+ if (!pState->m_zip64)
+ {
+ /* Try to detect if the new archive will most likely wind up too big and bail early (+(sizeof(mz_uint32) * 4) is for the optional descriptor which could be present, +64 is a fudge factor). */
+ /* We also check when the archive is finalized so this doesn't need to be perfect. */
+ mz_uint64 approx_new_archive_size = cur_dst_file_ofs + num_alignment_padding_bytes + MZ_ZIP_LOCAL_DIR_HEADER_SIZE + src_archive_bytes_remaining + (sizeof(mz_uint32) * 4) +
+ pState->m_central_dir.m_size + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_central_dir_following_data_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE + 64;
+
+ if (approx_new_archive_size >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+ }
+
+ /* Write dest archive padding */
+ if (!mz_zip_writer_write_zeros(pZip, cur_dst_file_ofs, num_alignment_padding_bytes))
+ return MZ_FALSE;
+
+ cur_dst_file_ofs += num_alignment_padding_bytes;
+
+ local_dir_header_ofs = cur_dst_file_ofs;
+ if (pZip->m_file_offset_alignment)
+ {
+ MZ_ASSERT((local_dir_header_ofs & (pZip->m_file_offset_alignment - 1)) == 0);
+ }
+
+ /* The original zip's local header+ext block doesn't change, even with zip64, so we can just copy it over to the dest zip */
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pLocal_header, MZ_ZIP_LOCAL_DIR_HEADER_SIZE) != MZ_ZIP_LOCAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ cur_dst_file_ofs += MZ_ZIP_LOCAL_DIR_HEADER_SIZE;
+
+ /* Copy over the source archive bytes to the dest archive, also ensure we have enough buf space to handle optional data descriptor */
+ if (NULL == (pBuf = pZip->m_pAlloc(pZip->m_pAlloc_opaque, 1, (size_t)MZ_MAX(32U, MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining)))))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ while (src_archive_bytes_remaining)
+ {
+ n = (mz_uint)MZ_MIN((mz_uint64)MZ_ZIP_MAX_IO_BUF_SIZE, src_archive_bytes_remaining);
+ if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, n) != n)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ }
+ cur_src_file_ofs += n;
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+ cur_dst_file_ofs += n;
+
+ src_archive_bytes_remaining -= n;
+ }
+
+ /* Now deal with the optional data descriptor */
+ bit_flags = MZ_READ_LE16(pLocal_header + MZ_ZIP_LDH_BIT_FLAG_OFS);
+ if (bit_flags & 8)
+ {
+ /* Copy data descriptor */
+ if ((pSource_zip->m_pState->m_zip64) || (found_zip64_ext_data_in_ldir))
+ {
+ /* src is zip64, dest must be zip64 */
+
+ /* name uint32_t's */
+ /* id 1 (optional in zip64?) */
+ /* crc 1 */
+ /* comp_size 2 */
+ /* uncomp_size 2 */
+ if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, (sizeof(mz_uint32) * 6)) != (sizeof(mz_uint32) * 6))
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ }
+
+ n = sizeof(mz_uint32) * ((MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID) ? 6 : 5);
+ }
+ else
+ {
+ /* src is NOT zip64 */
+ mz_bool has_id;
+
+ if (pSource_zip->m_pRead(pSource_zip->m_pIO_opaque, cur_src_file_ofs, pBuf, sizeof(mz_uint32) * 4) != sizeof(mz_uint32) * 4)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_READ_FAILED);
+ }
+
+ has_id = (MZ_READ_LE32(pBuf) == MZ_ZIP_DATA_DESCRIPTOR_ID);
+
+ if (pZip->m_pState->m_zip64)
+ {
+ /* dest is zip64, so upgrade the data descriptor */
+ const mz_uint8 *pSrc_descriptor = (const mz_uint8 *)pBuf + (has_id ? sizeof(mz_uint32) : 0);
+ const mz_uint32 src_crc32 = MZ_READ_LE32(pSrc_descriptor);
+ const mz_uint64 src_comp_size = MZ_READ_LE32(pSrc_descriptor + sizeof(mz_uint32));
+ const mz_uint64 src_uncomp_size = MZ_READ_LE32(pSrc_descriptor + 2 * sizeof(mz_uint32));
+
+ mz_write_le32((mz_uint8 *)pBuf, MZ_ZIP_DATA_DESCRIPTOR_ID);
+ mz_write_le32((mz_uint8 *)pBuf + sizeof(mz_uint32) * 1, src_crc32);
+ mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 2, src_comp_size);
+ mz_write_le64((mz_uint8 *)pBuf + sizeof(mz_uint32) * 4, src_uncomp_size);
+
+ n = sizeof(mz_uint32) * 6;
+ }
+ else
+ {
+ /* dest is NOT zip64, just copy it as-is */
+ n = sizeof(mz_uint32) * (has_id ? 4 : 3);
+ }
+ }
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, cur_dst_file_ofs, pBuf, n) != n)
+ {
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf);
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+ }
+
+ cur_src_file_ofs += n;
+ cur_dst_file_ofs += n;
+ }
+ pZip->m_pFree(pZip->m_pAlloc_opaque, pBuf);
+
+ /* Finally, add the new central dir header */
+ orig_central_dir_size = pState->m_central_dir.m_size;
+
+ memcpy(new_central_header, pSrc_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE);
+
+ if (pState->m_zip64)
+ {
+ /* This is the painful part: We need to write a new central dir header + ext block with updated zip64 fields, and ensure the old fields (if any) are not included. */
+ const mz_uint8 *pSrc_ext = pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len;
+ mz_zip_array new_ext_block;
+
+ mz_zip_array_init(&new_ext_block, sizeof(mz_uint8));
+
+ MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_COMPRESSED_SIZE_OFS, MZ_UINT32_MAX);
+ MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_DECOMPRESSED_SIZE_OFS, MZ_UINT32_MAX);
+ MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, MZ_UINT32_MAX);
+
+ if (!mz_zip_writer_update_zip64_extension_block(&new_ext_block, pZip, pSrc_ext, src_ext_len, &src_file_stat.m_comp_size, &src_file_stat.m_uncomp_size, &local_dir_header_ofs, NULL))
+ {
+ mz_zip_array_clear(pZip, &new_ext_block);
+ return MZ_FALSE;
+ }
+
+ MZ_WRITE_LE16(new_central_header + MZ_ZIP_CDH_EXTRA_LEN_OFS, new_ext_block.m_size);
+
+ if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE))
+ {
+ mz_zip_array_clear(pZip, &new_ext_block);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_filename_len))
+ {
+ mz_zip_array_clear(pZip, &new_ext_block);
+ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_ext_block.m_p, new_ext_block.m_size))
+ {
+ mz_zip_array_clear(pZip, &new_ext_block);
+ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE + src_filename_len + src_ext_len, src_comment_len))
+ {
+ mz_zip_array_clear(pZip, &new_ext_block);
+ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ mz_zip_array_clear(pZip, &new_ext_block);
+ }
+ else
+ {
+ /* sanity checks */
+ if (cur_dst_file_ofs > MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+
+ if (local_dir_header_ofs >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_ARCHIVE_TOO_LARGE);
+
+ MZ_WRITE_LE32(new_central_header + MZ_ZIP_CDH_LOCAL_HEADER_OFS, local_dir_header_ofs);
+
+ if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, new_central_header, MZ_ZIP_CENTRAL_DIR_HEADER_SIZE))
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+
+ if (!mz_zip_array_push_back(pZip, &pState->m_central_dir, pSrc_central_header + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, src_central_dir_following_data_size))
+ {
+ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+ }
+
+ /* This shouldn't trigger unless we screwed up during the initial sanity checks */
+ if (pState->m_central_dir.m_size >= MZ_UINT32_MAX)
+ {
+ /* TODO: Support central dirs >= 32-bits in size */
+ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_UNSUPPORTED_CDIR_SIZE);
+ }
+
+ n = (mz_uint32)orig_central_dir_size;
+ if (!mz_zip_array_push_back(pZip, &pState->m_central_dir_offsets, &n, 1))
+ {
+ mz_zip_array_resize(pZip, &pState->m_central_dir, orig_central_dir_size, MZ_FALSE);
+ return mz_zip_set_error(pZip, MZ_ZIP_ALLOC_FAILED);
+ }
+
+ pZip->m_total_files++;
+ pZip->m_archive_size = cur_dst_file_ofs;
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip)
+ {
+ mz_zip_internal_state *pState;
+ mz_uint64 central_dir_ofs, central_dir_size;
+ mz_uint8 hdr[256];
+
+ if ((!pZip) || (!pZip->m_pState) || (pZip->m_zip_mode != MZ_ZIP_MODE_WRITING))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ pState = pZip->m_pState;
+
+ if (pState->m_zip64)
+ {
+ if ((mz_uint64)pState->m_central_dir.m_size >= MZ_UINT32_MAX)
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+ }
+ else
+ {
+ if ((pZip->m_total_files > MZ_UINT16_MAX) || ((pZip->m_archive_size + pState->m_central_dir.m_size + MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) > MZ_UINT32_MAX))
+ return mz_zip_set_error(pZip, MZ_ZIP_TOO_MANY_FILES);
+ }
+
+ central_dir_ofs = 0;
+ central_dir_size = 0;
+ if (pZip->m_total_files)
+ {
+ /* Write central directory */
+ central_dir_ofs = pZip->m_archive_size;
+ central_dir_size = pState->m_central_dir.m_size;
+ pZip->m_central_directory_file_ofs = central_dir_ofs;
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, central_dir_ofs, pState->m_central_dir.m_p, (size_t)central_dir_size) != central_dir_size)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ pZip->m_archive_size += central_dir_size;
+ }
+
+ if (pState->m_zip64)
+ {
+ /* Write zip64 end of central directory header */
+ mz_uint64 rel_ofs_to_zip64_ecdr = pZip->m_archive_size;
+
+ MZ_CLEAR_ARR(hdr);
+ MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDH_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIG);
+ MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_SIZE_OF_RECORD_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE - sizeof(mz_uint32) - sizeof(mz_uint64));
+ MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_MADE_BY_OFS, 0x031E); /* TODO: always Unix */
+ MZ_WRITE_LE16(hdr + MZ_ZIP64_ECDH_VERSION_NEEDED_OFS, 0x002D);
+ MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, pZip->m_total_files);
+ MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_TOTAL_ENTRIES_OFS, pZip->m_total_files);
+ MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_SIZE_OFS, central_dir_size);
+ MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDH_CDIR_OFS_OFS, central_dir_ofs);
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_HEADER_SIZE;
+
+ /* Write zip64 end of central directory locator */
+ MZ_CLEAR_ARR(hdr);
+ MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_SIG_OFS, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG);
+ MZ_WRITE_LE64(hdr + MZ_ZIP64_ECDL_REL_OFS_TO_ZIP64_ECDR_OFS, rel_ofs_to_zip64_ecdr);
+ MZ_WRITE_LE32(hdr + MZ_ZIP64_ECDL_TOTAL_NUMBER_OF_DISKS_OFS, 1);
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE) != MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+ pZip->m_archive_size += MZ_ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIZE;
+ }
+
+ /* Write end of central directory record */
+ MZ_CLEAR_ARR(hdr);
+ MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_SIG_OFS, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIG);
+ MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_NUM_ENTRIES_ON_DISK_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files));
+ MZ_WRITE_LE16(hdr + MZ_ZIP_ECDH_CDIR_TOTAL_ENTRIES_OFS, MZ_MIN(MZ_UINT16_MAX, pZip->m_total_files));
+ MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_SIZE_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_size));
+ MZ_WRITE_LE32(hdr + MZ_ZIP_ECDH_CDIR_OFS_OFS, MZ_MIN(MZ_UINT32_MAX, central_dir_ofs));
+
+ if (pZip->m_pWrite(pZip->m_pIO_opaque, pZip->m_archive_size, hdr, MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE) != MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE)
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_WRITE_FAILED);
+
+#ifndef MINIZ_NO_STDIO
+ if ((pState->m_pFile) && (MZ_FFLUSH(pState->m_pFile) == EOF))
+ return mz_zip_set_error(pZip, MZ_ZIP_FILE_CLOSE_FAILED);
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+ pZip->m_archive_size += MZ_ZIP_END_OF_CENTRAL_DIR_HEADER_SIZE;
+
+ pZip->m_zip_mode = MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED;
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize)
+ {
+ if ((!ppBuf) || (!pSize))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ *ppBuf = NULL;
+ *pSize = 0;
+
+ if ((!pZip) || (!pZip->m_pState))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (pZip->m_pWrite != mz_zip_heap_write_func)
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ if (!mz_zip_writer_finalize_archive(pZip))
+ return MZ_FALSE;
+
+ *ppBuf = pZip->m_pState->m_pMem;
+ *pSize = pZip->m_pState->m_mem_size;
+ pZip->m_pState->m_pMem = NULL;
+ pZip->m_pState->m_mem_size = pZip->m_pState->m_mem_capacity = 0;
+
+ return MZ_TRUE;
+ }
+
+ mz_bool mz_zip_writer_end(mz_zip_archive *pZip)
+ {
+ return mz_zip_writer_end_internal(pZip, MZ_TRUE);
+ }
+
+#ifndef MINIZ_NO_STDIO
+ mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags)
+ {
+ return mz_zip_add_mem_to_archive_file_in_place_v2(pZip_filename, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, NULL);
+ }
+
+ mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr)
+ {
+ mz_bool status, created_new_archive = MZ_FALSE;
+ mz_zip_archive zip_archive;
+ struct MZ_FILE_STAT_STRUCT file_stat;
+ mz_zip_error actual_err = MZ_ZIP_NO_ERROR;
+
+ mz_zip_zero_struct(&zip_archive);
+ if ((int)level_and_flags < 0)
+ level_and_flags = MZ_DEFAULT_LEVEL;
+
+ if ((!pZip_filename) || (!pArchive_name) || ((buf_size) && (!pBuf)) || ((comment_size) && (!pComment)) || ((level_and_flags & 0xF) > MZ_UBER_COMPRESSION))
+ {
+ if (pErr)
+ *pErr = MZ_ZIP_INVALID_PARAMETER;
+ return MZ_FALSE;
+ }
+
+ if (!mz_zip_writer_validate_archive_name(pArchive_name))
+ {
+ if (pErr)
+ *pErr = MZ_ZIP_INVALID_FILENAME;
+ return MZ_FALSE;
+ }
+
+ /* Important: The regular non-64 bit version of stat() can fail here if the file is very large, which could cause the archive to be overwritten. */
+ /* So be sure to compile with _LARGEFILE64_SOURCE 1 */
+ if (MZ_FILE_STAT(pZip_filename, &file_stat) != 0)
+ {
+ /* Create a new archive. */
+ if (!mz_zip_writer_init_file_v2(&zip_archive, pZip_filename, 0, level_and_flags))
+ {
+ if (pErr)
+ *pErr = zip_archive.m_last_error;
+ return MZ_FALSE;
+ }
+
+ created_new_archive = MZ_TRUE;
+ }
+ else
+ {
+ /* Append to an existing archive. */
+ if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, level_and_flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY | MZ_ZIP_FLAG_READ_ALLOW_WRITING, 0, 0))
+ {
+ if (pErr)
+ *pErr = zip_archive.m_last_error;
+ return MZ_FALSE;
+ }
+
+ if (!mz_zip_writer_init_from_reader_v2(&zip_archive, pZip_filename, level_and_flags | MZ_ZIP_FLAG_READ_ALLOW_WRITING))
+ {
+ if (pErr)
+ *pErr = zip_archive.m_last_error;
+
+ mz_zip_reader_end_internal(&zip_archive, MZ_FALSE);
+
+ return MZ_FALSE;
+ }
+ }
+
+ status = mz_zip_writer_add_mem_ex(&zip_archive, pArchive_name, pBuf, buf_size, pComment, comment_size, level_and_flags, 0, 0);
+ actual_err = zip_archive.m_last_error;
+
+ /* Always finalize, even if adding failed for some reason, so we have a valid central directory. (This may not always succeed, but we can try.) */
+ if (!mz_zip_writer_finalize_archive(&zip_archive))
+ {
+ if (!actual_err)
+ actual_err = zip_archive.m_last_error;
+
+ status = MZ_FALSE;
+ }
+
+ if (!mz_zip_writer_end_internal(&zip_archive, status))
+ {
+ if (!actual_err)
+ actual_err = zip_archive.m_last_error;
+
+ status = MZ_FALSE;
+ }
+
+ if ((!status) && (created_new_archive))
+ {
+ /* It's a new archive and something went wrong, so just delete it. */
+ int ignoredStatus = MZ_DELETE_FILE(pZip_filename);
+ (void)ignoredStatus;
+ }
+
+ if (pErr)
+ *pErr = actual_err;
+
+ return status;
+ }
+
+ void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr)
+ {
+ mz_uint32 file_index;
+ mz_zip_archive zip_archive;
+ void *p = NULL;
+
+ if (pSize)
+ *pSize = 0;
+
+ if ((!pZip_filename) || (!pArchive_name))
+ {
+ if (pErr)
+ *pErr = MZ_ZIP_INVALID_PARAMETER;
+
+ return NULL;
+ }
+
+ mz_zip_zero_struct(&zip_archive);
+ if (!mz_zip_reader_init_file_v2(&zip_archive, pZip_filename, flags | MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY, 0, 0))
+ {
+ if (pErr)
+ *pErr = zip_archive.m_last_error;
+
+ return NULL;
+ }
+
+ if (mz_zip_reader_locate_file_v2(&zip_archive, pArchive_name, pComment, flags, &file_index))
+ {
+ p = mz_zip_reader_extract_to_heap(&zip_archive, file_index, pSize, flags);
+ }
+
+ mz_zip_reader_end_internal(&zip_archive, p != NULL);
+
+ if (pErr)
+ *pErr = zip_archive.m_last_error;
+
+ return p;
+ }
+
+ void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags)
+ {
+ return mz_zip_extract_archive_file_to_heap_v2(pZip_filename, pArchive_name, NULL, pSize, flags, NULL);
+ }
+
+#endif /* #ifndef MINIZ_NO_STDIO */
+
+#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */
+
+ /* ------------------- Misc utils */
+
+ mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip)
+ {
+ return pZip ? pZip->m_zip_mode : MZ_ZIP_MODE_INVALID;
+ }
+
+ mz_zip_type mz_zip_get_type(mz_zip_archive *pZip)
+ {
+ return pZip ? pZip->m_zip_type : MZ_ZIP_TYPE_INVALID;
+ }
+
+ mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num)
+ {
+ mz_zip_error prev_err;
+
+ if (!pZip)
+ return MZ_ZIP_INVALID_PARAMETER;
+
+ prev_err = pZip->m_last_error;
+
+ pZip->m_last_error = err_num;
+ return prev_err;
+ }
+
+ mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip)
+ {
+ if (!pZip)
+ return MZ_ZIP_INVALID_PARAMETER;
+
+ return pZip->m_last_error;
+ }
+
+ mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip)
+ {
+ return mz_zip_set_last_error(pZip, MZ_ZIP_NO_ERROR);
+ }
+
+ mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip)
+ {
+ mz_zip_error prev_err;
+
+ if (!pZip)
+ return MZ_ZIP_INVALID_PARAMETER;
+
+ prev_err = pZip->m_last_error;
+
+ pZip->m_last_error = MZ_ZIP_NO_ERROR;
+ return prev_err;
+ }
+
+ const char *mz_zip_get_error_string(mz_zip_error mz_err)
+ {
+ switch (mz_err)
+ {
+ case MZ_ZIP_NO_ERROR:
+ return "no error";
+ case MZ_ZIP_UNDEFINED_ERROR:
+ return "undefined error";
+ case MZ_ZIP_TOO_MANY_FILES:
+ return "too many files";
+ case MZ_ZIP_FILE_TOO_LARGE:
+ return "file too large";
+ case MZ_ZIP_UNSUPPORTED_METHOD:
+ return "unsupported method";
+ case MZ_ZIP_UNSUPPORTED_ENCRYPTION:
+ return "unsupported encryption";
+ case MZ_ZIP_UNSUPPORTED_FEATURE:
+ return "unsupported feature";
+ case MZ_ZIP_FAILED_FINDING_CENTRAL_DIR:
+ return "failed finding central directory";
+ case MZ_ZIP_NOT_AN_ARCHIVE:
+ return "not a ZIP archive";
+ case MZ_ZIP_INVALID_HEADER_OR_CORRUPTED:
+ return "invalid header or archive is corrupted";
+ case MZ_ZIP_UNSUPPORTED_MULTIDISK:
+ return "unsupported multidisk archive";
+ case MZ_ZIP_DECOMPRESSION_FAILED:
+ return "decompression failed or archive is corrupted";
+ case MZ_ZIP_COMPRESSION_FAILED:
+ return "compression failed";
+ case MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE:
+ return "unexpected decompressed size";
+ case MZ_ZIP_CRC_CHECK_FAILED:
+ return "CRC-32 check failed";
+ case MZ_ZIP_UNSUPPORTED_CDIR_SIZE:
+ return "unsupported central directory size";
+ case MZ_ZIP_ALLOC_FAILED:
+ return "allocation failed";
+ case MZ_ZIP_FILE_OPEN_FAILED:
+ return "file open failed";
+ case MZ_ZIP_FILE_CREATE_FAILED:
+ return "file create failed";
+ case MZ_ZIP_FILE_WRITE_FAILED:
+ return "file write failed";
+ case MZ_ZIP_FILE_READ_FAILED:
+ return "file read failed";
+ case MZ_ZIP_FILE_CLOSE_FAILED:
+ return "file close failed";
+ case MZ_ZIP_FILE_SEEK_FAILED:
+ return "file seek failed";
+ case MZ_ZIP_FILE_STAT_FAILED:
+ return "file stat failed";
+ case MZ_ZIP_INVALID_PARAMETER:
+ return "invalid parameter";
+ case MZ_ZIP_INVALID_FILENAME:
+ return "invalid filename";
+ case MZ_ZIP_BUF_TOO_SMALL:
+ return "buffer too small";
+ case MZ_ZIP_INTERNAL_ERROR:
+ return "internal error";
+ case MZ_ZIP_FILE_NOT_FOUND:
+ return "file not found";
+ case MZ_ZIP_ARCHIVE_TOO_LARGE:
+ return "archive is too large";
+ case MZ_ZIP_VALIDATION_FAILED:
+ return "validation failed";
+ case MZ_ZIP_WRITE_CALLBACK_FAILED:
+ return "write callback failed";
+ case MZ_ZIP_TOTAL_ERRORS:
+ return "total errors";
+ default:
+ break;
+ }
+
+ return "unknown error";
+ }
+
+ /* Note: Just because the archive is not zip64 doesn't necessarily mean it doesn't have Zip64 extended information extra field, argh. */
+ mz_bool mz_zip_is_zip64(mz_zip_archive *pZip)
+ {
+ if ((!pZip) || (!pZip->m_pState))
+ return MZ_FALSE;
+
+ return pZip->m_pState->m_zip64;
+ }
+
+ size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip)
+ {
+ if ((!pZip) || (!pZip->m_pState))
+ return 0;
+
+ return pZip->m_pState->m_central_dir.m_size;
+ }
+
+ mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip)
+ {
+ return pZip ? pZip->m_total_files : 0;
+ }
+
+ mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip)
+ {
+ if (!pZip)
+ return 0;
+ return pZip->m_archive_size;
+ }
+
+ mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip)
+ {
+ if ((!pZip) || (!pZip->m_pState))
+ return 0;
+ return pZip->m_pState->m_file_archive_start_ofs;
+ }
+
+ MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip)
+ {
+ if ((!pZip) || (!pZip->m_pState))
+ return 0;
+ return pZip->m_pState->m_pFile;
+ }
+
+ size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n)
+ {
+ if ((!pZip) || (!pZip->m_pState) || (!pBuf) || (!pZip->m_pRead))
+ return mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+
+ return pZip->m_pRead(pZip->m_pIO_opaque, file_ofs, pBuf, n);
+ }
+
+ mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size)
+ {
+ mz_uint n;
+ const mz_uint8 *p = mz_zip_get_cdh(pZip, file_index);
+ if (!p)
+ {
+ if (filename_buf_size)
+ pFilename[0] = '\0';
+ mz_zip_set_error(pZip, MZ_ZIP_INVALID_PARAMETER);
+ return 0;
+ }
+ n = MZ_READ_LE16(p + MZ_ZIP_CDH_FILENAME_LEN_OFS);
+ if (filename_buf_size)
+ {
+ n = MZ_MIN(n, filename_buf_size - 1);
+ memcpy(pFilename, p + MZ_ZIP_CENTRAL_DIR_HEADER_SIZE, n);
+ pFilename[n] = '\0';
+ }
+ return n + 1;
+ }
+
+ mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat)
+ {
+ return mz_zip_file_stat_internal(pZip, file_index, mz_zip_get_cdh(pZip, file_index), pStat, NULL);
+ }
+
+ mz_bool mz_zip_end(mz_zip_archive *pZip)
+ {
+ if (!pZip)
+ return MZ_FALSE;
+
+ if (pZip->m_zip_mode == MZ_ZIP_MODE_READING)
+ return mz_zip_reader_end(pZip);
+#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS
+ else if ((pZip->m_zip_mode == MZ_ZIP_MODE_WRITING) || (pZip->m_zip_mode == MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED))
+ return mz_zip_writer_end(pZip);
+#endif
+
+ return MZ_FALSE;
+ }
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*#ifndef MINIZ_NO_ARCHIVE_APIS*/
diff --git a/src/installer/thirdparty/miniz/miniz.h b/src/installer/thirdparty/miniz/miniz.h
new file mode 100644
index 00000000..d8244d08
--- /dev/null
+++ b/src/installer/thirdparty/miniz/miniz.h
@@ -0,0 +1,1510 @@
+#ifndef MINIZ_EXPORT
+#define MINIZ_EXPORT
+#endif
+/* miniz.c 3.1.2 - public domain deflate/inflate, zlib-subset, ZIP reading/writing/appending, PNG writing
+ See "unlicense" statement at the end of this file.
+ Rich Geldreich , last updated Oct. 13, 2013
+ Implements RFC 1950: http://www.ietf.org/rfc/rfc1950.txt and RFC 1951: http://www.ietf.org/rfc/rfc1951.txt
+
+ Most API's defined in miniz.c are optional. For example, to disable the archive related functions just define
+ MINIZ_NO_ARCHIVE_APIS, or to get rid of all stdio usage define MINIZ_NO_STDIO (see the list below for more macros).
+
+ * Low-level Deflate/Inflate implementation notes:
+
+ Compression: Use the "tdefl" API's. The compressor supports raw, static, and dynamic blocks, lazy or
+ greedy parsing, match length filtering, RLE-only, and Huffman-only streams. It performs and compresses
+ approximately as well as zlib.
+
+ Decompression: Use the "tinfl" API's. The entire decompressor is implemented as a single function
+ coroutine: see tinfl_decompress(). It supports decompression into a 32KB (or larger power of 2) wrapping buffer, or into a memory
+ block large enough to hold the entire file.
+
+ The low-level tdefl/tinfl API's do not make any use of dynamic memory allocation.
+
+ * zlib-style API notes:
+
+ miniz.c implements a fairly large subset of zlib. There's enough functionality present for it to be a drop-in
+ zlib replacement in many apps:
+ The z_stream struct, optional memory allocation callbacks
+ deflateInit/deflateInit2/deflate/deflateReset/deflateEnd/deflateBound
+ inflateInit/inflateInit2/inflate/inflateReset/inflateEnd
+ compress, compress2, compressBound, uncompress
+ CRC-32, Adler-32 - Using modern, minimal code size, CPU cache friendly routines.
+ Supports raw deflate streams or standard zlib streams with adler-32 checking.
+
+ Limitations:
+ The callback API's are not implemented yet. No support for gzip headers or zlib static dictionaries.
+ I've tried to closely emulate zlib's various flavors of stream flushing and return status codes, but
+ there are no guarantees that miniz.c pulls this off perfectly.
+
+ * PNG writing: See the tdefl_write_image_to_png_file_in_memory() function, originally written by
+ Alex Evans. Supports 1-4 bytes/pixel images.
+
+ * ZIP archive API notes:
+
+ The ZIP archive API's where designed with simplicity and efficiency in mind, with just enough abstraction to
+ get the job done with minimal fuss. There are simple API's to retrieve file information, read files from
+ existing archives, create new archives, append new files to existing archives, or clone archive data from
+ one archive to another. It supports archives located in memory or the heap, on disk (using stdio.h),
+ or you can specify custom file read/write callbacks.
+
+ - Archive reading: Just call this function to read a single file from a disk archive:
+
+ void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name,
+ size_t *pSize, mz_uint zip_flags);
+
+ For more complex cases, use the "mz_zip_reader" functions. Upon opening an archive, the entire central
+ directory is located and read as-is into memory, and subsequent file access only occurs when reading individual files.
+
+ - Archives file scanning: The simple way is to use this function to scan a loaded archive for a specific file:
+
+ int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags);
+
+ The locate operation can optionally check file comments too, which (as one example) can be used to identify
+ multiple versions of the same file in an archive. This function uses a simple linear search through the central
+ directory, so it's not very fast.
+
+ Alternately, you can iterate through all the files in an archive (using mz_zip_reader_get_num_files()) and
+ retrieve detailed info on each file by calling mz_zip_reader_file_stat().
+
+ - Archive creation: Use the "mz_zip_writer" functions. The ZIP writer immediately writes compressed file data
+ to disk and builds an exact image of the central directory in memory. The central directory image is written
+ all at once at the end of the archive file when the archive is finalized.
+
+ The archive writer can optionally align each file's local header and file data to any power of 2 alignment,
+ which can be useful when the archive will be read from optical media. Also, the writer supports placing
+ arbitrary data blobs at the very beginning of ZIP archives. Archives written using either feature are still
+ readable by any ZIP tool.
+
+ - Archive appending: The simple way to add a single file to an archive is to call this function:
+
+ mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name,
+ const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags);
+
+ The archive will be created if it doesn't already exist, otherwise it'll be appended to.
+ Note the appending is done in-place and is not an atomic operation, so if something goes wrong
+ during the operation it's possible the archive could be left without a central directory (although the local
+ file headers and file data will be fine, so the archive will be recoverable).
+
+ For more complex archive modification scenarios:
+ 1. The safest way is to use a mz_zip_reader to read the existing archive, cloning only those bits you want to
+ preserve into a new archive using using the mz_zip_writer_add_from_zip_reader() function (which compiles the
+ compressed file data as-is). When you're done, delete the old archive and rename the newly written archive, and
+ you're done. This is safe but requires a bunch of temporary disk space or heap memory.
+
+ 2. Or, you can convert an mz_zip_reader in-place to an mz_zip_writer using mz_zip_writer_init_from_reader(),
+ append new files as needed, then finalize the archive which will write an updated central directory to the
+ original archive. (This is basically what mz_zip_add_mem_to_archive_file_in_place() does.) There's a
+ possibility that the archive's central directory could be lost with this method if anything goes wrong, though.
+
+ - ZIP archive support limitations:
+ No spanning support. Extraction functions can only handle unencrypted, stored or deflated files.
+ Requires streams capable of seeking.
+
+ * This is a header file library, like stb_image.c. To get only a header file, either cut and paste the
+ below header, or create miniz.h, #define MINIZ_HEADER_FILE_ONLY, and then include miniz.c from it.
+
+ * Important: For best perf. be sure to customize the below macros for your target platform:
+ #define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 1
+ #define MINIZ_LITTLE_ENDIAN 1
+ #define MINIZ_HAS_64BIT_REGISTERS 1
+
+ * On platforms using glibc, Be sure to "#define _LARGEFILE64_SOURCE 1" before including miniz.c to ensure miniz
+ uses the 64-bit variants: fopen64(), stat64(), etc. Otherwise you won't be able to process large files
+ (i.e. 32-bit stat() fails for me on files > 0x7FFFFFFF bytes).
+*/
+#pragma once
+
+
+
+#if defined(__STRICT_ANSI__)
+#define MZ_FORCEINLINE
+#elif defined(_MSC_VER)
+#define MZ_FORCEINLINE __forceinline
+#elif defined(__GNUC__)
+#define MZ_FORCEINLINE __inline__ __attribute__((__always_inline__))
+#else
+#define MZ_FORCEINLINE inline
+#endif
+
+/* Defines to completely disable specific portions of miniz.c:
+ If all macros here are defined the only functionality remaining will be CRC-32 and adler-32. */
+
+/* Define MINIZ_NO_STDIO to disable all usage and any functions which rely on stdio for file I/O. */
+/*#define MINIZ_NO_STDIO */
+
+/* If MINIZ_NO_TIME is specified then the ZIP archive functions will not be able to get the current time, or */
+/* get/set file times, and the C run-time funcs that get/set times won't be called. */
+/* The current downside is the times written to your archives will be from 1979. */
+/*#define MINIZ_NO_TIME */
+
+/* Define MINIZ_NO_DEFLATE_APIS to disable all compression API's. */
+/*#define MINIZ_NO_DEFLATE_APIS */
+
+/* Define MINIZ_NO_INFLATE_APIS to disable all decompression API's. */
+/*#define MINIZ_NO_INFLATE_APIS */
+
+/* Define MINIZ_NO_ARCHIVE_APIS to disable all ZIP archive API's. */
+/*#define MINIZ_NO_ARCHIVE_APIS */
+
+/* Define MINIZ_NO_ARCHIVE_WRITING_APIS to disable all writing related ZIP archive API's. */
+/*#define MINIZ_NO_ARCHIVE_WRITING_APIS */
+
+/* Define MINIZ_NO_ZLIB_APIS to remove all ZLIB-style compression/decompression API's. */
+/*#define MINIZ_NO_ZLIB_APIS */
+
+/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAME to disable zlib names, to prevent conflicts against stock zlib. */
+/*#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES */
+
+/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc.
+ Note if MINIZ_NO_MALLOC is defined then the user must always provide custom user alloc/free/realloc
+ callbacks to the zlib and archive API's, and a few stand-alone helper API's which don't provide custom user
+ functions (such as tdefl_compress_mem_to_heap() and tinfl_decompress_mem_to_heap()) won't work. */
+/*#define MINIZ_NO_MALLOC */
+
+#ifdef MINIZ_NO_INFLATE_APIS
+#define MINIZ_NO_ARCHIVE_APIS
+#endif
+
+#ifdef MINIZ_NO_DEFLATE_APIS
+#define MINIZ_NO_ARCHIVE_WRITING_APIS
+#endif
+
+#if defined(__TINYC__) && (defined(__linux) || defined(__linux__))
+/* TODO: Work around "error: include file 'sys\utime.h' when compiling with tcc on Linux */
+#define MINIZ_NO_TIME
+#endif
+
+#include
+
+#if !defined(MINIZ_NO_TIME) && !defined(MINIZ_NO_ARCHIVE_APIS)
+#include
+#endif
+
+#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__i386) || defined(__i486__) || defined(__i486) || defined(i386) || defined(__ia64__) || defined(__x86_64__)
+/* MINIZ_X86_OR_X64_CPU is only used to help set the below macros. */
+#define MINIZ_X86_OR_X64_CPU 1
+#else
+#define MINIZ_X86_OR_X64_CPU 0
+#endif
+
+/* Set MINIZ_LITTLE_ENDIAN only if not set */
+#if !defined(MINIZ_LITTLE_ENDIAN)
+#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__)
+
+#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+/* Set MINIZ_LITTLE_ENDIAN to 1 if the processor is little endian. */
+#define MINIZ_LITTLE_ENDIAN 1
+#else
+#define MINIZ_LITTLE_ENDIAN 0
+#endif
+
+#else
+
+#if MINIZ_X86_OR_X64_CPU
+#define MINIZ_LITTLE_ENDIAN 1
+#else
+#define MINIZ_LITTLE_ENDIAN 0
+#endif
+
+#endif
+#endif
+
+/* Using unaligned loads and stores causes errors when using UBSan */
+#if defined(__has_feature)
+#if __has_feature(undefined_behavior_sanitizer)
+#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0
+#endif
+#endif
+
+/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES only if not set */
+#if !defined(MINIZ_USE_UNALIGNED_LOADS_AND_STORES)
+#if MINIZ_X86_OR_X64_CPU
+/* Set MINIZ_USE_UNALIGNED_LOADS_AND_STORES to 1 on CPU's that permit efficient integer loads and stores from unaligned addresses. */
+#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0
+#define MINIZ_UNALIGNED_USE_MEMCPY
+#else
+#define MINIZ_USE_UNALIGNED_LOADS_AND_STORES 0
+#endif
+#endif
+
+#if defined(_M_X64) || defined(_WIN64) || defined(__MINGW64__) || defined(_LP64) || defined(__LP64__) || defined(__ia64__) || defined(__x86_64__)
+/* Set MINIZ_HAS_64BIT_REGISTERS to 1 if operations on 64-bit integers are reasonably fast (and don't involve compiler generated calls to helper functions). */
+#define MINIZ_HAS_64BIT_REGISTERS 1
+#else
+#define MINIZ_HAS_64BIT_REGISTERS 0
+#endif
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ /* ------------------- zlib-style API Definitions. */
+
+ /* For more compatibility with zlib, miniz.c uses unsigned long for some parameters/struct members. Beware: mz_ulong can be either 32 or 64-bits! */
+ typedef unsigned long mz_ulong;
+
+ /* mz_free() internally uses the MZ_FREE() macro (which by default calls free() unless you've modified the MZ_MALLOC macro) to release a block allocated from the heap. */
+ MINIZ_EXPORT void mz_free(void *p);
+
+#define MZ_ADLER32_INIT (1)
+ /* mz_adler32() returns the initial adler-32 value to use when called with ptr==NULL. */
+ MINIZ_EXPORT mz_ulong mz_adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len);
+
+#define MZ_CRC32_INIT (0)
+ /* mz_crc32() returns the initial CRC-32 value to use when called with ptr==NULL. */
+ MINIZ_EXPORT mz_ulong mz_crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len);
+
+ /* Compression strategies. */
+ enum
+ {
+ MZ_DEFAULT_STRATEGY = 0,
+ MZ_FILTERED = 1,
+ MZ_HUFFMAN_ONLY = 2,
+ MZ_RLE = 3,
+ MZ_FIXED = 4
+ };
+
+/* Method */
+#define MZ_DEFLATED 8
+
+ /* Heap allocation callbacks.
+ Note that mz_alloc_func parameter types purposely differ from zlib's: items/size is size_t, not unsigned long. */
+ typedef void *(*mz_alloc_func)(void *opaque, size_t items, size_t size);
+ typedef void (*mz_free_func)(void *opaque, void *address);
+ typedef void *(*mz_realloc_func)(void *opaque, void *address, size_t items, size_t size);
+
+ /* Compression levels: 0-9 are the standard zlib-style levels, 10 is best possible compression (not zlib compatible, and may be very slow), MZ_DEFAULT_COMPRESSION=MZ_DEFAULT_LEVEL. */
+ enum
+ {
+ MZ_NO_COMPRESSION = 0,
+ MZ_BEST_SPEED = 1,
+ MZ_BEST_COMPRESSION = 9,
+ MZ_UBER_COMPRESSION = 10,
+ MZ_DEFAULT_LEVEL = 6,
+ MZ_DEFAULT_COMPRESSION = -1
+ };
+
+#define MZ_VERSION "11.3.2"
+#define MZ_VERNUM 0xB302
+#define MZ_VER_MAJOR 11
+#define MZ_VER_MINOR 3
+#define MZ_VER_REVISION 2
+#define MZ_VER_SUBREVISION 0
+
+#ifndef MINIZ_NO_ZLIB_APIS
+
+ /* Flush values. For typical usage you only need MZ_NO_FLUSH and MZ_FINISH. The other values are for advanced use (refer to the zlib docs). */
+ enum
+ {
+ MZ_NO_FLUSH = 0,
+ MZ_PARTIAL_FLUSH = 1,
+ MZ_SYNC_FLUSH = 2,
+ MZ_FULL_FLUSH = 3,
+ MZ_FINISH = 4,
+ MZ_BLOCK = 5
+ };
+
+ /* Return status codes. MZ_PARAM_ERROR is non-standard. */
+ enum
+ {
+ MZ_OK = 0,
+ MZ_STREAM_END = 1,
+ MZ_NEED_DICT = 2,
+ MZ_ERRNO = -1,
+ MZ_STREAM_ERROR = -2,
+ MZ_DATA_ERROR = -3,
+ MZ_MEM_ERROR = -4,
+ MZ_BUF_ERROR = -5,
+ MZ_VERSION_ERROR = -6,
+ MZ_PARAM_ERROR = -10000
+ };
+
+/* Window bits */
+#define MZ_DEFAULT_WINDOW_BITS 15
+
+ struct mz_internal_state;
+
+ /* Compression/decompression stream struct. */
+ typedef struct mz_stream_s
+ {
+ const unsigned char *next_in; /* pointer to next byte to read */
+ unsigned int avail_in; /* number of bytes available at next_in */
+ mz_ulong total_in; /* total number of bytes consumed so far */
+
+ unsigned char *next_out; /* pointer to next byte to write */
+ unsigned int avail_out; /* number of bytes that can be written to next_out */
+ mz_ulong total_out; /* total number of bytes produced so far */
+
+ char *msg; /* error msg (unused) */
+ struct mz_internal_state *state; /* internal state, allocated by zalloc/zfree */
+
+ mz_alloc_func zalloc; /* optional heap allocation function (defaults to malloc) */
+ mz_free_func zfree; /* optional heap free function (defaults to free) */
+ void *opaque; /* heap alloc function user pointer */
+
+ int data_type; /* data_type (unused) */
+ mz_ulong adler; /* adler32 of the source or uncompressed data */
+ mz_ulong reserved; /* not used */
+ } mz_stream;
+
+ typedef mz_stream *mz_streamp;
+
+ /* Returns the version string of miniz.c. */
+ MINIZ_EXPORT const char *mz_version(void);
+
+#ifndef MINIZ_NO_DEFLATE_APIS
+
+ /* mz_deflateInit() initializes a compressor with default options: */
+ /* Parameters: */
+ /* pStream must point to an initialized mz_stream struct. */
+ /* level must be between [MZ_NO_COMPRESSION, MZ_BEST_COMPRESSION]. */
+ /* level 1 enables a specially optimized compression function that's been optimized purely for performance, not ratio. */
+ /* (This special func. is currently only enabled when MINIZ_USE_UNALIGNED_LOADS_AND_STORES and MINIZ_LITTLE_ENDIAN are defined.) */
+ /* Return values: */
+ /* MZ_OK on success. */
+ /* MZ_STREAM_ERROR if the stream is bogus. */
+ /* MZ_PARAM_ERROR if the input parameters are bogus. */
+ /* MZ_MEM_ERROR on out of memory. */
+ MINIZ_EXPORT int mz_deflateInit(mz_streamp pStream, int level);
+
+ /* mz_deflateInit2() is like mz_deflate(), except with more control: */
+ /* Additional parameters: */
+ /* method must be MZ_DEFLATED */
+ /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to wrap the deflate stream with zlib header/adler-32 footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate/no header or footer) */
+ /* mem_level must be between [1, 9] (it's checked but ignored by miniz.c) */
+ MINIZ_EXPORT int mz_deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy);
+
+ /* Quickly resets a compressor without having to reallocate anything. Same as calling mz_deflateEnd() followed by mz_deflateInit()/mz_deflateInit2(). */
+ MINIZ_EXPORT int mz_deflateReset(mz_streamp pStream);
+
+ /* mz_deflate() compresses the input to output, consuming as much of the input and producing as much output as possible. */
+ /* Parameters: */
+ /* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */
+ /* flush may be MZ_NO_FLUSH, MZ_PARTIAL_FLUSH/MZ_SYNC_FLUSH, MZ_FULL_FLUSH, or MZ_FINISH. */
+ /* Return values: */
+ /* MZ_OK on success (when flushing, or if more input is needed but not available, and/or there's more output to be written but the output buffer is full). */
+ /* MZ_STREAM_END if all input has been consumed and all output bytes have been written. Don't call mz_deflate() on the stream anymore. */
+ /* MZ_STREAM_ERROR if the stream is bogus. */
+ /* MZ_PARAM_ERROR if one of the parameters is invalid. */
+ /* MZ_BUF_ERROR if no forward progress is possible because the input and/or output buffers are empty. (Fill up the input buffer or free up some output space and try again.) */
+ MINIZ_EXPORT int mz_deflate(mz_streamp pStream, int flush);
+
+ /* mz_deflateEnd() deinitializes a compressor: */
+ /* Return values: */
+ /* MZ_OK on success. */
+ /* MZ_STREAM_ERROR if the stream is bogus. */
+ MINIZ_EXPORT int mz_deflateEnd(mz_streamp pStream);
+
+ /* mz_deflateBound() returns a (very) conservative upper bound on the amount of data that could be generated by deflate(), assuming flush is set to only MZ_NO_FLUSH or MZ_FINISH. */
+ MINIZ_EXPORT mz_ulong mz_deflateBound(mz_streamp pStream, mz_ulong source_len);
+
+ /* Single-call compression functions mz_compress() and mz_compress2(): */
+ /* Returns MZ_OK on success, or one of the error codes from mz_deflate() on failure. */
+ MINIZ_EXPORT int mz_compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len);
+ MINIZ_EXPORT int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level);
+
+ /* mz_compressBound() returns a (very) conservative upper bound on the amount of data that could be generated by calling mz_compress(). */
+ MINIZ_EXPORT mz_ulong mz_compressBound(mz_ulong source_len);
+
+#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
+
+#ifndef MINIZ_NO_INFLATE_APIS
+
+ /* Initializes a decompressor. */
+ MINIZ_EXPORT int mz_inflateInit(mz_streamp pStream);
+
+ /* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether or not the stream has been wrapped with a zlib header/footer: */
+ /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to parse zlib header/footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate). */
+ MINIZ_EXPORT int mz_inflateInit2(mz_streamp pStream, int window_bits);
+
+ /* Quickly resets a compressor without having to reallocate anything. Same as calling mz_inflateEnd() followed by mz_inflateInit()/mz_inflateInit2(). */
+ MINIZ_EXPORT int mz_inflateReset(mz_streamp pStream);
+
+ /* Decompresses the input stream to the output, consuming only as much of the input as needed, and writing as much to the output as possible. */
+ /* Parameters: */
+ /* pStream is the stream to read from and write to. You must initialize/update the next_in, avail_in, next_out, and avail_out members. */
+ /* flush may be MZ_NO_FLUSH, MZ_SYNC_FLUSH, or MZ_FINISH. */
+ /* On the first call, if flush is MZ_FINISH it's assumed the input and output buffers are both sized large enough to decompress the entire stream in a single call (this is slightly faster). */
+ /* MZ_FINISH implies that there are no more source bytes available beside what's already in the input buffer, and that the output buffer is large enough to hold the rest of the decompressed data. */
+ /* Return values: */
+ /* MZ_OK on success. Either more input is needed but not available, and/or there's more output to be written but the output buffer is full. */
+ /* MZ_STREAM_END if all needed input has been consumed and all output bytes have been written. For zlib streams, the adler-32 of the decompressed data has also been verified. */
+ /* MZ_STREAM_ERROR if the stream is bogus. */
+ /* MZ_DATA_ERROR if the deflate stream is invalid. */
+ /* MZ_PARAM_ERROR if one of the parameters is invalid. */
+ /* MZ_BUF_ERROR if no forward progress is possible because the input buffer is empty but the inflater needs more input to continue, or if the output buffer is not large enough. Call mz_inflate() again */
+ /* with more input data, or with more room in the output buffer (except when using single call decompression, described above). */
+ MINIZ_EXPORT int mz_inflate(mz_streamp pStream, int flush);
+
+ /* Deinitializes a decompressor. */
+ MINIZ_EXPORT int mz_inflateEnd(mz_streamp pStream);
+
+ /* Single-call decompression. */
+ /* Returns MZ_OK on success, or one of the error codes from mz_inflate() on failure. */
+ MINIZ_EXPORT int mz_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len);
+ MINIZ_EXPORT int mz_uncompress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len);
+#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
+
+ /* Returns a string description of the specified error code, or NULL if the error code is invalid. */
+ MINIZ_EXPORT const char *mz_error(int err);
+
+/* Redefine zlib-compatible names to miniz equivalents, so miniz.c can be used as a drop-in replacement for the subset of zlib that miniz.c supports. */
+/* Define MINIZ_NO_ZLIB_COMPATIBLE_NAMES to disable zlib-compatibility if you use zlib in the same project. */
+#ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES
+ typedef unsigned char Byte;
+ typedef unsigned int uInt;
+ typedef mz_ulong uLong;
+ typedef Byte Bytef;
+ typedef uInt uIntf;
+ typedef char charf;
+ typedef int intf;
+ typedef void *voidpf;
+ typedef uLong uLongf;
+ typedef void *voidp;
+ typedef void *const voidpc;
+#define Z_NULL 0
+#define Z_NO_FLUSH MZ_NO_FLUSH
+#define Z_PARTIAL_FLUSH MZ_PARTIAL_FLUSH
+#define Z_SYNC_FLUSH MZ_SYNC_FLUSH
+#define Z_FULL_FLUSH MZ_FULL_FLUSH
+#define Z_FINISH MZ_FINISH
+#define Z_BLOCK MZ_BLOCK
+#define Z_OK MZ_OK
+#define Z_STREAM_END MZ_STREAM_END
+#define Z_NEED_DICT MZ_NEED_DICT
+#define Z_ERRNO MZ_ERRNO
+#define Z_STREAM_ERROR MZ_STREAM_ERROR
+#define Z_DATA_ERROR MZ_DATA_ERROR
+#define Z_MEM_ERROR MZ_MEM_ERROR
+#define Z_BUF_ERROR MZ_BUF_ERROR
+#define Z_VERSION_ERROR MZ_VERSION_ERROR
+#define Z_PARAM_ERROR MZ_PARAM_ERROR
+#define Z_NO_COMPRESSION MZ_NO_COMPRESSION
+#define Z_BEST_SPEED MZ_BEST_SPEED
+#define Z_BEST_COMPRESSION MZ_BEST_COMPRESSION
+#define Z_DEFAULT_COMPRESSION MZ_DEFAULT_COMPRESSION
+#define Z_DEFAULT_STRATEGY MZ_DEFAULT_STRATEGY
+#define Z_FILTERED MZ_FILTERED
+#define Z_HUFFMAN_ONLY MZ_HUFFMAN_ONLY
+#define Z_RLE MZ_RLE
+#define Z_FIXED MZ_FIXED
+#define Z_DEFLATED MZ_DEFLATED
+#define Z_DEFAULT_WINDOW_BITS MZ_DEFAULT_WINDOW_BITS
+ /* See mz_alloc_func */
+ typedef void *(*alloc_func)(void *opaque, size_t items, size_t size);
+ /* See mz_free_func */
+ typedef void (*free_func)(void *opaque, void *address);
+
+#define internal_state mz_internal_state
+#define z_stream mz_stream
+
+#ifndef MINIZ_NO_DEFLATE_APIS
+ /* Compatiblity with zlib API. See called functions for documentation */
+ static MZ_FORCEINLINE int deflateInit(mz_streamp pStream, int level)
+ {
+ return mz_deflateInit(pStream, level);
+ }
+ static MZ_FORCEINLINE int deflateInit2(mz_streamp pStream, int level, int method, int window_bits, int mem_level, int strategy)
+ {
+ return mz_deflateInit2(pStream, level, method, window_bits, mem_level, strategy);
+ }
+ static MZ_FORCEINLINE int deflateReset(mz_streamp pStream)
+ {
+ return mz_deflateReset(pStream);
+ }
+ static MZ_FORCEINLINE int deflate(mz_streamp pStream, int flush)
+ {
+ return mz_deflate(pStream, flush);
+ }
+ static MZ_FORCEINLINE int deflateEnd(mz_streamp pStream)
+ {
+ return mz_deflateEnd(pStream);
+ }
+ static MZ_FORCEINLINE mz_ulong deflateBound(mz_streamp pStream, mz_ulong source_len)
+ {
+ return mz_deflateBound(pStream, source_len);
+ }
+ static MZ_FORCEINLINE int compress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len)
+ {
+ return mz_compress(pDest, pDest_len, pSource, source_len);
+ }
+ static MZ_FORCEINLINE int compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level)
+ {
+ return mz_compress2(pDest, pDest_len, pSource, source_len, level);
+ }
+ static MZ_FORCEINLINE mz_ulong compressBound(mz_ulong source_len)
+ {
+ return mz_compressBound(source_len);
+ }
+#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
+
+#ifndef MINIZ_NO_INFLATE_APIS
+ /* Compatiblity with zlib API. See called functions for documentation */
+ static MZ_FORCEINLINE int inflateInit(mz_streamp pStream)
+ {
+ return mz_inflateInit(pStream);
+ }
+
+ static MZ_FORCEINLINE int inflateInit2(mz_streamp pStream, int window_bits)
+ {
+ return mz_inflateInit2(pStream, window_bits);
+ }
+
+ static MZ_FORCEINLINE int inflateReset(mz_streamp pStream)
+ {
+ return mz_inflateReset(pStream);
+ }
+
+ static MZ_FORCEINLINE int inflate(mz_streamp pStream, int flush)
+ {
+ return mz_inflate(pStream, flush);
+ }
+
+ static MZ_FORCEINLINE int inflateEnd(mz_streamp pStream)
+ {
+ return mz_inflateEnd(pStream);
+ }
+
+ static MZ_FORCEINLINE int uncompress(unsigned char* pDest, mz_ulong* pDest_len, const unsigned char* pSource, mz_ulong source_len)
+ {
+ return mz_uncompress(pDest, pDest_len, pSource, source_len);
+ }
+
+ static MZ_FORCEINLINE int uncompress2(unsigned char* pDest, mz_ulong* pDest_len, const unsigned char* pSource, mz_ulong* pSource_len)
+ {
+ return mz_uncompress2(pDest, pDest_len, pSource, pSource_len);
+ }
+#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
+
+ static MZ_FORCEINLINE mz_ulong crc32(mz_ulong crc, const unsigned char *ptr, size_t buf_len)
+ {
+ return mz_crc32(crc, ptr, buf_len);
+ }
+
+ static MZ_FORCEINLINE mz_ulong adler32(mz_ulong adler, const unsigned char *ptr, size_t buf_len)
+ {
+ return mz_adler32(adler, ptr, buf_len);
+ }
+
+#define MAX_WBITS 15
+#define MAX_MEM_LEVEL 9
+
+ static MZ_FORCEINLINE const char* zError(int err)
+ {
+ return mz_error(err);
+ }
+#define ZLIB_VERSION MZ_VERSION
+#define ZLIB_VERNUM MZ_VERNUM
+#define ZLIB_VER_MAJOR MZ_VER_MAJOR
+#define ZLIB_VER_MINOR MZ_VER_MINOR
+#define ZLIB_VER_REVISION MZ_VER_REVISION
+#define ZLIB_VER_SUBREVISION MZ_VER_SUBREVISION
+
+#define zlibVersion mz_version
+#define zlib_version mz_version()
+#endif /* #ifndef MINIZ_NO_ZLIB_COMPATIBLE_NAMES */
+
+#endif /* MINIZ_NO_ZLIB_APIS */
+
+#ifdef __cplusplus
+}
+#endif
+
+
+
+
+
+#pragma once
+#include
+#include
+#include
+#include
+
+
+
+/* ------------------- Types and macros */
+typedef unsigned char mz_uint8;
+typedef int16_t mz_int16;
+typedef uint16_t mz_uint16;
+typedef uint32_t mz_uint32;
+typedef uint32_t mz_uint;
+typedef int64_t mz_int64;
+typedef uint64_t mz_uint64;
+typedef int mz_bool;
+
+#define MZ_FALSE (0)
+#define MZ_TRUE (1)
+
+/* Works around MSVC's spammy "warning C4127: conditional expression is constant" message. */
+#ifdef _MSC_VER
+#define MZ_MACRO_END while (0, 0)
+#else
+#define MZ_MACRO_END while (0)
+#endif
+
+#ifdef MINIZ_NO_STDIO
+#define MZ_FILE void *
+#else
+#include
+#define MZ_FILE FILE
+#endif /* #ifdef MINIZ_NO_STDIO */
+
+#ifdef MINIZ_NO_TIME
+typedef struct mz_dummy_time_t_tag
+{
+ mz_uint32 m_dummy1;
+ mz_uint32 m_dummy2;
+} mz_dummy_time_t;
+#define MZ_TIME_T mz_dummy_time_t
+#else
+#define MZ_TIME_T time_t
+#endif
+
+#define MZ_ASSERT(x) assert(x)
+
+#ifdef MINIZ_NO_MALLOC
+#define MZ_MALLOC(x) NULL
+#define MZ_FREE(x) (void)x, ((void)0)
+#define MZ_REALLOC(p, x) NULL
+#else
+#define MZ_MALLOC(x) malloc(x)
+#define MZ_FREE(x) free(x)
+#define MZ_REALLOC(p, x) realloc(p, x)
+#endif
+
+#define MZ_MAX(a, b) (((a) > (b)) ? (a) : (b))
+#define MZ_MIN(a, b) (((a) < (b)) ? (a) : (b))
+#define MZ_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj))
+#define MZ_CLEAR_ARR(obj) memset((obj), 0, sizeof(obj))
+#define MZ_CLEAR_PTR(obj) memset((obj), 0, sizeof(*obj))
+
+#if MINIZ_USE_UNALIGNED_LOADS_AND_STORES && MINIZ_LITTLE_ENDIAN
+#define MZ_READ_LE16(p) *((const mz_uint16 *)(p))
+#define MZ_READ_LE32(p) *((const mz_uint32 *)(p))
+#else
+#define MZ_READ_LE16(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U))
+#define MZ_READ_LE32(p) ((mz_uint32)(((const mz_uint8 *)(p))[0]) | ((mz_uint32)(((const mz_uint8 *)(p))[1]) << 8U) | ((mz_uint32)(((const mz_uint8 *)(p))[2]) << 16U) | ((mz_uint32)(((const mz_uint8 *)(p))[3]) << 24U))
+#endif
+
+#define MZ_READ_LE64(p) (((mz_uint64)MZ_READ_LE32(p)) | (((mz_uint64)MZ_READ_LE32((const mz_uint8 *)(p) + sizeof(mz_uint32))) << 32U))
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ extern MINIZ_EXPORT void *miniz_def_alloc_func(void *opaque, size_t items, size_t size);
+ extern MINIZ_EXPORT void miniz_def_free_func(void *opaque, void *address);
+ extern MINIZ_EXPORT void *miniz_def_realloc_func(void *opaque, void *address, size_t items, size_t size);
+
+#define MZ_UINT16_MAX (0xFFFFU)
+#define MZ_UINT32_MAX (0xFFFFFFFFU)
+
+#ifdef __cplusplus
+}
+#endif
+ #pragma once
+
+
+#ifndef MINIZ_NO_DEFLATE_APIS
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+/* ------------------- Low-level Compression API Definitions */
+
+/* Set TDEFL_LESS_MEMORY to 1 to use less memory (compression will be slightly slower, and raw/dynamic blocks will be output more frequently). */
+#ifndef TDEFL_LESS_MEMORY
+#define TDEFL_LESS_MEMORY 0
+#endif
+
+ /* tdefl_init() compression flags logically OR'd together (low 12 bits contain the max. number of probes per dictionary search): */
+ /* TDEFL_DEFAULT_MAX_PROBES: The compressor defaults to 128 dictionary probes per dictionary search. 0=Huffman only, 1=Huffman+LZ (fastest/crap compression), 4095=Huffman+LZ (slowest/best compression). */
+ enum
+ {
+ TDEFL_HUFFMAN_ONLY = 0,
+ TDEFL_DEFAULT_MAX_PROBES = 128,
+ TDEFL_MAX_PROBES_MASK = 0xFFF
+ };
+
+ /* TDEFL_WRITE_ZLIB_HEADER: If set, the compressor outputs a zlib header before the deflate data, and the Adler-32 of the source data at the end. Otherwise, you'll get raw deflate data. */
+ /* TDEFL_COMPUTE_ADLER32: Always compute the adler-32 of the input data (even when not writing zlib headers). */
+ /* TDEFL_GREEDY_PARSING_FLAG: Set to use faster greedy parsing, instead of more efficient lazy parsing. */
+ /* TDEFL_NONDETERMINISTIC_PARSING_FLAG: Enable to decrease the compressor's initialization time to the minimum, but the output may vary from run to run given the same input (depending on the contents of memory). */
+ /* TDEFL_RLE_MATCHES: Only look for RLE matches (matches with a distance of 1) */
+ /* TDEFL_FILTER_MATCHES: Discards matches <= 5 chars if enabled. */
+ /* TDEFL_FORCE_ALL_STATIC_BLOCKS: Disable usage of optimized Huffman tables. */
+ /* TDEFL_FORCE_ALL_RAW_BLOCKS: Only use raw (uncompressed) deflate blocks. */
+ /* The low 12 bits are reserved to control the max # of hash probes per dictionary lookup (see TDEFL_MAX_PROBES_MASK). */
+ enum
+ {
+ TDEFL_WRITE_ZLIB_HEADER = 0x01000,
+ TDEFL_COMPUTE_ADLER32 = 0x02000,
+ TDEFL_GREEDY_PARSING_FLAG = 0x04000,
+ TDEFL_NONDETERMINISTIC_PARSING_FLAG = 0x08000,
+ TDEFL_RLE_MATCHES = 0x10000,
+ TDEFL_FILTER_MATCHES = 0x20000,
+ TDEFL_FORCE_ALL_STATIC_BLOCKS = 0x40000,
+ TDEFL_FORCE_ALL_RAW_BLOCKS = 0x80000
+ };
+
+ /* High level compression functions: */
+ /* tdefl_compress_mem_to_heap() compresses a block in memory to a heap block allocated via malloc(). */
+ /* On entry: */
+ /* pSrc_buf, src_buf_len: Pointer and size of source block to compress. */
+ /* flags: The max match finder probes (default is 128) logically OR'd against the above flags. Higher probes are slower but improve compression. */
+ /* On return: */
+ /* Function returns a pointer to the compressed data, or NULL on failure. */
+ /* *pOut_len will be set to the compressed data's size, which could be larger than src_buf_len on uncompressible data. */
+ /* The caller must free() the returned block when it's no longer needed. */
+ MINIZ_EXPORT void *tdefl_compress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags);
+
+ /* tdefl_compress_mem_to_mem() compresses a block in memory to another block in memory. */
+ /* Returns 0 on failure. */
+ MINIZ_EXPORT size_t tdefl_compress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags);
+
+ /* Compresses an image to a compressed PNG file in memory. */
+ /* On entry: */
+ /* pImage, w, h, and num_chans describe the image to compress. num_chans may be 1, 2, 3, or 4. */
+ /* The image pitch in bytes per scanline will be w*num_chans. The leftmost pixel on the top scanline is stored first in memory. */
+ /* level may range from [0,10], use MZ_NO_COMPRESSION, MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc. or a decent default is MZ_DEFAULT_LEVEL */
+ /* If flip is true, the image will be flipped on the Y axis (useful for OpenGL apps). */
+ /* On return: */
+ /* Function returns a pointer to the compressed data, or NULL on failure. */
+ /* *pLen_out will be set to the size of the PNG image file. */
+ /* The caller must mz_free() the returned heap block (which will typically be larger than *pLen_out) when it's no longer needed. */
+ MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory_ex(const void *pImage, int w, int h, int num_chans, size_t *pLen_out, mz_uint level, mz_bool flip);
+ MINIZ_EXPORT void *tdefl_write_image_to_png_file_in_memory(const void *pImage, int w, int h, int num_chans, size_t *pLen_out);
+
+ /* Output stream interface. The compressor uses this interface to write compressed data. It'll typically be called TDEFL_OUT_BUF_SIZE at a time. */
+ typedef mz_bool (*tdefl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser);
+
+ /* tdefl_compress_mem_to_output() compresses a block to an output stream. The above helpers use this function internally. */
+ MINIZ_EXPORT mz_bool tdefl_compress_mem_to_output(const void *pBuf, size_t buf_len, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags);
+
+ enum
+ {
+ TDEFL_MAX_HUFF_TABLES = 3,
+ TDEFL_MAX_HUFF_SYMBOLS_0 = 288,
+ TDEFL_MAX_HUFF_SYMBOLS_1 = 32,
+ TDEFL_MAX_HUFF_SYMBOLS_2 = 19,
+ TDEFL_LZ_DICT_SIZE = 32768,
+ TDEFL_LZ_DICT_SIZE_MASK = TDEFL_LZ_DICT_SIZE - 1,
+ TDEFL_MIN_MATCH_LEN = 3,
+ TDEFL_MAX_MATCH_LEN = 258
+ };
+
+/* TDEFL_OUT_BUF_SIZE MUST be large enough to hold a single entire compressed output block (using static/fixed Huffman codes). */
+#if TDEFL_LESS_MEMORY
+ enum
+ {
+ TDEFL_LZ_CODE_BUF_SIZE = 24 * 1024,
+ TDEFL_OUT_BUF_SIZE = (TDEFL_LZ_CODE_BUF_SIZE * 13) / 10,
+ TDEFL_MAX_HUFF_SYMBOLS = 288,
+ TDEFL_LZ_HASH_BITS = 12,
+ TDEFL_LEVEL1_HASH_SIZE_MASK = 4095,
+ TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3,
+ TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS
+ };
+#else
+enum
+{
+ TDEFL_LZ_CODE_BUF_SIZE = 64 * 1024,
+ TDEFL_OUT_BUF_SIZE = (mz_uint)((TDEFL_LZ_CODE_BUF_SIZE * 13) / 10),
+ TDEFL_MAX_HUFF_SYMBOLS = 288,
+ TDEFL_LZ_HASH_BITS = 15,
+ TDEFL_LEVEL1_HASH_SIZE_MASK = 4095,
+ TDEFL_LZ_HASH_SHIFT = (TDEFL_LZ_HASH_BITS + 2) / 3,
+ TDEFL_LZ_HASH_SIZE = 1 << TDEFL_LZ_HASH_BITS
+};
+#endif
+
+ /* The low-level tdefl functions below may be used directly if the above helper functions aren't flexible enough. The low-level functions don't make any heap allocations, unlike the above helper functions. */
+ typedef enum
+ {
+ TDEFL_STATUS_BAD_PARAM = -2,
+ TDEFL_STATUS_PUT_BUF_FAILED = -1,
+ TDEFL_STATUS_OKAY = 0,
+ TDEFL_STATUS_DONE = 1
+ } tdefl_status;
+
+ /* Must map to MZ_NO_FLUSH, MZ_SYNC_FLUSH, etc. enums */
+ typedef enum
+ {
+ TDEFL_NO_FLUSH = 0,
+ TDEFL_SYNC_FLUSH = 2,
+ TDEFL_FULL_FLUSH = 3,
+ TDEFL_FINISH = 4
+ } tdefl_flush;
+
+ /* tdefl's compression state structure. */
+ typedef struct
+ {
+ tdefl_put_buf_func_ptr m_pPut_buf_func;
+ void *m_pPut_buf_user;
+ mz_uint m_flags, m_max_probes[2];
+ int m_greedy_parsing;
+ mz_uint m_adler32, m_lookahead_pos, m_lookahead_size, m_dict_size;
+ mz_uint8 *m_pLZ_code_buf, *m_pLZ_flags, *m_pOutput_buf, *m_pOutput_buf_end;
+ mz_uint m_num_flags_left, m_total_lz_bytes, m_lz_code_buf_dict_pos, m_bits_in, m_bit_buffer;
+ mz_uint m_saved_match_dist, m_saved_match_len, m_saved_lit, m_output_flush_ofs, m_output_flush_remaining, m_finished, m_block_index, m_wants_to_finish;
+ tdefl_status m_prev_return_status;
+ const void *m_pIn_buf;
+ void *m_pOut_buf;
+ size_t *m_pIn_buf_size, *m_pOut_buf_size;
+ tdefl_flush m_flush;
+ const mz_uint8 *m_pSrc;
+ size_t m_src_buf_left, m_out_buf_ofs;
+ mz_uint8 m_dict[TDEFL_LZ_DICT_SIZE + TDEFL_MAX_MATCH_LEN - 1];
+ mz_uint16 m_huff_count[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS];
+ mz_uint16 m_huff_codes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS];
+ mz_uint8 m_huff_code_sizes[TDEFL_MAX_HUFF_TABLES][TDEFL_MAX_HUFF_SYMBOLS];
+ mz_uint8 m_lz_code_buf[TDEFL_LZ_CODE_BUF_SIZE];
+ mz_uint16 m_next[TDEFL_LZ_DICT_SIZE];
+ mz_uint16 m_hash[TDEFL_LZ_HASH_SIZE];
+ mz_uint8 m_output_buf[TDEFL_OUT_BUF_SIZE];
+ } tdefl_compressor;
+
+ /* Initializes the compressor. */
+ /* There is no corresponding deinit() function because the tdefl API's do not dynamically allocate memory. */
+ /* pBut_buf_func: If NULL, output data will be supplied to the specified callback. In this case, the user should call the tdefl_compress_buffer() API for compression. */
+ /* If pBut_buf_func is NULL the user should always call the tdefl_compress() API. */
+ /* flags: See the above enums (TDEFL_HUFFMAN_ONLY, TDEFL_WRITE_ZLIB_HEADER, etc.) */
+ MINIZ_EXPORT tdefl_status tdefl_init(tdefl_compressor *d, tdefl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags);
+
+ /* Compresses a block of data, consuming as much of the specified input buffer as possible, and writing as much compressed data to the specified output buffer as possible. */
+ MINIZ_EXPORT tdefl_status tdefl_compress(tdefl_compressor *d, const void *pIn_buf, size_t *pIn_buf_size, void *pOut_buf, size_t *pOut_buf_size, tdefl_flush flush);
+
+ /* tdefl_compress_buffer() is only usable when the tdefl_init() is called with a non-NULL tdefl_put_buf_func_ptr. */
+ /* tdefl_compress_buffer() always consumes the entire input buffer. */
+ MINIZ_EXPORT tdefl_status tdefl_compress_buffer(tdefl_compressor *d, const void *pIn_buf, size_t in_buf_size, tdefl_flush flush);
+
+ MINIZ_EXPORT tdefl_status tdefl_get_prev_return_status(tdefl_compressor *d);
+ MINIZ_EXPORT mz_uint32 tdefl_get_adler32(tdefl_compressor *d);
+
+ /* Create tdefl_compress() flags given zlib-style compression parameters. */
+ /* level may range from [0,10] (where 10 is absolute max compression, but may be much slower on some files) */
+ /* window_bits may be -15 (raw deflate) or 15 (zlib) */
+ /* strategy may be either MZ_DEFAULT_STRATEGY, MZ_FILTERED, MZ_HUFFMAN_ONLY, MZ_RLE, or MZ_FIXED */
+ MINIZ_EXPORT mz_uint tdefl_create_comp_flags_from_zip_params(int level, int window_bits, int strategy);
+
+#ifndef MINIZ_NO_MALLOC
+ /* Allocate the tdefl_compressor structure in C so that */
+ /* non-C language bindings to tdefl_ API don't need to worry about */
+ /* structure size and allocation mechanism. */
+ MINIZ_EXPORT tdefl_compressor *tdefl_compressor_alloc(void);
+ MINIZ_EXPORT void tdefl_compressor_free(tdefl_compressor *pComp);
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*#ifndef MINIZ_NO_DEFLATE_APIS*/
+ #pragma once
+
+/* ------------------- Low-level Decompression API Definitions */
+
+#ifndef MINIZ_NO_INFLATE_APIS
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+ /* Decompression flags used by tinfl_decompress(). */
+ /* TINFL_FLAG_PARSE_ZLIB_HEADER: If set, the input has a valid zlib header and ends with an adler32 checksum (it's a valid zlib stream). Otherwise, the input is a raw deflate stream. */
+ /* TINFL_FLAG_HAS_MORE_INPUT: If set, there are more input bytes available beyond the end of the supplied input buffer. If clear, the input buffer contains all remaining input. */
+ /* TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF: If set, the output buffer is large enough to hold the entire decompressed stream. If clear, the output buffer is at least the size of the dictionary (typically 32KB). */
+ /* TINFL_FLAG_COMPUTE_ADLER32: Force adler-32 checksum computation of the decompressed bytes. */
+ enum
+ {
+ TINFL_FLAG_PARSE_ZLIB_HEADER = 1,
+ TINFL_FLAG_HAS_MORE_INPUT = 2,
+ TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF = 4,
+ TINFL_FLAG_COMPUTE_ADLER32 = 8
+ };
+
+ /* High level decompression functions: */
+ /* tinfl_decompress_mem_to_heap() decompresses a block in memory to a heap block allocated via malloc(). */
+ /* On entry: */
+ /* pSrc_buf, src_buf_len: Pointer and size of the Deflate or zlib source data to decompress. */
+ /* On return: */
+ /* Function returns a pointer to the decompressed data, or NULL on failure. */
+ /* *pOut_len will be set to the decompressed data's size, which could be larger than src_buf_len on uncompressible data. */
+ /* The caller must call mz_free() on the returned block when it's no longer needed. */
+ MINIZ_EXPORT void *tinfl_decompress_mem_to_heap(const void *pSrc_buf, size_t src_buf_len, size_t *pOut_len, int flags);
+
+/* tinfl_decompress_mem_to_mem() decompresses a block in memory to another block in memory. */
+/* Returns TINFL_DECOMPRESS_MEM_TO_MEM_FAILED on failure, or the number of bytes written on success. */
+#define TINFL_DECOMPRESS_MEM_TO_MEM_FAILED ((size_t)(-1))
+ MINIZ_EXPORT size_t tinfl_decompress_mem_to_mem(void *pOut_buf, size_t out_buf_len, const void *pSrc_buf, size_t src_buf_len, int flags);
+
+ /* tinfl_decompress_mem_to_callback() decompresses a block in memory to an internal 32KB buffer, and a user provided callback function will be called to flush the buffer. */
+ /* Returns 1 on success or 0 on failure. */
+ typedef int (*tinfl_put_buf_func_ptr)(const void *pBuf, int len, void *pUser);
+ MINIZ_EXPORT int tinfl_decompress_mem_to_callback(const void *pIn_buf, size_t *pIn_buf_size, tinfl_put_buf_func_ptr pPut_buf_func, void *pPut_buf_user, int flags);
+
+ struct tinfl_decompressor_tag;
+ typedef struct tinfl_decompressor_tag tinfl_decompressor;
+
+#ifndef MINIZ_NO_MALLOC
+ /* Allocate the tinfl_decompressor structure in C so that */
+ /* non-C language bindings to tinfl_ API don't need to worry about */
+ /* structure size and allocation mechanism. */
+ MINIZ_EXPORT tinfl_decompressor *tinfl_decompressor_alloc(void);
+ MINIZ_EXPORT void tinfl_decompressor_free(tinfl_decompressor *pDecomp);
+#endif
+
+/* Max size of LZ dictionary. */
+#define TINFL_LZ_DICT_SIZE 32768
+
+ /* Return status. */
+ typedef enum
+ {
+ /* This flags indicates the inflator needs 1 or more input bytes to make forward progress, but the caller is indicating that no more are available. The compressed data */
+ /* is probably corrupted. If you call the inflator again with more bytes it'll try to continue processing the input but this is a BAD sign (either the data is corrupted or you called it incorrectly). */
+ /* If you call it again with no input you'll just get TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS again. */
+ TINFL_STATUS_FAILED_CANNOT_MAKE_PROGRESS = -4,
+
+ /* This flag indicates that one or more of the input parameters was obviously bogus. (You can try calling it again, but if you get this error the calling code is wrong.) */
+ TINFL_STATUS_BAD_PARAM = -3,
+
+ /* This flags indicate the inflator is finished but the adler32 check of the uncompressed data didn't match. If you call it again it'll return TINFL_STATUS_DONE. */
+ TINFL_STATUS_ADLER32_MISMATCH = -2,
+
+ /* This flags indicate the inflator has somehow failed (bad code, corrupted input, etc.). If you call it again without resetting via tinfl_init() it it'll just keep on returning the same status failure code. */
+ TINFL_STATUS_FAILED = -1,
+
+ /* Any status code less than TINFL_STATUS_DONE must indicate a failure. */
+
+ /* This flag indicates the inflator has returned every byte of uncompressed data that it can, has consumed every byte that it needed, has successfully reached the end of the deflate stream, and */
+ /* if zlib headers and adler32 checking enabled that it has successfully checked the uncompressed data's adler32. If you call it again you'll just get TINFL_STATUS_DONE over and over again. */
+ TINFL_STATUS_DONE = 0,
+
+ /* This flag indicates the inflator MUST have more input data (even 1 byte) before it can make any more forward progress, or you need to clear the TINFL_FLAG_HAS_MORE_INPUT */
+ /* flag on the next call if you don't have any more source data. If the source data was somehow corrupted it's also possible (but unlikely) for the inflator to keep on demanding input to */
+ /* proceed, so be sure to properly set the TINFL_FLAG_HAS_MORE_INPUT flag. */
+ TINFL_STATUS_NEEDS_MORE_INPUT = 1,
+
+ /* This flag indicates the inflator definitely has 1 or more bytes of uncompressed data available, but it cannot write this data into the output buffer. */
+ /* Note if the source compressed data was corrupted it's possible for the inflator to return a lot of uncompressed data to the caller. I've been assuming you know how much uncompressed data to expect */
+ /* (either exact or worst case) and will stop calling the inflator and fail after receiving too much. In pure streaming scenarios where you have no idea how many bytes to expect this may not be possible */
+ /* so I may need to add some code to address this. */
+ TINFL_STATUS_HAS_MORE_OUTPUT = 2
+ } tinfl_status;
+
+/* Initializes the decompressor to its initial state. */
+#define tinfl_init(r) \
+ do \
+ { \
+ (r)->m_state = 0; \
+ } \
+ MZ_MACRO_END
+#define tinfl_get_adler32(r) (r)->m_check_adler32
+
+ /* Main low-level decompressor coroutine function. This is the only function actually needed for decompression. All the other functions are just high-level helpers for improved usability. */
+ /* This is a universal API, i.e. it can be used as a building block to build any desired higher level decompression API. In the limit case, it can be called once per every byte input or output. */
+ MINIZ_EXPORT tinfl_status tinfl_decompress(tinfl_decompressor *r, const mz_uint8 *pIn_buf_next, size_t *pIn_buf_size, mz_uint8 *pOut_buf_start, mz_uint8 *pOut_buf_next, size_t *pOut_buf_size, const mz_uint32 decomp_flags);
+
+ /* Internal/private bits follow. */
+ enum
+ {
+ TINFL_MAX_HUFF_TABLES = 3,
+ TINFL_MAX_HUFF_SYMBOLS_0 = 288,
+ TINFL_MAX_HUFF_SYMBOLS_1 = 32,
+ TINFL_MAX_HUFF_SYMBOLS_2 = 19,
+ TINFL_FAST_LOOKUP_BITS = 10,
+ TINFL_FAST_LOOKUP_SIZE = 1 << TINFL_FAST_LOOKUP_BITS
+ };
+
+#if MINIZ_HAS_64BIT_REGISTERS
+#define TINFL_USE_64BIT_BITBUF 1
+#else
+#define TINFL_USE_64BIT_BITBUF 0
+#endif
+
+#if TINFL_USE_64BIT_BITBUF
+ typedef mz_uint64 tinfl_bit_buf_t;
+#define TINFL_BITBUF_SIZE (64)
+#else
+typedef mz_uint32 tinfl_bit_buf_t;
+#define TINFL_BITBUF_SIZE (32)
+#endif
+
+ struct tinfl_decompressor_tag
+ {
+ mz_uint32 m_state, m_num_bits, m_zhdr0, m_zhdr1, m_z_adler32, m_final, m_type, m_check_adler32, m_dist, m_counter, m_num_extra, m_table_sizes[TINFL_MAX_HUFF_TABLES];
+ tinfl_bit_buf_t m_bit_buf;
+ size_t m_dist_from_out_buf_start;
+ mz_int16 m_look_up[TINFL_MAX_HUFF_TABLES][TINFL_FAST_LOOKUP_SIZE];
+ mz_int16 m_tree_0[TINFL_MAX_HUFF_SYMBOLS_0 * 2];
+ mz_int16 m_tree_1[TINFL_MAX_HUFF_SYMBOLS_1 * 2];
+ mz_int16 m_tree_2[TINFL_MAX_HUFF_SYMBOLS_2 * 2];
+ mz_uint8 m_code_size_0[TINFL_MAX_HUFF_SYMBOLS_0];
+ mz_uint8 m_code_size_1[TINFL_MAX_HUFF_SYMBOLS_1];
+ mz_uint8 m_code_size_2[TINFL_MAX_HUFF_SYMBOLS_2];
+ mz_uint8 m_raw_header[4], m_len_codes[TINFL_MAX_HUFF_SYMBOLS_0 + TINFL_MAX_HUFF_SYMBOLS_1 + 137];
+ };
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*#ifndef MINIZ_NO_INFLATE_APIS*/
+
+#pragma once
+
+
+/* ------------------- ZIP archive reading/writing */
+
+#ifndef MINIZ_NO_ARCHIVE_APIS
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+ enum
+ {
+ /* Note: These enums can be reduced as needed to save memory or stack space - they are pretty conservative. */
+ MZ_ZIP_MAX_IO_BUF_SIZE = 64 * 1024,
+ MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE = 512,
+ MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE = 512
+ };
+
+ typedef struct
+ {
+ /* Central directory file index. */
+ mz_uint32 m_file_index;
+
+ /* Byte offset of this entry in the archive's central directory. Note we currently only support up to UINT_MAX or less bytes in the central dir. */
+ mz_uint64 m_central_dir_ofs;
+
+ /* These fields are copied directly from the zip's central dir. */
+ mz_uint16 m_version_made_by;
+ mz_uint16 m_version_needed;
+ mz_uint16 m_bit_flag;
+ mz_uint16 m_method;
+
+ /* CRC-32 of uncompressed data. */
+ mz_uint32 m_crc32;
+
+ /* File's compressed size. */
+ mz_uint64 m_comp_size;
+
+ /* File's uncompressed size. Note, I've seen some old archives where directory entries had 512 bytes for their uncompressed sizes, but when you try to unpack them you actually get 0 bytes. */
+ mz_uint64 m_uncomp_size;
+
+ /* Zip internal and external file attributes. */
+ mz_uint16 m_internal_attr;
+ mz_uint32 m_external_attr;
+
+ /* Entry's local header file offset in bytes. */
+ mz_uint64 m_local_header_ofs;
+
+ /* Size of comment in bytes. */
+ mz_uint32 m_comment_size;
+
+ /* MZ_TRUE if the entry appears to be a directory. */
+ mz_bool m_is_directory;
+
+ /* MZ_TRUE if the entry uses encryption/strong encryption (which miniz_zip doesn't support) */
+ mz_bool m_is_encrypted;
+
+ /* MZ_TRUE if the file is not encrypted, a patch file, and if it uses a compression method we support. */
+ mz_bool m_is_supported;
+
+ /* Filename. If string ends in '/' it's a subdirectory entry. */
+ /* Guaranteed to be zero terminated, may be truncated to fit. */
+ char m_filename[MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE];
+
+ /* Comment field. */
+ /* Guaranteed to be zero terminated, may be truncated to fit. */
+ char m_comment[MZ_ZIP_MAX_ARCHIVE_FILE_COMMENT_SIZE];
+
+#ifdef MINIZ_NO_TIME
+ MZ_TIME_T m_padding;
+#else
+ MZ_TIME_T m_time;
+#endif
+ } mz_zip_archive_file_stat;
+
+ typedef size_t (*mz_file_read_func)(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n);
+ typedef size_t (*mz_file_write_func)(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n);
+ typedef mz_bool (*mz_file_needs_keepalive)(void *pOpaque);
+
+ struct mz_zip_internal_state_tag;
+ typedef struct mz_zip_internal_state_tag mz_zip_internal_state;
+
+ typedef enum
+ {
+ MZ_ZIP_MODE_INVALID = 0,
+ MZ_ZIP_MODE_READING = 1,
+ MZ_ZIP_MODE_WRITING = 2,
+ MZ_ZIP_MODE_WRITING_HAS_BEEN_FINALIZED = 3
+ } mz_zip_mode;
+
+ typedef enum
+ {
+ MZ_ZIP_FLAG_CASE_SENSITIVE = 0x0100,
+ MZ_ZIP_FLAG_IGNORE_PATH = 0x0200,
+ MZ_ZIP_FLAG_COMPRESSED_DATA = 0x0400,
+ MZ_ZIP_FLAG_DO_NOT_SORT_CENTRAL_DIRECTORY = 0x0800,
+ MZ_ZIP_FLAG_VALIDATE_LOCATE_FILE_FLAG = 0x1000, /* if enabled, mz_zip_reader_locate_file() will be called on each file as its validated to ensure the func finds the file in the central dir (intended for testing) */
+ MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY = 0x2000, /* validate the local headers, but don't decompress the entire file and check the crc32 */
+ MZ_ZIP_FLAG_WRITE_ZIP64 = 0x4000, /* always use the zip64 file format, instead of the original zip file format with automatic switch to zip64. Use as flags parameter with mz_zip_writer_init*_v2 */
+ MZ_ZIP_FLAG_WRITE_ALLOW_READING = 0x8000,
+ MZ_ZIP_FLAG_ASCII_FILENAME = 0x10000,
+ /*After adding a compressed file, seek back
+ to local file header and set the correct sizes*/
+ MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE = 0x20000,
+ MZ_ZIP_FLAG_READ_ALLOW_WRITING = 0x40000
+ } mz_zip_flags;
+
+ typedef enum
+ {
+ MZ_ZIP_TYPE_INVALID = 0,
+ MZ_ZIP_TYPE_USER,
+ MZ_ZIP_TYPE_MEMORY,
+ MZ_ZIP_TYPE_HEAP,
+ MZ_ZIP_TYPE_FILE,
+ MZ_ZIP_TYPE_CFILE,
+ MZ_ZIP_TOTAL_TYPES
+ } mz_zip_type;
+
+ /* miniz error codes. Be sure to update mz_zip_get_error_string() if you add or modify this enum. */
+ typedef enum
+ {
+ MZ_ZIP_NO_ERROR = 0,
+ MZ_ZIP_UNDEFINED_ERROR,
+ MZ_ZIP_TOO_MANY_FILES,
+ MZ_ZIP_FILE_TOO_LARGE,
+ MZ_ZIP_UNSUPPORTED_METHOD,
+ MZ_ZIP_UNSUPPORTED_ENCRYPTION,
+ MZ_ZIP_UNSUPPORTED_FEATURE,
+ MZ_ZIP_FAILED_FINDING_CENTRAL_DIR,
+ MZ_ZIP_NOT_AN_ARCHIVE,
+ MZ_ZIP_INVALID_HEADER_OR_CORRUPTED,
+ MZ_ZIP_UNSUPPORTED_MULTIDISK,
+ MZ_ZIP_DECOMPRESSION_FAILED,
+ MZ_ZIP_COMPRESSION_FAILED,
+ MZ_ZIP_UNEXPECTED_DECOMPRESSED_SIZE,
+ MZ_ZIP_CRC_CHECK_FAILED,
+ MZ_ZIP_UNSUPPORTED_CDIR_SIZE,
+ MZ_ZIP_ALLOC_FAILED,
+ MZ_ZIP_FILE_OPEN_FAILED,
+ MZ_ZIP_FILE_CREATE_FAILED,
+ MZ_ZIP_FILE_WRITE_FAILED,
+ MZ_ZIP_FILE_READ_FAILED,
+ MZ_ZIP_FILE_CLOSE_FAILED,
+ MZ_ZIP_FILE_SEEK_FAILED,
+ MZ_ZIP_FILE_STAT_FAILED,
+ MZ_ZIP_INVALID_PARAMETER,
+ MZ_ZIP_INVALID_FILENAME,
+ MZ_ZIP_BUF_TOO_SMALL,
+ MZ_ZIP_INTERNAL_ERROR,
+ MZ_ZIP_FILE_NOT_FOUND,
+ MZ_ZIP_ARCHIVE_TOO_LARGE,
+ MZ_ZIP_VALIDATION_FAILED,
+ MZ_ZIP_WRITE_CALLBACK_FAILED,
+ MZ_ZIP_TOTAL_ERRORS
+ } mz_zip_error;
+
+ typedef struct
+ {
+ mz_uint64 m_archive_size;
+ mz_uint64 m_central_directory_file_ofs;
+
+ /* We only support up to UINT32_MAX files in zip64 mode. */
+ mz_uint32 m_total_files;
+ mz_zip_mode m_zip_mode;
+ mz_zip_type m_zip_type;
+ mz_zip_error m_last_error;
+
+ mz_uint64 m_file_offset_alignment;
+
+ mz_alloc_func m_pAlloc;
+ mz_free_func m_pFree;
+ mz_realloc_func m_pRealloc;
+ void *m_pAlloc_opaque;
+
+ mz_file_read_func m_pRead;
+ mz_file_write_func m_pWrite;
+ mz_file_needs_keepalive m_pNeeds_keepalive;
+ void *m_pIO_opaque;
+
+ mz_zip_internal_state *m_pState;
+
+ } mz_zip_archive;
+
+ typedef struct
+ {
+ mz_zip_archive *pZip;
+ mz_uint flags;
+
+ int status;
+
+ mz_uint64 read_buf_size, read_buf_ofs, read_buf_avail, comp_remaining, out_buf_ofs, cur_file_ofs;
+ mz_zip_archive_file_stat file_stat;
+ void *pRead_buf;
+ void *pWrite_buf;
+
+ size_t out_blk_remain;
+
+ tinfl_decompressor inflator;
+
+#ifdef MINIZ_DISABLE_ZIP_READER_CRC32_CHECKS
+ mz_uint padding;
+#else
+ mz_uint file_crc32;
+#endif
+
+ } mz_zip_reader_extract_iter_state;
+
+ /* -------- ZIP reading */
+
+ /* Inits a ZIP archive reader. */
+ /* These functions read and validate the archive's central directory. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_init(mz_zip_archive *pZip, mz_uint64 size, mz_uint flags);
+
+ MINIZ_EXPORT mz_bool mz_zip_reader_init_mem(mz_zip_archive *pZip, const void *pMem, size_t size, mz_uint flags);
+
+#ifndef MINIZ_NO_STDIO
+ /* Read a archive from a disk file. */
+ /* file_start_ofs is the file offset where the archive actually begins, or 0. */
+ /* actual_archive_size is the true total size of the archive, which may be smaller than the file's actual size on disk. If zero the entire file is treated as the archive. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint32 flags);
+ MINIZ_EXPORT mz_bool mz_zip_reader_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags, mz_uint64 file_start_ofs, mz_uint64 archive_size);
+
+ /* Read an archive from an already opened FILE, beginning at the current file position. */
+ /* The archive is assumed to be archive_size bytes long. If archive_size is 0, then the entire rest of the file is assumed to contain the archive. */
+ /* The FILE will NOT be closed when mz_zip_reader_end() is called. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint64 archive_size, mz_uint flags);
+#endif
+
+ /* Ends archive reading, freeing all allocations, and closing the input archive file if mz_zip_reader_init_file() was used. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_end(mz_zip_archive *pZip);
+
+ /* -------- ZIP reading or writing */
+
+ /* Clears a mz_zip_archive struct to all zeros. */
+ /* Important: This must be done before passing the struct to any mz_zip functions. */
+ MINIZ_EXPORT void mz_zip_zero_struct(mz_zip_archive *pZip);
+
+ MINIZ_EXPORT mz_zip_mode mz_zip_get_mode(mz_zip_archive *pZip);
+ MINIZ_EXPORT mz_zip_type mz_zip_get_type(mz_zip_archive *pZip);
+
+ /* Returns the total number of files in the archive. */
+ MINIZ_EXPORT mz_uint mz_zip_reader_get_num_files(mz_zip_archive *pZip);
+
+ MINIZ_EXPORT mz_uint64 mz_zip_get_archive_size(mz_zip_archive *pZip);
+ MINIZ_EXPORT mz_uint64 mz_zip_get_archive_file_start_offset(mz_zip_archive *pZip);
+ MINIZ_EXPORT MZ_FILE *mz_zip_get_cfile(mz_zip_archive *pZip);
+
+ /* Reads n bytes of raw archive data, starting at file offset file_ofs, to pBuf. */
+ MINIZ_EXPORT size_t mz_zip_read_archive_data(mz_zip_archive *pZip, mz_uint64 file_ofs, void *pBuf, size_t n);
+
+ /* All mz_zip funcs set the m_last_error field in the mz_zip_archive struct. These functions retrieve/manipulate this field. */
+ /* Note that the m_last_error functionality is not thread safe. */
+ MINIZ_EXPORT mz_zip_error mz_zip_set_last_error(mz_zip_archive *pZip, mz_zip_error err_num);
+ MINIZ_EXPORT mz_zip_error mz_zip_peek_last_error(mz_zip_archive *pZip);
+ MINIZ_EXPORT mz_zip_error mz_zip_clear_last_error(mz_zip_archive *pZip);
+ MINIZ_EXPORT mz_zip_error mz_zip_get_last_error(mz_zip_archive *pZip);
+ MINIZ_EXPORT const char *mz_zip_get_error_string(mz_zip_error mz_err);
+
+ /* MZ_TRUE if the archive file entry is a directory entry. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_is_file_a_directory(mz_zip_archive *pZip, mz_uint file_index);
+
+ /* MZ_TRUE if the file is encrypted/strong encrypted. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_is_file_encrypted(mz_zip_archive *pZip, mz_uint file_index);
+
+ /* MZ_TRUE if the compression method is supported, and the file is not encrypted, and the file is not a compressed patch file. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_is_file_supported(mz_zip_archive *pZip, mz_uint file_index);
+
+ /* Retrieves the filename of an archive file entry. */
+ /* Returns the number of bytes written to pFilename, or if filename_buf_size is 0 this function returns the number of bytes needed to fully store the filename. */
+ MINIZ_EXPORT mz_uint mz_zip_reader_get_filename(mz_zip_archive *pZip, mz_uint file_index, char *pFilename, mz_uint filename_buf_size);
+
+ /* Attempts to locates a file in the archive's central directory. */
+ /* Valid flags: MZ_ZIP_FLAG_CASE_SENSITIVE, MZ_ZIP_FLAG_IGNORE_PATH */
+ /* Returns -1 if the file cannot be found. */
+ MINIZ_EXPORT int mz_zip_reader_locate_file(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags);
+ MINIZ_EXPORT mz_bool mz_zip_reader_locate_file_v2(mz_zip_archive *pZip, const char *pName, const char *pComment, mz_uint flags, mz_uint32 *file_index);
+
+ /* Returns detailed information about an archive file entry. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_file_stat(mz_zip_archive *pZip, mz_uint file_index, mz_zip_archive_file_stat *pStat);
+
+ /* MZ_TRUE if the file is in zip64 format. */
+ /* A file is considered zip64 if it contained a zip64 end of central directory marker, or if it contained any zip64 extended file information fields in the central directory. */
+ MINIZ_EXPORT mz_bool mz_zip_is_zip64(mz_zip_archive *pZip);
+
+ /* Returns the total central directory size in bytes. */
+ /* The current max supported size is <= MZ_UINT32_MAX. */
+ MINIZ_EXPORT size_t mz_zip_get_central_dir_size(mz_zip_archive *pZip);
+
+ /* Extracts a archive file to a memory buffer using no memory allocation. */
+ /* There must be at least enough room on the stack to store the inflator's state (~34KB or so). */
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem_no_alloc(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size);
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem_no_alloc(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags, void *pUser_read_buf, size_t user_read_buf_size);
+
+ /* Extracts a archive file to a memory buffer. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_mem(mz_zip_archive *pZip, mz_uint file_index, void *pBuf, size_t buf_size, mz_uint flags);
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_mem(mz_zip_archive *pZip, const char *pFilename, void *pBuf, size_t buf_size, mz_uint flags);
+
+ /* Extracts a archive file to a dynamically allocated heap buffer. */
+ /* The memory will be allocated via the mz_zip_archive's alloc/realloc functions. */
+ /* Returns NULL and sets the last error on failure. */
+ MINIZ_EXPORT void *mz_zip_reader_extract_to_heap(mz_zip_archive *pZip, mz_uint file_index, size_t *pSize, mz_uint flags);
+ MINIZ_EXPORT void *mz_zip_reader_extract_file_to_heap(mz_zip_archive *pZip, const char *pFilename, size_t *pSize, mz_uint flags);
+
+ /* Extracts a archive file using a callback function to output the file's data. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_callback(mz_zip_archive *pZip, mz_uint file_index, mz_file_write_func pCallback, void *pOpaque, mz_uint flags);
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_callback(mz_zip_archive *pZip, const char *pFilename, mz_file_write_func pCallback, void *pOpaque, mz_uint flags);
+
+ /* Extract a file iteratively */
+ MINIZ_EXPORT mz_zip_reader_extract_iter_state *mz_zip_reader_extract_iter_new(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags);
+ MINIZ_EXPORT mz_zip_reader_extract_iter_state *mz_zip_reader_extract_file_iter_new(mz_zip_archive *pZip, const char *pFilename, mz_uint flags);
+ MINIZ_EXPORT size_t mz_zip_reader_extract_iter_read(mz_zip_reader_extract_iter_state *pState, void *pvBuf, size_t buf_size);
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_iter_free(mz_zip_reader_extract_iter_state *pState);
+
+#ifndef MINIZ_NO_STDIO
+ /* Extracts a archive file to a disk file and sets its last accessed and modified times. */
+ /* This function only extracts files, not archive directory records. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_file(mz_zip_archive *pZip, mz_uint file_index, const char *pDst_filename, mz_uint flags);
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_file(mz_zip_archive *pZip, const char *pArchive_filename, const char *pDst_filename, mz_uint flags);
+
+ /* Extracts a archive file starting at the current position in the destination FILE stream. */
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_to_cfile(mz_zip_archive *pZip, mz_uint file_index, MZ_FILE *File, mz_uint flags);
+ MINIZ_EXPORT mz_bool mz_zip_reader_extract_file_to_cfile(mz_zip_archive *pZip, const char *pArchive_filename, MZ_FILE *pFile, mz_uint flags);
+#endif
+
+#if 0
+/* TODO */
+ typedef void *mz_zip_streaming_extract_state_ptr;
+ mz_zip_streaming_extract_state_ptr mz_zip_streaming_extract_begin(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags);
+ mz_uint64 mz_zip_streaming_extract_get_size(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState);
+ mz_uint64 mz_zip_streaming_extract_get_cur_ofs(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState);
+ mz_bool mz_zip_streaming_extract_seek(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, mz_uint64 new_ofs);
+ size_t mz_zip_streaming_extract_read(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState, void *pBuf, size_t buf_size);
+ mz_bool mz_zip_streaming_extract_end(mz_zip_archive *pZip, mz_zip_streaming_extract_state_ptr pState);
+#endif
+
+ /* This function compares the archive's local headers, the optional local zip64 extended information block, and the optional descriptor following the compressed data vs. the data in the central directory. */
+ /* It also validates that each file can be successfully uncompressed unless the MZ_ZIP_FLAG_VALIDATE_HEADERS_ONLY is specified. */
+ MINIZ_EXPORT mz_bool mz_zip_validate_file(mz_zip_archive *pZip, mz_uint file_index, mz_uint flags);
+
+ /* Validates an entire archive by calling mz_zip_validate_file() on each file. */
+ MINIZ_EXPORT mz_bool mz_zip_validate_archive(mz_zip_archive *pZip, mz_uint flags);
+
+ /* Misc utils/helpers, valid for ZIP reading or writing */
+ MINIZ_EXPORT mz_bool mz_zip_validate_mem_archive(const void *pMem, size_t size, mz_uint flags, mz_zip_error *pErr);
+#ifndef MINIZ_NO_STDIO
+ MINIZ_EXPORT mz_bool mz_zip_validate_file_archive(const char *pFilename, mz_uint flags, mz_zip_error *pErr);
+#endif
+
+ /* Universal end function - calls either mz_zip_reader_end() or mz_zip_writer_end(). */
+ MINIZ_EXPORT mz_bool mz_zip_end(mz_zip_archive *pZip);
+
+ /* -------- ZIP writing */
+
+#ifndef MINIZ_NO_ARCHIVE_WRITING_APIS
+
+ /* Inits a ZIP archive writer. */
+ /*Set pZip->m_pWrite (and pZip->m_pIO_opaque) before calling mz_zip_writer_init or mz_zip_writer_init_v2*/
+ /*The output is streamable, i.e. file_ofs in mz_file_write_func always increases only by n*/
+ MINIZ_EXPORT mz_bool mz_zip_writer_init(mz_zip_archive *pZip, mz_uint64 existing_size);
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_v2(mz_zip_archive *pZip, mz_uint64 existing_size, mz_uint flags);
+
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_heap(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size);
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_heap_v2(mz_zip_archive *pZip, size_t size_to_reserve_at_beginning, size_t initial_allocation_size, mz_uint flags);
+
+#ifndef MINIZ_NO_STDIO
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_file(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning);
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_file_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint64 size_to_reserve_at_beginning, mz_uint flags);
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_cfile(mz_zip_archive *pZip, MZ_FILE *pFile, mz_uint flags);
+#endif
+
+ /* Converts a ZIP archive reader object into a writer object, to allow efficient in-place file appends to occur on an existing archive. */
+ /* For archives opened using mz_zip_reader_init_file, pFilename must be the archive's filename so it can be reopened for writing. If the file can't be reopened, mz_zip_reader_end() will be called. */
+ /* For archives opened using mz_zip_reader_init_mem, the memory block must be growable using the realloc callback (which defaults to realloc unless you've overridden it). */
+ /* Finally, for archives opened using mz_zip_reader_init, the mz_zip_archive's user provided m_pWrite function cannot be NULL. */
+ /* Note: In-place archive modification is not recommended unless you know what you're doing, because if execution stops or something goes wrong before */
+ /* the archive is finalized the file's central directory will be hosed. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader(mz_zip_archive *pZip, const char *pFilename);
+ MINIZ_EXPORT mz_bool mz_zip_writer_init_from_reader_v2(mz_zip_archive *pZip, const char *pFilename, mz_uint flags);
+
+ /* Adds the contents of a memory buffer to an archive. These functions record the current local time into the archive. */
+ /* To add a directory entry, call this method with an archive name ending in a forwardslash with an empty buffer. */
+ /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_add_mem(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, mz_uint level_and_flags);
+
+ /* Like mz_zip_writer_add_mem(), except you can specify a file comment field, and optionally supply the function with already compressed data. */
+ /* uncomp_size/uncomp_crc32 are only used if the MZ_ZIP_FLAG_COMPRESSED_DATA flag is specified. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags,
+ mz_uint64 uncomp_size, mz_uint32 uncomp_crc32);
+
+ MINIZ_EXPORT mz_bool mz_zip_writer_add_mem_ex_v2(mz_zip_archive *pZip, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags,
+ mz_uint64 uncomp_size, mz_uint32 uncomp_crc32, MZ_TIME_T *last_modified, const char *user_extra_data_local, mz_uint user_extra_data_local_len,
+ const char *user_extra_data_central, mz_uint user_extra_data_central_len);
+
+ /* Adds the contents of a file to an archive. This function also records the disk file's modified time into the archive. */
+ /* File data is supplied via a read callback function. User mz_zip_writer_add_(c)file to add a file directly.*/
+ MINIZ_EXPORT mz_bool mz_zip_writer_add_read_buf_callback(mz_zip_archive *pZip, const char *pArchive_name, mz_file_read_func read_callback, void *callback_opaque, mz_uint64 max_size,
+ const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len,
+ const char *user_extra_data_central, mz_uint user_extra_data_central_len);
+
+#ifndef MINIZ_NO_STDIO
+ /* Adds the contents of a disk file to an archive. This function also records the disk file's modified time into the archive. */
+ /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_add_file(mz_zip_archive *pZip, const char *pArchive_name, const char *pSrc_filename, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags);
+
+ /* Like mz_zip_writer_add_file(), except the file data is read from the specified FILE stream. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_add_cfile(mz_zip_archive *pZip, const char *pArchive_name, MZ_FILE *pSrc_file, mz_uint64 max_size,
+ const MZ_TIME_T *pFile_time, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, const char *user_extra_data_local, mz_uint user_extra_data_local_len,
+ const char *user_extra_data_central, mz_uint user_extra_data_central_len);
+#endif
+
+ /* Adds a file to an archive by fully cloning the data from another archive. */
+ /* This function fully clones the source file's compressed data (no recompression), along with its full filename, extra data (it may add or modify the zip64 local header extra data field), and the optional descriptor following the compressed data. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_add_from_zip_reader(mz_zip_archive *pZip, mz_zip_archive *pSource_zip, mz_uint src_file_index);
+
+ /* Finalizes the archive by writing the central directory records followed by the end of central directory record. */
+ /* After an archive is finalized, the only valid call on the mz_zip_archive struct is mz_zip_writer_end(). */
+ /* An archive must be manually finalized by calling this function for it to be valid. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_finalize_archive(mz_zip_archive *pZip);
+
+ /* Finalizes a heap archive, returning a pointer to the heap block and its size. */
+ /* The heap block will be allocated using the mz_zip_archive's alloc/realloc callbacks. */
+ MINIZ_EXPORT mz_bool mz_zip_writer_finalize_heap_archive(mz_zip_archive *pZip, void **ppBuf, size_t *pSize);
+
+ /* Ends archive writing, freeing all allocations, and closing the output file if mz_zip_writer_init_file() was used. */
+ /* Note for the archive to be valid, it *must* have been finalized before ending (this function will not do it for you). */
+ MINIZ_EXPORT mz_bool mz_zip_writer_end(mz_zip_archive *pZip);
+
+ /* -------- Misc. high-level helper functions: */
+
+ /* mz_zip_add_mem_to_archive_file_in_place() efficiently (but not atomically) appends a memory blob to a ZIP archive. */
+ /* Note this is NOT a fully safe operation. If it crashes or dies in some way your archive can be left in a screwed up state (without a central directory). */
+ /* level_and_flags - compression level (0-10, see MZ_BEST_SPEED, MZ_BEST_COMPRESSION, etc.) logically OR'd with zero or more mz_zip_flags, or just set to MZ_DEFAULT_COMPRESSION. */
+ /* TODO: Perhaps add an option to leave the existing central dir in place in case the add dies? We could then truncate the file (so the old central dir would be at the end) if something goes wrong. */
+ MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags);
+ MINIZ_EXPORT mz_bool mz_zip_add_mem_to_archive_file_in_place_v2(const char *pZip_filename, const char *pArchive_name, const void *pBuf, size_t buf_size, const void *pComment, mz_uint16 comment_size, mz_uint level_and_flags, mz_zip_error *pErr);
+
+#ifndef MINIZ_NO_STDIO
+ /* Reads a single file from an archive into a heap block. */
+ /* If pComment is not NULL, only the file with the specified comment will be extracted. */
+ /* Returns NULL on failure. */
+ MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap(const char *pZip_filename, const char *pArchive_name, size_t *pSize, mz_uint flags);
+ MINIZ_EXPORT void *mz_zip_extract_archive_file_to_heap_v2(const char *pZip_filename, const char *pArchive_name, const char *pComment, size_t *pSize, mz_uint flags, mz_zip_error *pErr);
+#endif
+
+#endif /* #ifndef MINIZ_NO_ARCHIVE_WRITING_APIS */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MINIZ_NO_ARCHIVE_APIS */
diff --git a/src/libs/kpunch/audio/Makefile b/src/libs/kpunch/audio/Makefile
new file mode 100644
index 00000000..6dc26bc8
--- /dev/null
+++ b/src/libs/kpunch/audio/Makefile
@@ -0,0 +1,80 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# Sound Library Makefile for DJGPP cross-compilation
+#
+# audio.lib (see audio.h) and its sound drivers: every drivers//
+# directory is one driver, built from all its sources into
+# drivers/.drv beside the library, where audio.lib finds it.
+#
+# The mixer and the drivers run code at interrupt time from pinned
+# memory (audioLockSpan pins from a module's first listed function to the
+# end of its data), so gcc must not split functions into hot and cold
+# parts placed elsewhere, nor turn loops into memset/memcpy calls.
+
+include ../../../common.mk
+
+ISR_CFLAGS = -fno-reorder-blocks-and-partition -fno-tree-loop-distribute-patterns
+CFLAGS = $(DOS_CFLAGS) $(ISR_CFLAGS) -I. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../libtasks -I../dvxshell
+
+OBJDIR = ../../../../obj/audio
+LIBSDIR = ../../../../bin/libs
+
+SRCS = $(wildcard *.c)
+OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
+TARGETDIR = $(LIBSDIR)/kpunch/audio
+TARGET = $(TARGETDIR)/audio.lib
+
+DRVDIR = $(TARGETDIR)/drivers
+DRV_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/*/*.c))
+DRV_TARGETS = $(patsubst %,$(DRVDIR)/%.drv,$(DVX_DRIVERS))
+
+.PHONY: all clean
+
+all: $(TARGET) $(TARGETDIR)/audio.dep $(DRV_TARGETS)
+
+$(TARGETDIR)/audio.dep: audio.dep | $(TARGETDIR)
+ sed 's/$$/\r/' $< > $@
+
+$(TARGET): $(OBJS) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/audio.dxe -U $(OBJS)
+ mv $(TARGETDIR)/audio.dxe $@
+
+$(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d)))
+
+$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(OBJDIR):
+ mkdir -p $(OBJDIR)
+
+$(TARGETDIR):
+ mkdir -p $(TARGETDIR)
+
+$(DRVDIR):
+ mkdir -p $(DRVDIR)
+
+-include $(OBJS:.o=.d) $(DRV_OBJS:.o=.d)
+
+clean:
+ rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/audio/audio.c b/src/libs/kpunch/audio/audio.c
new file mode 100644
index 00000000..1de35319
--- /dev/null
+++ b/src/libs/kpunch/audio/audio.c
@@ -0,0 +1,1761 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audio.c -- contexts, voices, samples, streams and the sound device
+//
+// See audio.h. Everything here runs at program time; the mixer
+// (audioMix.c) runs at interrupt time and reads the voices through
+// audioMixer. A voice is started by filling its mixer slot while the
+// slot is free and marking it playing last; it is stopped by marking it
+// ended. Ended slots are reclaimed at program time (reclaimSlots): their
+// sample reference is dropped and, when the owner wants to know, an end
+// notification is queued for audioPump, so a slot is reusable at once
+// and callbacks only ever run from audioPump.
+//
+// The device is opened when the first context opens and closed with the
+// last one. In between, its output is closed after IDLE_CLOSE_US without
+// a voice (a desktop with sound effects would otherwise mix silence all
+// the time) and opened again, with the same driver, when a voice starts. Drivers are the driver modules in AUDIO_DRV_DIR (dvxDrv.h);
+// they are loaded, sorted by priority and tried in turn (DVX.INI [audio]
+// driver first), and all but the one that starts are unloaded again. Synth
+// drivers (an FM chip, a MIDI port) come from the same folder: the first
+// of each kind that finds its hardware is kept beside the device.
+
+#include "audioInt.h"
+#include "stb_ds.h"
+
+#include
+#include
+#include
+#include
+
+// ABI stamp checked by the loader (see DVX_ABI_VERSION).
+DVX_ABI_STAMP(audio);
+
+// Voice ids: generation * VOICE_ID_STRIDE + slot + 1, so a stale id never
+// names the slot's next voice.
+#define VOICE_ID_STRIDE 256
+#define VOICE_GEN_MAX (INT32_MAX / VOICE_ID_STRIDE - 1)
+
+// DVX.INI [audio] keys and defaults
+#define INI_RATE "rate"
+#define INI_BITS "bits"
+#define INI_CHANNELS "channels"
+#define INI_VOICES "voices"
+#define INI_FM_TONES "fmTones"
+#define PERCENT_MAX 100
+
+#define ERROR_TEXT_UNKNOWN "Unknown error"
+
+// Frames a pull stream is asked for at a time
+#define STREAM_FILL_FRAMES 1024
+
+#define MS_PER_SEC 1000
+#define BITS_PER_BYTE 8
+#define BITS_8 8
+#define BITS_16 16
+#define CHANNELS_MAX 2
+#define PHASE_SHIFT 32
+
+#define IDLE_CLOSE_US ((uint64_t)AUDIO_IDLE_CLOSE_SEC * PLATFORM_US_PER_SEC)
+
+// The two kinds of driver module in AUDIO_DRV_DIR
+#define DRV_KIND_SOUND 0
+#define DRV_KIND_SYNTH 1
+#define DRV_KINDS 2
+
+struct AudioCtxT {
+ AudioCtxT *next;
+ AudioEndFnT endFn;
+ void *endData;
+ int32_t appId; // program that opened it
+ int32_t volume;
+ bool basic; // opened for a BASIC program (audioBasicContext)
+};
+
+struct AudioNotesT {
+ MixNoteRingT ring; // first: the whole struct is pinned for the mixer
+ MixNoteT data[AUDIO_NOTES_MAX];
+ AudioNotesT *next;
+ AudioCtxT *owner;
+ uint64_t usQueued; // time queued since the queue last went quiet ...
+ uint64_t framesQueued; // ... and the frames it was rounded to
+ int32_t voice;
+};
+
+struct AudioSampleT {
+ AudioSampleT *next;
+ AudioCtxT *owner;
+ int16_t *pcm; // pinned
+ uint32_t frames;
+ AudioFormatT fmt; // as recorded (bits of the source)
+ int32_t users; // voices playing it
+ bool autoFree; // audioPlayFile: freed when its voice ends
+};
+
+struct AudioStreamT {
+ AudioRingT ring; // first: the whole struct is pinned for the mixer
+ AudioStreamT *next;
+ AudioCtxT *owner;
+ AudioFillFnT fill;
+ void *fillData;
+ uint8_t *scratch; // STREAM_FILL_FRAMES frames for the fill function
+ AudioFormatT fmt;
+ int32_t voice;
+};
+
+// A voice as the program side sees it.
+typedef struct {
+ AudioCtxT *owner;
+ AudioSampleT *sample;
+ AudioStreamT *stream;
+ AudioNotesT *notes;
+ uint32_t rate; // source rate (samples, streams)
+ uint32_t hz; // tone frequency
+ int32_t id;
+ int32_t fmVoice; // tones on the FM chip: its FM voice id (0: none)
+ int32_t volume;
+ int32_t pan;
+ int32_t pitch;
+} VoiceInfoT;
+
+// An end notification waiting for audioPump.
+typedef struct {
+ AudioCtxT *ctx;
+ int32_t voice;
+ AudioEndE reason;
+} EndNoteT;
+
+// The device in use, and the synthesizers beside it.
+typedef struct {
+ DvxDriverModT mod; // desc NULL: none
+ AudioDrvInfoT info;
+ DvxDriverModT synths[AUDIO_SYNTH_KINDS]; // desc NULL: none of that kind
+ uint32_t voiceLimit;
+ uint64_t quietSince; // platformMicros when the last voice ended
+ bool quiet; // no voice since quietSince
+ bool suspended; // output closed for silence (the driver stays chosen)
+ bool fmTones; // square / triangle tones on FM voices
+} AudioDevT;
+
+static const char *sErrorText[AUDIO_ERR_COUNT] = {
+ "No error",
+ "Invalid argument",
+ "Not enough memory",
+ "The file could not be read",
+ "Not a WAV or VOC sound file",
+ "Unsupported sound format (only uncompressed 8- and 16-bit sound can be played)",
+ "The sound file is damaged or incomplete",
+ "Too many sounds are playing",
+ "No sound device could be started",
+ "No such sound is playing",
+ "The synthesizer is being used by another program",
+ "There is no such synthesizer"
+};
+
+static AudioDevT sDev;
+static AudioCtxT *sCtxs = NULL;
+static AudioSampleT *sSamples = NULL;
+static AudioStreamT *sStreams = NULL;
+static AudioNotesT *sNoteQueues = NULL;
+static EndNoteT *sNotes = NULL;
+static VoiceInfoT sVoices[AUDIO_VOICES_MAX];
+static int32_t sMaster = AUDIO_VOLUME_MAX;
+static uint32_t sGeneration = 0;
+static uint32_t sSeq = 0;
+static uint32_t sDropped = 0;
+static bool sPumping = false;
+static bool sStreamsDirty = false;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t appCurrent(void);
+void audioAppEnded(int32_t appId);
+int32_t audioAppEnter(int32_t appId);
+AudioCtxT *audioBasicContext(void);
+void audioClose(AudioCtxT *ctx);
+int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max);
+const char *audioErrorText(int32_t err);
+void audioGetInfo(AudioInfoT *info);
+int32_t audioGetMasterVolume(void);
+void audioGetStats(AudioStatsT *stats);
+int32_t audioGetVolume(const AudioCtxT *ctx);
+void audioIdle(void *unused);
+int32_t audioNotesAdd(AudioNotesT *notes, uint32_t milliHz, uint32_t us);
+void audioNotesClear(AudioNotesT *notes);
+void audioNotesClose(AudioNotesT *notes);
+int32_t audioNotesOpen(AudioCtxT *ctx, AudioWaveE wave, int32_t volume, AudioNotesT **out);
+uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones);
+uint32_t audioNotesSpace(const AudioNotesT *notes);
+int32_t audioNotesVoice(const AudioNotesT *notes);
+AudioCtxT *audioOpen(void);
+int32_t audioPlay(AudioSampleT *sample, int32_t volume, int32_t pan, bool loop);
+int32_t audioPlayFile(AudioCtxT *ctx, const char *path, int32_t volume, int32_t pan, bool loop);
+void audioPump(void);
+int32_t audioSampleCreate(AudioCtxT *ctx, const void *pcm, uint32_t frames, const AudioFormatT *fmt, AudioSampleT **out);
+void audioSampleFree(AudioSampleT *sample);
+void audioSampleInfo(const AudioSampleT *sample, AudioFormatT *fmt, uint32_t *frames);
+int32_t audioSampleLoad(AudioCtxT *ctx, const char *path, AudioSampleT **out);
+int32_t audioSampleLoadMem(AudioCtxT *ctx, const void *data, uint32_t size, AudioSampleT **out);
+void audioSetEndFn(AudioCtxT *ctx, AudioEndFnT fn, void *userData);
+void audioSetMasterVolume(int32_t volume);
+void audioSetVolume(AudioCtxT *ctx, int32_t volume);
+void audioShutdown(void);
+void audioStopAll(AudioCtxT *ctx);
+void audioStreamClose(AudioStreamT *stream);
+int32_t audioStreamOpen(AudioCtxT *ctx, const AudioFormatT *fmt, uint32_t bufferMs, AudioFillFnT fill, void *userData, AudioStreamT **out);
+uint32_t audioStreamQueued(const AudioStreamT *stream);
+uint32_t audioStreamSpace(const AudioStreamT *stream);
+int32_t audioStreamVoice(const AudioStreamT *stream);
+uint32_t audioStreamWrite(AudioStreamT *stream, const void *pcm, uint32_t frames);
+int32_t audioTone(AudioCtxT *ctx, AudioWaveE wave, uint32_t hz, int32_t volume, uint32_t ms);
+bool audioVoiceIsPlaying(const AudioCtxT *ctx, int32_t voice);
+int32_t audioVoiceSetFrequency(AudioCtxT *ctx, int32_t voice, uint32_t hz);
+int32_t audioVoiceSetPan(AudioCtxT *ctx, int32_t voice, int32_t pan);
+int32_t audioVoiceSetPitch(AudioCtxT *ctx, int32_t voice, int32_t pitch);
+int32_t audioVoiceSetVolume(AudioCtxT *ctx, int32_t voice, int32_t volume);
+int32_t audioVoiceStop(AudioCtxT *ctx, int32_t voice);
+static int32_t clampInt(int32_t v, int32_t lo, int32_t hi);
+static void deliverNotes(void);
+static void devClose(void);
+static void devCloseSynths(void);
+static void devConfig(PrefsHandleT *prefs, AudioDrvConfigT *cfg);
+static void devEmergencyStop(void);
+static void devIdle(void);
+static bool devOpen(void);
+static void devOpenSynths(DvxDriverModT *mods, const AudioDrvConfigT *cfg);
+static bool devResume(void);
+static bool devTry(DvxDriverModT *mods, int32_t index, const AudioDrvConfigT *cfg);
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind);
+static void fmToneRefresh(int32_t slot);
+static bool formatValid(const AudioFormatT *fmt);
+static bool infoUsable(const AudioDrvInfoT *info);
+static void gainsUpdate(int32_t slot);
+static int32_t lookupVoice(const AudioCtxT *ctx, int32_t voice);
+static int32_t makeSample(AudioCtxT *ctx, int16_t *pcm, uint32_t frames, const AudioFormatT *fmt, AudioSampleT **out);
+static void reclaimSlots(void);
+static void releaseSlot(int32_t slot);
+static const AudioDriverT *soundOf(const DvxDriverModT *mod);
+static void stepUpdate(int32_t slot);
+static void streamFill(AudioStreamT *s);
+static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod);
+static int32_t voiceBegin(AudioCtxT *owner, int32_t volume, int32_t pan, uint8_t kind);
+static int32_t voiceGo(int32_t slot);
+
+// What audio.lib looks for in AUDIO_DRV_DIR
+static const char *const sDrvSymbols[DRV_KINDS] = { DVX_SYM(AUDIO_DRV_SYMBOL), DVX_SYM(AUDIO_SYNTH_SYMBOL) };
+static const uint32_t sDrvSizes[DRV_KINDS] = { sizeof(AudioDriverT), sizeof(AudioSynthDriverT) };
+static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, DRV_KINDS, driverValid };
+
+
+// The program running now, as the memory tracker and the shell see it
+// (0 without a shell: host tests).
+static int32_t appCurrent(void) {
+ return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
+}
+
+
+void audioAppEnded(int32_t appId) {
+ AudioCtxT *ctx = sCtxs;
+
+ while (ctx) {
+ AudioCtxT *next = ctx->next;
+
+ if (ctx->appId == appId) {
+ audioClose(ctx);
+ }
+
+ ctx = next;
+ }
+}
+
+
+// Make appId the current program and return the one before (for calling
+// a program's callback as that program).
+int32_t audioAppEnter(int32_t appId) {
+ int32_t prev = appCurrent();
+
+ if (dvxMemAppIdPtr) {
+ *dvxMemAppIdPtr = appId;
+ }
+
+ return prev;
+}
+
+
+// The calling BASIC program's context, opened on first use.
+AudioCtxT *audioBasicContext(void) {
+ int32_t appId = appCurrent();
+
+ for (AudioCtxT *ctx = sCtxs; ctx; ctx = ctx->next) {
+ if (ctx->basic && ctx->appId == appId) {
+ return ctx;
+ }
+ }
+
+ AudioCtxT *ctx = audioOpen();
+
+ if (ctx) {
+ ctx->basic = true;
+ }
+
+ return ctx;
+}
+
+
+// Stops and frees everything ctx owns without notifying it (it is going
+// away); its queued notifications are dropped too.
+void audioClose(AudioCtxT *ctx) {
+ if (!ctx) {
+ return;
+ }
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (sVoices[i].owner == ctx) {
+ audioMixer.voices[i].state = MIX_FREE;
+ releaseSlot(i);
+ }
+ }
+
+ for (int32_t i = (int32_t)arrlen(sNotes) - 1; i >= 0; i--) {
+ if (sNotes[i].ctx == ctx) {
+ arrdel(sNotes, i);
+ }
+ }
+
+ AudioSampleT **sp = &sSamples;
+
+ while (*sp) {
+ AudioSampleT *s = *sp;
+
+ if (s->owner == ctx) {
+ audioSampleFree(s);
+ } else {
+ sp = &s->next;
+ }
+ }
+
+ AudioStreamT **tp = &sStreams;
+
+ while (*tp) {
+ AudioStreamT *t = *tp;
+
+ if (t->owner == ctx) {
+ audioStreamClose(t);
+ } else {
+ tp = &t->next;
+ }
+ }
+
+ AudioNotesT **np = &sNoteQueues;
+
+ while (*np) {
+ AudioNotesT *n = *np;
+
+ if (n->owner == ctx) {
+ audioNotesClose(n);
+ } else {
+ np = &n->next;
+ }
+ }
+
+ audioSynthCtxClosed(ctx);
+
+ for (AudioCtxT **cp = &sCtxs; *cp; cp = &(*cp)->next) {
+ if (*cp == ctx) {
+ *cp = ctx->next;
+ break;
+ }
+ }
+
+ free(ctx);
+
+ if (!sCtxs) {
+ devClose();
+ }
+}
+
+
+int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max) {
+ DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass);
+ int32_t count = 0;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(mods) && count < max; i++) {
+ if (soundOf(&mods[i])) {
+ snprintf(names[count++], AUDIO_NAME_MAX, "%s", mods[i].desc->name);
+ }
+ }
+
+ dvxDriversFree(mods);
+ return count;
+}
+
+
+// A voice id (positive) is no error either.
+const char *audioErrorText(int32_t err) {
+ if (err >= AUDIO_OK) {
+ return sErrorText[AUDIO_OK];
+ }
+
+ return err > -AUDIO_ERR_COUNT ? sErrorText[-err] : ERROR_TEXT_UNKNOWN;
+}
+
+
+void audioGetInfo(AudioInfoT *info) {
+ memset(info, 0, sizeof(*info));
+
+ if (!sDev.mod.desc) {
+ return;
+ }
+
+ snprintf(info->driver, sizeof(info->driver), "%s", sDev.mod.desc->name);
+ snprintf(info->description, sizeof(info->description), "%s", sDev.info.description);
+ info->rate = sDev.info.rate;
+ info->bits = sDev.info.bits;
+ info->channels = sDev.info.channels;
+ info->voiceLimit = sDev.voiceLimit;
+ info->running = true;
+ info->hasDac = sDev.info.hasDac;
+ info->hasSynth = sDev.info.hasSynth || sDev.synths[AUDIO_SYNTH_FM].desc || sDev.synths[AUDIO_SYNTH_MIDI].desc;
+ info->hasTone = audioMixer.hwTone != NULL;
+}
+
+
+int32_t audioGetMasterVolume(void) {
+ return sMaster;
+}
+
+
+void audioGetStats(AudioStatsT *stats) {
+ memset(stats, 0, sizeof(*stats));
+ stats->renders = audioMixer.renders;
+ stats->frames = audioMixer.frames;
+ stats->underruns = audioMixer.underruns;
+ stats->clipped = audioMixer.clipped;
+ stats->dropped = sDropped;
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (audioMixer.voices[i].state == MIX_PLAYING) {
+ stats->playing++;
+ }
+ }
+}
+
+
+int32_t audioGetVolume(const AudioCtxT *ctx) {
+ return ctx->volume;
+}
+
+
+void audioIdle(void *unused) {
+ (void)unused;
+ audioPump();
+}
+
+
+// The entry's frames come from the whole time queued since the queue went
+// quiet, so rounding never adds up over a tune. Frequencies above half
+// the mixing rate play at half the rate.
+int32_t audioNotesAdd(AudioNotesT *notes, uint32_t milliHz, uint32_t us) {
+ if (!notes || (milliHz != 0 && (milliHz < AUDIO_TONE_MIN_HZ * AUDIO_MILLIHZ_PER_HZ || milliHz > AUDIO_TONE_MAX_HZ * AUDIO_MILLIHZ_PER_HZ))) {
+ return AUDIO_ERR_ARG;
+ }
+
+ MixNoteRingT *q = ¬es->ring;
+
+ if (audioNotesSpace(notes) == 0) {
+ return AUDIO_ERR_BUSY;
+ }
+
+ if (audioNotesQueued(notes, NULL) == 0) {
+ notes->usQueued = 0;
+ notes->framesQueued = 0;
+ }
+
+ uint32_t rate = audioMixer.rate;
+ uint32_t limit = rate / 2 * AUDIO_MILLIHZ_PER_HZ;
+ uint32_t mhz = milliHz < limit ? milliHz : limit;
+ uint64_t end = (notes->usQueued + us) * rate / PLATFORM_US_PER_SEC;
+ MixNoteT *e = &q->data[q->head & q->mask];
+
+ e->hz = (mhz + AUDIO_MILLIHZ_PER_HZ / 2) / AUDIO_MILLIHZ_PER_HZ;
+ e->phaseStep = (uint32_t)(((uint64_t)mhz << PHASE_SHIFT) / ((uint64_t)rate * AUDIO_MILLIHZ_PER_HZ));
+ e->frames = (uint32_t)(end - notes->framesQueued);
+ notes->usQueued += us;
+ notes->framesQueued = end;
+ __atomic_signal_fence(__ATOMIC_RELEASE);
+ q->head++;
+ return AUDIO_OK;
+}
+
+
+void audioNotesClear(AudioNotesT *notes) {
+ if (!notes) {
+ return;
+ }
+
+ int32_t slot = lookupVoice(notes->owner, notes->voice);
+
+ if (slot >= 0) {
+ MixVoiceT *v = &audioMixer.voices[slot];
+ uint32_t flags = audioIrqOff();
+
+ notes->ring.tail = notes->ring.head;
+ v->remain = 0;
+ v->toneHz = 0;
+ audioIrqRestore(flags);
+ }
+}
+
+
+void audioNotesClose(AudioNotesT *notes) {
+ if (!notes) {
+ return;
+ }
+
+ int32_t slot = lookupVoice(notes->owner, notes->voice);
+
+ if (slot >= 0) {
+ audioMixer.voices[slot].endReason = AUDIO_END_STOPPED;
+ audioMixer.voices[slot].state = MIX_ENDED;
+ }
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (sVoices[i].notes == notes) {
+ sVoices[i].notes = NULL;
+ }
+ }
+
+ for (AudioNotesT **np = &sNoteQueues; *np; np = &(*np)->next) {
+ if (*np == notes) {
+ *np = notes->next;
+ break;
+ }
+ }
+
+ platformUnlockRegion(notes, sizeof(*notes), false);
+ free(notes);
+}
+
+
+int32_t audioNotesOpen(AudioCtxT *ctx, AudioWaveE wave, int32_t volume, AudioNotesT **out) {
+ *out = NULL;
+
+ if (!ctx || (wave != AUDIO_WAVE_SQUARE && wave != AUDIO_WAVE_TRIANGLE && wave != AUDIO_WAVE_NOISE)) {
+ return AUDIO_ERR_ARG;
+ }
+
+ AudioNotesT *n = (AudioNotesT *)calloc(1, sizeof(AudioNotesT));
+
+ if (!n || platformLockRegion(n, sizeof(*n), false) != 0) {
+ free(n);
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ n->ring.data = n->data;
+ n->ring.mask = AUDIO_NOTES_MAX - 1;
+ n->owner = ctx;
+ n->next = sNoteQueues;
+ sNoteQueues = n;
+
+ int32_t slot = voiceBegin(ctx, volume, AUDIO_PAN_CENTER, MIX_KIND_NOTES);
+
+ if (slot < 0) {
+ n->voice = 0;
+ audioNotesClose(n);
+ return slot;
+ }
+
+ audioMixer.voices[slot].notes = &n->ring;
+ audioMixer.voices[slot].wave = (uint8_t)wave;
+ audioMixer.voices[slot].lfsr = MIX_NOISE_SEED;
+ sVoices[slot].notes = n;
+ n->voice = voiceGo(slot);
+ *out = n;
+ return AUDIO_OK;
+}
+
+
+// tail is read before the voice's remaining frames: an entry the mixer
+// starts in between is counted twice, never not at all.
+uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones) {
+ const MixNoteRingT *q = ¬es->ring;
+ uint32_t tail = q->tail;
+ int32_t slot = lookupVoice(notes->owner, notes->voice);
+ uint32_t remain = slot >= 0 ? audioMixer.voices[slot].remain : 0;
+ uint32_t hz = slot >= 0 ? audioMixer.voices[slot].toneHz : 0;
+ uint32_t head = q->head;
+ uint32_t count = head - tail + (remain ? 1 : 0);
+
+ if (tones) {
+ *tones = remain && hz ? 1 : 0;
+
+ for (uint32_t i = tail; i != head; i++) {
+ *tones += q->data[i & q->mask].hz ? 1 : 0;
+ }
+ }
+
+ return count;
+}
+
+
+uint32_t audioNotesSpace(const AudioNotesT *notes) {
+ return notes->ring.mask + 1 - (notes->ring.head - notes->ring.tail);
+}
+
+
+int32_t audioNotesVoice(const AudioNotesT *notes) {
+ return notes->voice;
+}
+
+
+AudioCtxT *audioOpen(void) {
+ AudioCtxT *ctx = (AudioCtxT *)calloc(1, sizeof(AudioCtxT));
+
+ if (!ctx) {
+ return NULL;
+ }
+
+ if (!sDev.mod.desc && !devOpen()) {
+ free(ctx);
+ return NULL;
+ }
+
+ ctx->appId = appCurrent();
+ ctx->volume = AUDIO_VOLUME_MAX;
+ ctx->next = sCtxs;
+ sCtxs = ctx;
+ return ctx;
+}
+
+
+int32_t audioPlay(AudioSampleT *sample, int32_t volume, int32_t pan, bool loop) {
+ if (!sample) {
+ return AUDIO_ERR_ARG;
+ }
+
+ int32_t slot = voiceBegin(sample->owner, volume, pan, MIX_KIND_SAMPLE);
+
+ if (slot < 0) {
+ return slot;
+ }
+
+ MixVoiceT *v = &audioMixer.voices[slot];
+
+ v->data = sample->pcm;
+ v->frames = sample->frames;
+ v->channels = (uint8_t)sample->fmt.channels;
+ v->loop = loop;
+ sVoices[slot].rate = sample->fmt.rate;
+ sVoices[slot].sample = sample;
+ sample->users++;
+ return voiceGo(slot);
+}
+
+
+int32_t audioPlayFile(AudioCtxT *ctx, const char *path, int32_t volume, int32_t pan, bool loop) {
+ AudioSampleT *sample = NULL;
+ int32_t rc = audioSampleLoad(ctx, path, &sample);
+
+ if (rc != AUDIO_OK) {
+ return rc;
+ }
+
+ rc = audioPlay(sample, volume, pan, loop);
+
+ if (rc < 0) {
+ audioSampleFree(sample);
+ return rc;
+ }
+
+ sample->autoFree = true;
+ return rc;
+}
+
+
+// Callbacks may open, close and play anything; a nested pump (from inside
+// a callback) returns at once. The stream walk starts over when a
+// callback closed a stream under it.
+void audioPump(void) {
+ if (sPumping) {
+ return;
+ }
+
+ sPumping = true;
+ reclaimSlots();
+ devIdle();
+ deliverNotes();
+
+ AudioStreamT *s = sStreams;
+
+ while (s) {
+ AudioStreamT *next = s->next;
+
+ sStreamsDirty = false;
+
+ if (s->fill) {
+ streamFill(s);
+ }
+
+ s = sStreamsDirty ? sStreams : next;
+ }
+
+ sPumping = false;
+}
+
+
+int32_t audioSampleCreate(AudioCtxT *ctx, const void *pcm, uint32_t frames, const AudioFormatT *fmt, AudioSampleT **out) {
+ *out = NULL;
+
+ if (!ctx || !pcm || frames == 0 || !formatValid(fmt)) {
+ return AUDIO_ERR_ARG;
+ }
+
+ int16_t *copy = (int16_t *)malloc((size_t)frames * fmt->channels * sizeof(int16_t));
+
+ if (!copy) {
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ audioConvert(pcm, copy, frames * fmt->channels, fmt->bits);
+ return makeSample(ctx, copy, frames, fmt, out);
+}
+
+
+// Voices still playing it end as stopped (notified later) and forget it.
+void audioSampleFree(AudioSampleT *sample) {
+ if (!sample) {
+ return;
+ }
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (sVoices[i].sample == sample) {
+ if (audioMixer.voices[i].state == MIX_PLAYING) {
+ audioMixer.voices[i].endReason = AUDIO_END_STOPPED;
+ audioMixer.voices[i].state = MIX_ENDED;
+ }
+
+ sVoices[i].sample = NULL;
+ }
+ }
+
+ for (AudioSampleT **sp = &sSamples; *sp; sp = &(*sp)->next) {
+ if (*sp == sample) {
+ *sp = sample->next;
+ break;
+ }
+ }
+
+ platformUnlockRegion(sample->pcm, sample->frames * sample->fmt.channels * sizeof(int16_t), false);
+ free(sample->pcm);
+ free(sample);
+}
+
+
+void audioSampleInfo(const AudioSampleT *sample, AudioFormatT *fmt, uint32_t *frames) {
+ *fmt = sample->fmt;
+ *frames = sample->frames;
+}
+
+
+int32_t audioSampleLoad(AudioCtxT *ctx, const char *path, AudioSampleT **out) {
+ int32_t len = 0;
+ char *data = path ? platformReadFile(path, &len) : NULL;
+
+ *out = NULL;
+
+ if (!data) {
+ return path ? AUDIO_ERR_OPEN : AUDIO_ERR_ARG;
+ }
+
+ int32_t rc = audioSampleLoadMem(ctx, data, (uint32_t)len, out);
+
+ free(data);
+ return rc;
+}
+
+
+int32_t audioSampleLoadMem(AudioCtxT *ctx, const void *data, uint32_t size, AudioSampleT **out) {
+ int16_t *pcm = NULL;
+ uint32_t frames = 0;
+ AudioFormatT fmt;
+
+ *out = NULL;
+
+ if (!ctx || !data) {
+ return AUDIO_ERR_ARG;
+ }
+
+ int32_t rc = audioDecode((const uint8_t *)data, size, &pcm, &frames, &fmt);
+
+ if (rc != AUDIO_OK) {
+ return rc;
+ }
+
+ return makeSample(ctx, pcm, frames, &fmt, out);
+}
+
+
+void audioSetEndFn(AudioCtxT *ctx, AudioEndFnT fn, void *userData) {
+ ctx->endFn = fn;
+ ctx->endData = userData;
+}
+
+
+void audioSetMasterVolume(int32_t volume) {
+ sMaster = clampInt(volume, 0, AUDIO_VOLUME_MAX);
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ gainsUpdate(i);
+ }
+}
+
+
+void audioSetVolume(AudioCtxT *ctx, int32_t volume) {
+ ctx->volume = clampInt(volume, 0, AUDIO_VOLUME_MAX);
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (sVoices[i].owner == ctx) {
+ gainsUpdate(i);
+ }
+ }
+}
+
+
+// The library is being unloaded: close everything (no notifications).
+void audioShutdown(void) {
+ audioDeskShutdown();
+
+ while (sCtxs) {
+ audioClose(sCtxs);
+ }
+
+ arrfree(sNotes);
+ sNotes = NULL;
+}
+
+
+// Samples and tones; the voice of a stream or a note queue belongs to it.
+void audioStopAll(AudioCtxT *ctx) {
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (sVoices[i].owner == ctx && !sVoices[i].stream && !sVoices[i].notes) {
+ audioVoiceStop(ctx, sVoices[i].id);
+ }
+ }
+}
+
+
+void audioStreamClose(AudioStreamT *stream) {
+ if (!stream) {
+ return;
+ }
+
+ int32_t slot = lookupVoice(stream->owner, stream->voice);
+
+ if (slot >= 0) {
+ audioMixer.voices[slot].endReason = AUDIO_END_STOPPED;
+ audioMixer.voices[slot].state = MIX_ENDED;
+ }
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (sVoices[i].stream == stream) {
+ sVoices[i].stream = NULL;
+ }
+ }
+
+ for (AudioStreamT **tp = &sStreams; *tp; tp = &(*tp)->next) {
+ if (*tp == stream) {
+ *tp = stream->next;
+ break;
+ }
+ }
+
+ sStreamsDirty = true;
+ platformUnlockRegion(stream->ring.data, (stream->ring.mask + 1) * stream->ring.channels * sizeof(int16_t), false);
+ platformUnlockRegion(stream, sizeof(*stream), false);
+ free(stream->ring.data);
+ free(stream->scratch);
+ free(stream);
+}
+
+
+int32_t audioStreamOpen(AudioCtxT *ctx, const AudioFormatT *fmt, uint32_t bufferMs, AudioFillFnT fill, void *userData, AudioStreamT **out) {
+ *out = NULL;
+
+ if (!ctx || !formatValid(fmt)) {
+ return AUDIO_ERR_ARG;
+ }
+
+ uint32_t ms = bufferMs ? (uint32_t)clampInt((int32_t)bufferMs, AUDIO_STREAM_MS_MIN, AUDIO_STREAM_MS_MAX) : AUDIO_STREAM_MS_DEFAULT;
+ uint32_t want = fmt->rate * ms / MS_PER_SEC;
+ uint32_t frames = 1;
+
+ while (frames < want) {
+ frames <<= 1;
+ }
+
+ AudioStreamT *s = (AudioStreamT *)calloc(1, sizeof(AudioStreamT));
+
+ if (!s) {
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ uint32_t ringBytes = frames * fmt->channels * sizeof(int16_t);
+
+ s->ring.data = (int16_t *)malloc(ringBytes);
+ s->ring.mask = frames - 1;
+ s->ring.channels = fmt->channels;
+ s->scratch = fill ? (uint8_t *)malloc(STREAM_FILL_FRAMES * fmt->channels * fmt->bits / BITS_PER_BYTE) : NULL;
+
+ if (!s->ring.data || (fill && !s->scratch) || platformLockRegion(s, sizeof(*s), false) != 0) {
+ free(s->ring.data);
+ free(s->scratch);
+ free(s);
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ if (platformLockRegion(s->ring.data, ringBytes, false) != 0) {
+ platformUnlockRegion(s, sizeof(*s), false);
+ free(s->ring.data);
+ free(s->scratch);
+ free(s);
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ s->owner = ctx;
+ s->fill = fill;
+ s->fillData = userData;
+ s->fmt = *fmt;
+ s->next = sStreams;
+ sStreams = s;
+
+ int32_t slot = voiceBegin(ctx, AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, MIX_KIND_STREAM);
+
+ if (slot < 0) {
+ s->voice = 0;
+ audioStreamClose(s);
+ return slot;
+ }
+
+ audioMixer.voices[slot].ring = &s->ring;
+ audioMixer.voices[slot].channels = (uint8_t)fmt->channels;
+ sVoices[slot].rate = fmt->rate;
+ sVoices[slot].stream = s;
+ s->voice = voiceGo(slot);
+ *out = s;
+ return AUDIO_OK;
+}
+
+
+uint32_t audioStreamQueued(const AudioStreamT *stream) {
+ return stream->ring.head - stream->ring.tail;
+}
+
+
+uint32_t audioStreamSpace(const AudioStreamT *stream) {
+ return stream->ring.mask + 1 - audioStreamQueued(stream);
+}
+
+
+int32_t audioStreamVoice(const AudioStreamT *stream) {
+ return stream->voice;
+}
+
+
+// The samples are converted into the ring (in up to two pieces when it
+// wraps) before head moves, so the mixer never reads a frame early.
+uint32_t audioStreamWrite(AudioStreamT *stream, const void *pcm, uint32_t frames) {
+ AudioRingT *r = &stream->ring;
+ const uint8_t *src = (const uint8_t *)pcm;
+ uint32_t frameBytes = stream->fmt.channels * stream->fmt.bits / BITS_PER_BYTE;
+ uint32_t space = audioStreamSpace(stream);
+ uint32_t n = frames < space ? frames : space;
+ uint32_t head = r->head;
+ uint32_t done = 0;
+
+ while (done < n) {
+ uint32_t at = (head + done) & r->mask;
+ uint32_t piece = r->mask + 1 - at;
+
+ if (piece > n - done) {
+ piece = n - done;
+ }
+
+ audioConvert(src + done * frameBytes, &r->data[at * r->channels], piece * r->channels, stream->fmt.bits);
+ done += piece;
+ }
+
+ __atomic_signal_fence(__ATOMIC_RELEASE);
+ r->head = head + n;
+
+ if (n > 0) {
+ r->fed = true;
+ }
+
+ return n;
+}
+
+
+int32_t audioTone(AudioCtxT *ctx, AudioWaveE wave, uint32_t hz, int32_t volume, uint32_t ms) {
+ if (!ctx || (wave != AUDIO_WAVE_SQUARE && wave != AUDIO_WAVE_TRIANGLE && wave != AUDIO_WAVE_NOISE) || hz < AUDIO_TONE_MIN_HZ || hz > AUDIO_TONE_MAX_HZ) {
+ return AUDIO_ERR_ARG;
+ }
+
+ int32_t slot = voiceBegin(ctx, volume, AUDIO_PAN_CENTER, MIX_KIND_TONE);
+
+ if (slot < 0) {
+ return slot;
+ }
+
+ MixVoiceT *v = &audioMixer.voices[slot];
+
+ v->wave = (uint8_t)wave;
+ v->lfsr = MIX_NOISE_SEED;
+ v->timed = ms > 0;
+ v->remain = (uint32_t)((uint64_t)ms * audioMixer.rate / MS_PER_SEC);
+ sVoices[slot].hz = hz;
+
+ if (v->timed && v->remain == 0) {
+ v->remain = 1;
+ }
+
+ // On a free FM voice when there is one (voiceGo keys it on).
+ if (sDev.fmTones && wave != AUDIO_WAVE_NOISE) {
+ int32_t fm = audioFmToneStart(ctx, wave);
+
+ if (fm > 0) {
+ sVoices[slot].fmVoice = fm;
+ v->fmSlot = (uint8_t)(audioFmChannelOf(ctx, fm) + 1);
+ }
+ }
+
+ return voiceGo(slot);
+}
+
+
+bool audioVoiceIsPlaying(const AudioCtxT *ctx, int32_t voice) {
+ return lookupVoice(ctx, voice) >= 0;
+}
+
+
+int32_t audioVoiceSetFrequency(AudioCtxT *ctx, int32_t voice, uint32_t hz) {
+ int32_t slot = lookupVoice(ctx, voice);
+
+ if (slot < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ if (audioMixer.voices[slot].kind != MIX_KIND_TONE || hz < AUDIO_TONE_MIN_HZ || hz > AUDIO_TONE_MAX_HZ) {
+ return AUDIO_ERR_ARG;
+ }
+
+ sVoices[slot].hz = hz;
+ stepUpdate(slot);
+ return AUDIO_OK;
+}
+
+
+int32_t audioVoiceSetPan(AudioCtxT *ctx, int32_t voice, int32_t pan) {
+ int32_t slot = lookupVoice(ctx, voice);
+
+ if (slot < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ sVoices[slot].pan = clampInt(pan, AUDIO_PAN_LEFT, AUDIO_PAN_RIGHT);
+ gainsUpdate(slot);
+ return AUDIO_OK;
+}
+
+
+int32_t audioVoiceSetPitch(AudioCtxT *ctx, int32_t voice, int32_t pitch) {
+ int32_t slot = lookupVoice(ctx, voice);
+
+ if (slot < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ sVoices[slot].pitch = clampInt(pitch, AUDIO_PITCH_MIN, AUDIO_PITCH_MAX);
+ stepUpdate(slot);
+ return AUDIO_OK;
+}
+
+
+int32_t audioVoiceSetVolume(AudioCtxT *ctx, int32_t voice, int32_t volume) {
+ int32_t slot = lookupVoice(ctx, voice);
+
+ if (slot < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ sVoices[slot].volume = clampInt(volume, 0, AUDIO_VOLUME_MAX);
+ gainsUpdate(slot);
+ return AUDIO_OK;
+}
+
+
+int32_t audioVoiceStop(AudioCtxT *ctx, int32_t voice) {
+ int32_t slot = lookupVoice(ctx, voice);
+
+ if (slot < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ if (sVoices[slot].stream) {
+ return AUDIO_ERR_ARG;
+ }
+
+ audioMixer.voices[slot].endReason = AUDIO_END_STOPPED;
+ audioMixer.voices[slot].state = MIX_ENDED;
+ return AUDIO_OK;
+}
+
+
+static int32_t clampInt(int32_t v, int32_t lo, int32_t hi) {
+ if (v < lo) {
+ return lo;
+ }
+
+ return v > hi ? hi : v;
+}
+
+
+// Each callback runs as the program owning the context. The queue is
+// taken over first: callbacks may end more voices (queued for the next
+// pump) or close contexts (their notes are dropped from the new queue).
+static void deliverNotes(void) {
+ EndNoteT *notes = sNotes;
+
+ sNotes = NULL;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(notes); i++) {
+ AudioCtxT *ctx = notes[i].ctx;
+ bool live = false;
+
+ for (AudioCtxT *c = sCtxs; c; c = c->next) {
+ live = live || c == ctx;
+ }
+
+ if (live && ctx->endFn) {
+ int32_t prev = audioAppEnter(ctx->appId);
+
+ ctx->endFn(ctx, notes[i].voice, notes[i].reason, ctx->endData);
+ audioAppEnter(prev);
+ }
+ }
+
+ arrfree(notes);
+}
+
+
+static void devClose(void) {
+ if (!sDev.mod.desc) {
+ return;
+ }
+
+ platformShutdownHookRemove(devEmergencyStop);
+ devCloseSynths();
+
+ if (!sDev.suspended) {
+ soundOf(&sDev.mod)->close();
+ }
+
+ audioMixLock(false);
+ dvxDriverUnload(&sDev.mod);
+ memset(&sDev, 0, sizeof(sDev));
+ dvxLog("Audio: sound device closed");
+}
+
+
+// Every context is closed, so no program holds a voice or the port.
+static void devCloseSynths(void) {
+ audioFmDetach();
+ audioMidiDetach();
+ audioSynthLock(false);
+
+ for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
+ if (sDev.synths[k].desc) {
+ synthOf(&sDev.synths[k])->close();
+ dvxDriverUnload(&sDev.synths[k]);
+ }
+ }
+}
+
+
+// DVX is ending without closing the library (exit, fatal crash): stop the
+// hardware so it does not keep playing or interrupting.
+static void devEmergencyStop(void) {
+ for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
+ if (sDev.synths[k].desc) {
+ synthOf(&sDev.synths[k])->close();
+ sDev.synths[k].desc = NULL;
+ }
+ }
+
+ if (sDev.mod.desc && !sDev.suspended) {
+ soundOf(&sDev.mod)->close();
+ }
+
+ sDev.mod.desc = NULL;
+}
+
+
+// What a driver's open is asked for, from DVX.INI (prefs stays open
+// while the driver opens).
+static void devConfig(PrefsHandleT *prefs, AudioDrvConfigT *cfg) {
+ cfg->prefs = prefs;
+ cfg->rate = (uint32_t)clampInt(prefsGetInt(prefs, AUDIO_INI_SECTION, INI_RATE, AUDIO_RATE_DEFAULT), AUDIO_RATE_MIN, AUDIO_RATE_MAX);
+ cfg->bits = (uint32_t)prefsGetInt(prefs, AUDIO_INI_SECTION, INI_BITS, 0);
+ cfg->channels = (uint32_t)prefsGetInt(prefs, AUDIO_INI_SECTION, INI_CHANNELS, 0);
+
+ if (cfg->bits != BITS_8 && cfg->bits != BITS_16) {
+ cfg->bits = 0;
+ }
+
+ if (cfg->channels > CHANNELS_MAX) {
+ cfg->channels = 0;
+ }
+}
+
+
+// Close the output once no voice has played for IDLE_CLOSE_US; the driver
+// and the synthesizers stay (voiceBegin opens the output again).
+static void devIdle(void) {
+ if (!sDev.mod.desc || sDev.suspended) {
+ return;
+ }
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (audioMixer.voices[i].state != MIX_FREE) {
+ sDev.quiet = false;
+ return;
+ }
+ }
+
+ uint64_t now = platformMicros();
+
+ if (!sDev.quiet) {
+ sDev.quiet = true;
+ sDev.quietSince = now;
+ return;
+ }
+
+ if (now - sDev.quietSince >= IDLE_CLOSE_US) {
+ soundOf(&sDev.mod)->close();
+ sDev.suspended = true;
+ dvxLog("Audio: output closed after %d s of silence", AUDIO_IDLE_CLOSE_SEC);
+ }
+}
+
+
+static bool devOpen(void) {
+ PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH);
+ AudioDrvConfigT cfg;
+
+ devConfig(prefs, &cfg);
+ sDev.voiceLimit = (uint32_t)clampInt(prefsGetInt(prefs, AUDIO_INI_SECTION, INI_VOICES, AUDIO_VOICES_DEFAULT), 1, AUDIO_VOICES_MAX);
+ sMaster = clampInt(prefsGetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, PERCENT_MAX), 0, PERCENT_MAX) * AUDIO_VOLUME_MAX / PERCENT_MAX;
+ sDev.fmTones = prefsGetBool(prefs, AUDIO_INI_SECTION, INI_FM_TONES, true);
+
+ const char *want = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, AUDIO_DRIVER_AUTO);
+ DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass);
+ int32_t count = (int32_t)arrlen(mods);
+ int32_t chosen = -1;
+ int32_t tried = -1;
+
+ if (strcasecmp(want, AUDIO_DRIVER_AUTO) != 0) {
+ for (int32_t i = 0; i < count; i++) {
+ if (soundOf(&mods[i]) && strcasecmp(mods[i].desc->name, want) == 0) {
+ tried = i;
+ chosen = devTry(mods, i, &cfg) ? i : -1;
+ }
+ }
+
+ if (chosen < 0) {
+ dvxLog("Audio: sound driver '%s' %s; detecting", want, tried < 0 ? "not found" : "did not start");
+ }
+ }
+
+ for (int32_t i = 0; i < count && chosen < 0; i++) {
+ if (soundOf(&mods[i]) && i != tried && devTry(mods, i, &cfg)) {
+ chosen = i;
+ }
+ }
+
+ if (chosen >= 0) {
+ devOpenSynths(mods, &cfg);
+ }
+
+ dvxDriversFree(mods);
+ prefsClose(prefs);
+
+ if (chosen < 0) {
+ dvxLog("Audio: no sound driver could be started");
+ return false;
+ }
+
+ return true;
+}
+
+
+// The FM chip and the MIDI port: of each kind the first synth driver
+// (highest priority first) that finds its hardware. Kept modules are
+// taken out of mods so dvxDriversFree leaves them loaded.
+static void devOpenSynths(DvxDriverModT *mods, const AudioDrvConfigT *cfg) {
+ for (int32_t i = 0; i < (int32_t)arrlen(mods); i++) {
+ const AudioSynthDriverT *syn = synthOf(&mods[i]);
+ AudioSynthDrvInfoT info;
+
+ if (!syn || sDev.synths[syn->kind].desc) {
+ continue;
+ }
+
+ memset(&info, 0, sizeof(info));
+
+ if (!syn->open(cfg, &info)) {
+ dvxLog("Audio: synth driver '%s' found no device", syn->hdr.name);
+ continue;
+ }
+
+ sDev.synths[syn->kind] = dvxDriverKeep(mods, i);
+
+ if (syn->kind == AUDIO_SYNTH_FM) {
+ audioFmAttach(syn, &info);
+ } else {
+ audioMidiAttach(syn, &info);
+ }
+
+ dvxLog("Audio: using '%s': %s", syn->hdr.name, info.description);
+ }
+
+ if (!audioSynthLock(true)) {
+ dvxLog("Audio: could not pin the synthesizer code in memory");
+ }
+}
+
+
+// Open the output again with the driver in use. Every voice is free, so
+// a new format (DVX.INI changed meanwhile) only needs the mixer reset.
+static bool devResume(void) {
+ const AudioDriverT *drv = soundOf(&sDev.mod);
+ PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH);
+ AudioDrvConfigT cfg;
+ AudioDrvInfoT info;
+
+ memset(&info, 0, sizeof(info));
+ devConfig(prefs, &cfg);
+
+ bool opened = drv->open(&cfg, &info);
+
+ prefsClose(prefs);
+
+ if (!opened || !infoUsable(&info)) {
+ dvxLog("Audio: driver '%s' could not open the output again", drv->hdr.name);
+
+ if (opened) {
+ drv->close();
+ }
+
+ return false;
+ }
+
+ if (info.rate != sDev.info.rate || info.bits != sDev.info.bits || info.channels != sDev.info.channels || info.tone != sDev.info.tone || info.hasDac != sDev.info.hasDac) {
+ audioMixReset(info.rate, info.bits, info.channels, info.hasDac ? NULL : info.tone);
+ }
+
+ sDev.info = info;
+
+ if (!drv->play()) {
+ dvxLog("Audio: driver '%s' could not start playing again", drv->hdr.name);
+ drv->close();
+ return false;
+ }
+
+ sDev.suspended = false;
+ sDev.quiet = false;
+ dvxLog("Audio: output opened again");
+ return true;
+}
+
+
+// open, then set the mixer up for the format the device chose, then play;
+// the module is kept when it plays.
+static bool devTry(DvxDriverModT *mods, int32_t index, const AudioDrvConfigT *cfg) {
+ const AudioDriverT *drv = soundOf(&mods[index]);
+ AudioDrvInfoT info;
+
+ memset(&info, 0, sizeof(info));
+
+ if (!drv->open(cfg, &info)) {
+ dvxLog("Audio: driver '%s' found no device", drv->hdr.name);
+ return false;
+ }
+
+ if (!infoUsable(&info)) {
+ dvxLog("Audio: driver '%s' reported an unusable format", drv->hdr.name);
+ drv->close();
+ return false;
+ }
+
+ audioMixReset(info.rate, info.bits, info.channels, info.hasDac ? NULL : info.tone);
+
+ if (!audioMixLock(true)) {
+ dvxLog("Audio: could not pin the mixer in memory");
+ drv->close();
+ return false;
+ }
+
+ // Set before play: the driver may mix its first blocks right away.
+ sDev.mod = mods[index];
+ sDev.info = info;
+
+ if (!drv->play()) {
+ dvxLog("Audio: driver '%s' could not start playing", drv->hdr.name);
+ drv->close();
+ audioMixLock(false);
+ sDev.mod.desc = NULL;
+ return false;
+ }
+
+ sDev.mod = dvxDriverKeep(mods, index);
+ platformShutdownHookAdd(devEmergencyStop);
+ dvxLog("Audio: using '%s': %s, %lu Hz, %lu-bit %s, %lu voices", drv->hdr.name, info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels == CHANNELS_MAX ? "stereo" : "mono", (unsigned long)sDev.voiceLimit);
+ return true;
+}
+
+
+// audio.lib's own check of a driver module's descriptor.
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) {
+ if (kind == DRV_KIND_SOUND) {
+ const AudioDriverT *drv = (const AudioDriverT *)desc;
+
+ return drv->open && drv->play && drv->close;
+ }
+
+ const AudioSynthDriverT *syn = (const AudioSynthDriverT *)desc;
+
+ return syn->open && syn->close && ((syn->kind == AUDIO_SYNTH_FM && syn->write) || (syn->kind == AUDIO_SYNTH_MIDI && syn->put));
+}
+
+
+// The frequency and loudness of a tone on an FM voice (after its pitch,
+// volume, pan, program or master volume changed).
+static void fmToneRefresh(int32_t slot) {
+ const VoiceInfoT *vi = &sVoices[slot];
+ const MixVoiceT *v = &audioMixer.voices[slot];
+
+ if (vi->fmVoice && v->fmSlot) {
+ audioFmToneUpdate(vi->owner, vi->fmVoice, v->toneHz, v->gainL, v->gainR);
+ }
+}
+
+
+static bool formatValid(const AudioFormatT *fmt) {
+ return fmt && fmt->rate >= AUDIO_RATE_MIN && fmt->rate <= AUDIO_RATE_MAX && (fmt->bits == BITS_8 || fmt->bits == BITS_16) && fmt->channels >= 1 && fmt->channels <= CHANNELS_MAX;
+}
+
+
+// gain = volume x pan side x context volume x master, each 0..256.
+static void gainsUpdate(int32_t slot) {
+ const VoiceInfoT *vi = &sVoices[slot];
+
+ if (!vi->owner) {
+ return;
+ }
+
+ int32_t sideL = vi->pan > 0 ? AUDIO_VOLUME_MAX - vi->pan : AUDIO_VOLUME_MAX;
+ int32_t sideR = vi->pan < 0 ? AUDIO_VOLUME_MAX + vi->pan : AUDIO_VOLUME_MAX;
+ int32_t base = ((vi->volume * vi->owner->volume) >> MIX_GAIN_SHIFT) * sMaster >> MIX_GAIN_SHIFT;
+
+ audioMixer.voices[slot].gainL = (base * sideL) >> MIX_GAIN_SHIFT;
+ audioMixer.voices[slot].gainR = (base * sideR) >> MIX_GAIN_SHIFT;
+ fmToneRefresh(slot);
+}
+
+
+// The format a driver's open reported is one the mixer can produce.
+static bool infoUsable(const AudioDrvInfoT *info) {
+ return info->rate >= AUDIO_RATE_MIN && info->rate <= AUDIO_RATE_MAX && (info->bits == BITS_8 || info->bits == BITS_16) && info->channels >= 1 && info->channels <= CHANNELS_MAX;
+}
+
+
+// The slot of ctx's voice with this id while it plays, else -1.
+static int32_t lookupVoice(const AudioCtxT *ctx, int32_t voice) {
+ if (voice <= 0) {
+ return -1;
+ }
+
+ int32_t slot = (voice - 1) % VOICE_ID_STRIDE;
+
+ if (slot >= AUDIO_VOICES_MAX || sVoices[slot].id != voice || sVoices[slot].owner != ctx || audioMixer.voices[slot].state != MIX_PLAYING) {
+ return -1;
+ }
+
+ return slot;
+}
+
+
+// Takes pcm (malloc'd, frames * channels int16) and pins it for the mixer.
+static int32_t makeSample(AudioCtxT *ctx, int16_t *pcm, uint32_t frames, const AudioFormatT *fmt, AudioSampleT **out) {
+ AudioSampleT *s = (AudioSampleT *)calloc(1, sizeof(AudioSampleT));
+
+ if (!s || platformLockRegion(pcm, frames * fmt->channels * sizeof(int16_t), false) != 0) {
+ free(s);
+ free(pcm);
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ s->owner = ctx;
+ s->pcm = pcm;
+ s->frames = frames;
+ s->fmt = *fmt;
+ s->next = sSamples;
+ sSamples = s;
+ *out = s;
+ return AUDIO_OK;
+}
+
+
+// Free every ended slot, queueing a notification for owners that want
+// one.
+static void reclaimSlots(void) {
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (audioMixer.voices[i].state != MIX_ENDED) {
+ continue;
+ }
+
+ AudioCtxT *owner = sVoices[i].owner;
+
+ if (owner && owner->endFn) {
+ EndNoteT note = { owner, sVoices[i].id, (AudioEndE)audioMixer.voices[i].endReason };
+
+ arrput(sNotes, note);
+ }
+
+ audioMixer.voices[i].state = MIX_FREE;
+ releaseSlot(i);
+ }
+}
+
+
+// The slot's mixer state is already free: drop its sample reference (an
+// audioPlayFile sample goes with its voice) and forget the voice.
+static void releaseSlot(int32_t slot) {
+ AudioSampleT *sample = sVoices[slot].sample;
+
+ if (sVoices[slot].fmVoice) {
+ audioFmFree(sVoices[slot].owner, sVoices[slot].fmVoice);
+ }
+
+ memset(&sVoices[slot], 0, sizeof(sVoices[slot]));
+
+ if (sample) {
+ sample->users--;
+
+ if (sample->autoFree && sample->users == 0) {
+ audioSampleFree(sample);
+ }
+ }
+}
+
+
+// The sound driver of a module (NULL: none, or a synth driver).
+static const AudioDriverT *soundOf(const DvxDriverModT *mod) {
+ return mod->kind == DRV_KIND_SOUND ? (const AudioDriverT *)mod->desc : NULL;
+}
+
+
+// Source step, or tone phase step, for the voice's rate, frequency and
+// pitch at the mixing rate. A tone is kept below half the mixing rate.
+static void stepUpdate(int32_t slot) {
+ const VoiceInfoT *vi = &sVoices[slot];
+ MixVoiceT *v = &audioMixer.voices[slot];
+
+ // A note queue's entries carry their own steps.
+ if (v->kind == MIX_KIND_NOTES) {
+ return;
+ }
+
+ if (v->kind == MIX_KIND_TONE) {
+ uint32_t hz = (uint32_t)((uint64_t)vi->hz * (uint32_t)vi->pitch / AUDIO_PITCH_NORMAL);
+
+ if (hz > audioMixer.rate / 2) {
+ hz = audioMixer.rate / 2;
+ }
+
+ v->toneHz = hz;
+ v->phaseStep = (uint32_t)(((uint64_t)hz << PHASE_SHIFT) / audioMixer.rate);
+ fmToneRefresh(slot);
+ return;
+ }
+
+ uint64_t step = ((uint64_t)vi->rate << MIX_FRAC_BITS) * (uint32_t)vi->pitch / AUDIO_PITCH_NORMAL / audioMixer.rate;
+
+ v->step = step ? (uint32_t)step : 1;
+}
+
+
+// Ask the fill function for as much as the ring has room for, as the
+// program owning the stream.
+static void streamFill(AudioStreamT *s) {
+ uint32_t space = audioStreamSpace(s);
+
+ while (space > 0) {
+ uint32_t ask = space < STREAM_FILL_FRAMES ? space : STREAM_FILL_FRAMES;
+ int32_t prev = audioAppEnter(s->owner->appId);
+ uint32_t got = s->fill(s, s->scratch, ask, s->fillData);
+
+ audioAppEnter(prev);
+
+ // The callback closed a stream (maybe this one): stop here.
+ if (sStreamsDirty || got == 0) {
+ break;
+ }
+
+ audioStreamWrite(s, s->scratch, got < ask ? got : ask);
+
+ if (got < ask) {
+ break;
+ }
+
+ space = audioStreamSpace(s);
+ }
+}
+
+
+// The synth driver of a module (NULL: none, or a sound driver).
+static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod) {
+ return mod->kind == DRV_KIND_SYNTH ? (const AudioSynthDriverT *)mod->desc : NULL;
+}
+
+
+// Claim a free slot for a new voice of owner and fill in everything but
+// the source; the voice limit may drop the quietest sample or tone first
+// (or refuse this one when it would be the quietest). Returns the slot
+// or an error.
+static int32_t voiceBegin(AudioCtxT *owner, int32_t volume, int32_t pan, uint8_t kind) {
+ if (!owner || !sDev.mod.desc) {
+ return AUDIO_ERR_ARG;
+ }
+
+ if (sDev.suspended && !devResume()) {
+ return AUDIO_ERR_NO_DEVICE;
+ }
+
+ sDev.quiet = false;
+ reclaimSlots();
+
+ VoiceInfoT probe;
+
+ memset(&probe, 0, sizeof(probe));
+ probe.owner = owner;
+ probe.volume = clampInt(volume, 0, AUDIO_VOLUME_MAX);
+ probe.pan = clampInt(pan, AUDIO_PAN_LEFT, AUDIO_PAN_RIGHT);
+ probe.pitch = AUDIO_PITCH_NORMAL;
+
+ // Panning only turns one side down, so the louder side is at base.
+ int32_t newGain = ((probe.volume * owner->volume) >> MIX_GAIN_SHIFT) * sMaster >> MIX_GAIN_SHIFT;
+ int32_t playing = 0;
+ int32_t quiet = -1;
+ int32_t slotFree = -1;
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ const MixVoiceT *v = &audioMixer.voices[i];
+
+ if (v->state == MIX_FREE) {
+ slotFree = slotFree < 0 ? i : slotFree;
+ continue;
+ }
+
+ playing++;
+
+ if (sVoices[i].stream || sVoices[i].notes) {
+ continue;
+ }
+
+ if (quiet < 0) {
+ quiet = i;
+ continue;
+ }
+
+ const MixVoiceT *q = &audioMixer.voices[quiet];
+ int32_t gainV = v->gainL > v->gainR ? v->gainL : v->gainR;
+ int32_t gainQ = q->gainL > q->gainR ? q->gainL : q->gainR;
+
+ if (gainV < gainQ || (gainV == gainQ && v->seq < q->seq)) {
+ quiet = i;
+ }
+ }
+
+ if ((uint32_t)playing >= sDev.voiceLimit) {
+ const MixVoiceT *q = quiet >= 0 ? &audioMixer.voices[quiet] : NULL;
+ int32_t gainQ = q ? (q->gainL > q->gainR ? q->gainL : q->gainR) : 0;
+
+ sDropped++;
+
+ if (!q || kind == MIX_KIND_STREAM || kind == MIX_KIND_NOTES || newGain <= gainQ) {
+ return AUDIO_ERR_NO_VOICE;
+ }
+
+ audioMixer.voices[quiet].endReason = AUDIO_END_DROPPED;
+ audioMixer.voices[quiet].state = MIX_ENDED;
+ reclaimSlots();
+ slotFree = quiet;
+ }
+
+ if (slotFree < 0) {
+ return AUDIO_ERR_NO_VOICE;
+ }
+
+ MixVoiceT *v = &audioMixer.voices[slotFree];
+
+ if (++sGeneration > VOICE_GEN_MAX) {
+ sGeneration = 1;
+ }
+
+ memset(v, 0, sizeof(*v));
+ v->kind = kind;
+ v->seq = ++sSeq;
+ probe.id = (int32_t)(sGeneration * VOICE_ID_STRIDE) + slotFree + 1;
+ sVoices[slotFree] = probe;
+ gainsUpdate(slotFree);
+ return slotFree;
+}
+
+
+// The voice's source is set: compute its step, load its first frames and
+// let the mixer have it. Returns the voice id.
+static int32_t voiceGo(int32_t slot) {
+ MixVoiceT *v = &audioMixer.voices[slot];
+
+ stepUpdate(slot);
+
+ if (v->kind == MIX_KIND_SAMPLE || v->kind == MIX_KIND_STREAM) {
+ audioMixPrime(v);
+ }
+
+ __atomic_signal_fence(__ATOMIC_RELEASE);
+ v->state = MIX_PLAYING;
+ return sVoices[slot].id;
+}
diff --git a/src/libs/kpunch/audio/audio.dep b/src/libs/kpunch/audio/audio.dep
new file mode 100644
index 00000000..7f73e3de
--- /dev/null
+++ b/src/libs/kpunch/audio/audio.dep
@@ -0,0 +1,3 @@
+libtasks
+libdvx
+dvxshell
diff --git a/src/libs/kpunch/audio/audio.dhs b/src/libs/kpunch/audio/audio.dhs
new file mode 100644
index 00000000..8a65422c
--- /dev/null
+++ b/src/libs/kpunch/audio/audio.dhs
@@ -0,0 +1,229 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+.section Technical Reference (for Developers)|Libraries
+.topic lib.audio
+.title Sound Library
+.toc 0 Sound Library
+.index audio
+.index audio.h
+.index audioDrv.h
+.index audioOpen
+.index audioClose
+.index audioPlay
+.index audioPlayFile
+.index audioTone
+.index audioStreamOpen
+.index audioStreamWrite
+.index audioSampleLoad
+.index audioGetInfo
+.index AudioInfoT
+.index AudioDriverT
+.index sound
+.index sound.bas
+.index PlayWave
+.index PlayTone
+.index SoundInfo$
+.index Sound Blaster
+.index PC speaker
+.index audioFmAlloc
+.index audioFmWrite
+.index audioFmRawAcquire
+.index audioMidiAcquire
+.index AudioSynthDriverT
+.index OPL
+.index MPU-401
+.index audioNotesOpen
+.index audioNotesAdd
+.index AudioNotesT
+.index audioDeskPlay
+.index audioDeskPlayFile
+.index audioDriverNames
+.index Desktop sounds
+
+.h1 Sound Library
+
+Sound for DVX programs through whatever sound hardware the computer has: sampled sound from WAV and VOC files, tones and noise, and streams a program feeds itself (music, speech). Every program opens its own audio context; any number of programs play at the same time through one mixer, and everything a program opened is stopped and released when it ends, normally, killed or crashed.
+
+Header: audio/audio.h (programs), audio/audioDrv.h (sound drivers)
+
+Loaded as: bin/libs/kpunch/audio/audio.lib, drivers in bin/libs/kpunch/audio/drivers/*.drv
+
+.h2 Contexts and the Device
+
+.code
+AudioCtxT *audio = audioOpen(); // NULL: no driver could start
+
+int32_t voice = audioPlayFile(audio, "BOOM.WAV", AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, false);
+
+if (voice < 0) {
+ dvxLog("no boom: %s", audioErrorText(voice));
+}
+...
+audioClose(audio); // or let the program end
+.endcode
+
+The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth) or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices and the voices playing.
+
+None of the functions may be called from an interrupt (a timer-service callback).
+
+.h2 Samples and Voices
+
+audioSampleLoad (file), audioSampleLoadMem (memory) and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.
+
+.table
+ Setting Range
+ ------- -----
+ volume 0 .. AUDIO_VOLUME_MAX (256)
+ pan AUDIO_PAN_LEFT (-256) .. AUDIO_PAN_RIGHT (256); centre plays at full volume on both sides and panning turns the other side down
+ pitch AUDIO_PITCH_MIN (64) .. AUDIO_PITCH_MAX (1024), AUDIO_PITCH_NORMAL (256) = recorded speed
+.endtable
+
+audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoiceStop and audioVoiceIsPlaying take the context and the voice id; a voice id of another context, or of a voice that has ended, gives AUDIO_ERR_VOICE. audioStopAll stops every sample and tone of a context.
+
+audioSetEndFn(ctx, fn, userData) asks for a call when a voice of ctx ends, with the reason: AUDIO_END_FINISHED (played out), AUDIO_END_STOPPED (stopped, or its sample or stream went away) or AUDIO_END_DROPPED (the voice limit made room for a louder sound). The call comes from audioPump or the shell's idle time, as the program that owns the context, never from an interrupt. The mixer runs up to one buffer (about 50 ms) ahead of what is heard, so a notification can come that much before the sound has finished playing.
+
+.h2 Tones
+
+audioTone(ctx, wave, hz, volume, ms) plays a square (AUDIO_WAVE_SQUARE), triangle (AUDIO_WAVE_TRIANGLE) or noise (AUDIO_WAVE_NOISE) tone of hz (AUDIO_TONE_MIN_HZ 20 to AUDIO_TONE_MAX_HZ 20000) for ms milliseconds, or until stopped when ms is 0. For noise, hz is how fast the noise changes: higher sounds brighter. audioVoiceSetFrequency changes a playing tone. When the computer has an FM chip (and DVX.INI [audio] fmTones is not false), a square or triangle tone plays on a free FM voice (a square as a carrier modulated at twice its pitch, which gives odd harmonics like a square wave; a triangle as a sine) at up to the chip's highest note (about 6,200 Hz); when every FM voice is taken it is mixed like any sound. Its voice id, volume, pan, pitch and end notification work the same either way. On a device with a DAC tones are mixed like any sound; on the PC speaker in its tones mode the loudest square or triangle tone playing is sounded by the timer chip.
+
+.h2 Note Queues
+
+.code
+AudioNotesT *tune = NULL;
+
+audioNotesOpen(audio, AUDIO_WAVE_SQUARE, AUDIO_VOLUME_MAX, &tune);
+audioNotesAdd(tune, 440 * AUDIO_MILLIHZ_PER_HZ, 250000); // A for 1/4 s
+audioNotesAdd(tune, 0, 50000); // a rest
+.endcode
+
+A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.
+
+.h2 Streams
+
+.code
+AudioFormatT fmt = { 22050, 16, 2 };
+AudioStreamT *stream = NULL;
+
+audioStreamOpen(audio, &fmt, 250, fillMusic, &player, &stream);
+.endcode
+
+audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. A stream that runs dry plays silence and counts underruns. audioStreamClose stops and frees it.
+
+.h2 FM Synthesizer
+
+.code
+int32_t v = audioFmAlloc(audio); // AUDIO_ERR_NO_VOICE: all taken
+
+audioFmWrite(audio, v, AUDIO_FM_CAR_LEVEL, 0);
+audioFmWrite(audio, v, AUDIO_FM_FNUM_LOW, fnum & 0xFF);
+audioFmWrite(audio, v, AUDIO_FM_KEY_BLOCK, AUDIO_FM_KEY_ON | (block << AUDIO_FM_BLOCK_SHIFT) | (fnum >> AUDIO_FM_FNUM_HI_SHIFT));
+audioFmFree(audio, v); // key off, back to everyone
+.endcode
+
+An OPL2 (9 voices) or OPL3 (18 voices, each playing left, right or both) found by the opl driver is shared by every program: audioFmAlloc(ctx) takes a two-operator voice (the least recently given back first, so a released note's tail is cut last) and returns its id, AUDIO_ERR_NO_VOICE when all are taken, AUDIO_ERR_BUSY while a program has the whole chip, AUDIO_ERR_NO_SYNTH without a chip. audioFmWrite(ctx, voice, reg, value) sets a register of the voice's channel, numbered as for the first channel of an OPL2 (AUDIO_FM_MOD_CHAR 20h, AUDIO_FM_CAR_CHAR 23h, levels 40h/43h, attack 60h/63h, sustain 80h/83h, waveforms E0h/E3h, AUDIO_FM_FNUM_LOW A0h, AUDIO_FM_KEY_BLOCK B0h, AUDIO_FM_FEEDBACK C0h); the library adds the channel's offsets and bank. On an OPL3 a feedback value without an output bit (AUDIO_FM_OUT_LEFT, AUDIO_FM_OUT_RIGHT) plays on both sides. Frequencies: hz = fnum x AUDIO_FM_CHIP_HZ (49,716) x 2^block / 2^20. audioFmFree keys the voice off and gives it back; closing the context gives back all of its voices. audioFmGetInfo tells the chip, its voices and how many are free.
+
+audioFmRawAcquire(ctx) gives one program the whole chip: every voice the others hold is keyed off and taken back (their tones go on in the mixer), new voices are refused, and audioFmRawWrite(ctx, reg, value) sets any register (bank x AUDIO_FM_BANK_REGS + index). A second program asking gets AUDIO_ERR_BUSY. audioFmRawRelease (or closing the context) silences the chip, sets it up again and gives its voices back to everyone.
+
+audioFmAlloc, audioFmFree, audioFmWrite and audioFmRawWrite may also be called from a timer callback (platformTimerAdd); music.lib plays MIDI that way.
+
+.h2 MIDI Port
+
+With DVX.INI [audio] midi = mpu401 the mpu401 driver opens an MPU-401 (or compatible, such as a Sound Blaster 16's) in UART mode, for an external synthesizer such as a Roland MT-32 or SC-55. The port belongs to one program at a time: audioMidiAcquire(ctx) (AUDIO_ERR_BUSY when another has it, AUDIO_ERR_NO_SYNTH without a port), audioMidiPut(ctx, byte) sends a byte and returns AUDIO_ERR_BUSY when the port cannot take one yet (send it again later; it may be called from a timer callback), audioMidiRelease gives it back (closing the context does too). audioMidiGetInfo tells whether there is a port and whether it is in use. music.lib uses the port for MIDI music.
+
+.h2 Volume and the Voice Limit
+
+audioSetMasterVolume sets the volume of all sound (DVX.INI [audio] volume, in percent, sets it when the device starts); audioSetVolume sets the volume of everything one context plays. At most the voice limit (DVX.INI [audio] voices, 1 to AUDIO_VOICES_MAX, default 16) plays at once. Starting another sample or tone then stops the quietest one playing (it ends with AUDIO_END_DROPPED); when the new sound would itself be the quietest it is not started (AUDIO_ERR_NO_VOICE). Streams are never dropped.
+
+.h2 Files
+
+WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to 48000 Hz; WAVE_FORMAT_EXTENSIBLE with the PCM sub-format is accepted, other chunks are skipped, and a data chunk that claims more bytes than the file holds plays what is there. VOC (Creative Voice File, header checksum checked): sound blocks with 8-bit unsigned samples, continuation, silence, extended (rate and stereo for the next sound block) and new-format blocks with 8-bit or 16-bit PCM; markers and text are skipped, and a repeated section plays once. Compressed sound (ADPCM, 24-bit, floating point) gives AUDIO_ERR_UNSUPPORTED, a damaged file AUDIO_ERR_CORRUPT, a file that is neither AUDIO_ERR_FORMAT.
+
+.h2 The Mixer
+
+All sound is mixed in 16-bit integers at the rate the device achieved (the mixer never assumes the rate it asked for: a Sound Blaster Pro asked for 22,050 Hz plays at 22,222 Hz), and every source is resampled to it with linear interpolation. Gains combine volume, pan, context volume and master volume; the sum is clipped to 16 bits. The mixer runs from the sound card's own interrupt (the Sound Blaster, at the end of each half buffer of about 23 ms), or from the timer service for devices without one, so sound keeps playing however busy the programs are. It uses no floating point and touches only memory pinned for interrupts (sample and stream buffers are pinned when they are made).
+
+.h2 Settings
+
+DVX.INI section [audio]:
+
+.table
+ Key Meaning
+ --- -------
+ driver auto (default: find the hardware), or a driver name: sb, speaker, null
+ rate mixing rate asked for, 4000 to 48000 (default 22050)
+ bits 0 (the device's best), 8 or 16
+ channels 0 (the device's best), 1 or 2
+ voices voices mixed at once, 1 to 32 (default 16)
+ volume master volume in percent (default 100)
+ speakerPcm true: sampled sound on the PC speaker (see below); default false
+ fmTones false: tones always in the mixer, never on FM voices; default true
+ oplPort the FM chip's port, hex (default 388)
+ midi where MIDI music plays: auto (default: the FM chip, else music.lib's software synthesizer), mpu401, fm, software
+ mpuPort the MPU-401's port, hex (default 330); the port is only looked for with midi = mpu401
+ mpuMode what is on the MPU-401 (music.lib): gm (default), mt32 or raw
+ midiBank the OP2 instrument bank for FM MIDI (music.lib), a path from the DVX folder
+.endtable
+
+.h2 Desktop Sounds
+
+The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file (a path from the DVX folder), empty or missing for none; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). The default set, made by DVX's own sound generator at build time, is in AUDIO_SOUNDS_DIR (config/sounds).
+
+.table
+ Key Event
+ --- -----
+ default the default beep (dvxBeep; a message box without an icon)
+ startup DVX started
+ exit DVX is ending
+ error a message box with MB_ICONERROR
+ warning MB_ICONWARNING
+ question MB_ICONQUESTION
+ information MB_ICONINFO
+ windowOpen a window opened (not a dialog)
+ windowClose a window closed (not a dialog)
+.endtable
+
+.h2 Drivers
+
+.table
+ Driver Device
+ ------ ------
+ sb Sound Blaster family by DSP version: 1.x (8-bit single-cycle), 1.5 / 2.0 (8-bit auto-init, high-speed above 23,000 Hz on 2.0), Pro and Pro 2 (8-bit stereo at 11,025 or 22,050 Hz), 16 (8- or 16-bit, mono or stereo, any rate from 5,000 to 44,100 Hz). The port, IRQ and DMA channels come from the BLASTER variable (e.g. SET BLASTER=A220 I5 D1 H5); without it an SB16 is found at 220h or 240h and asked for its IRQ and DMA. If no interrupt arrives from the card within half a second (a wrong IRQ), the next driver is tried.
+ speaker The PC speaker. By default tones only: the timer chip plays the loudest square or triangle tone as a square wave, with no interrupts and no CPU cost; sampled sound and noise are not heard. With speakerPcm = true it plays everything by pulse-width modulation: the timer interrupt runs at 22,096 Hz and each interrupt sets the next pulse (about 5.7 bits). That costs a lot of CPU time: 22,096 interrupts a second plus the mixing. A 486DX2-66 spends a large share of its time on it (estimated 10 to 15 percent for the interrupts alone on plain DOS, much more under a memory manager or Windows) and can lose interrupts, which slows the sound and the DVX clock down. Meant for Pentium-class machines.
+ null No sound device: plays nothing, but voices still run their course and end, so programs behave the same.
+ opl (synthesizer) The OPL2 or OPL3 FM chip of an AdLib or Sound Blaster at 388h (oplPort), found by the AdLib timer test; the status register tells an OPL2 from an OPL3. Register writes are timed by status reads (3.3 and 23 us on an OPL2) with interrupts off around each index/data pair. Closing zeroes every register.
+ mpu401 (synthesizer) An MPU-401 MIDI port in UART mode at mpuPort, only with midi = mpu401: reset (FFh, answered FEh; tried twice, since a port left in UART mode takes the first FFh as data), then UART mode (3Fh). Bytes are sent when the port says it is ready (a short wait, else the caller sends later); input the port has (active sensing) is read and dropped. Closing sends All Notes Off on every channel and resets the port.
+.endtable
+
+audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen).
+
+Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Sound Blaster, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: of each kind (FM chip, MIDI port) the first that finds its hardware is kept, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.
+
+.h2 Writing a Driver
+
+A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
+
+A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM or AUDIO_SYNTH_MIDI, .open = ..., .close = ..., .write = ... or .put = ...). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready. Both run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver.
+
+.h2 BASIC
+
+src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends.
diff --git a/src/libs/kpunch/audio/audio.h b/src/libs/kpunch/audio/audio.h
new file mode 100644
index 00000000..abf1f324
--- /dev/null
+++ b/src/libs/kpunch/audio/audio.h
@@ -0,0 +1,504 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audio.h -- sound library (audio.lib)
+//
+// Sound for DVX programs through whatever sound hardware the computer
+// has. Every program opens its own audio context; any number of
+// programs play at the same time through one mixer, and everything a
+// program opened is stopped and released when it ends, however it ends.
+//
+// Samples a sound loaded from a WAV or VOC file (or memory, or raw
+// PCM) and played on voices: volume, pan, pitch, looping,
+// an end notification.
+// Tones square, triangle and noise tones of a frequency and length,
+// made by the mixer (on the PC speaker by its tone generator).
+// Streams a program's own sound (music, speech) at any rate: it
+// writes samples, or the library asks for them.
+// Notes a queue of tones and rests played back to back, each to the
+// exact sample (melodies; QBasic's PLAY and SOUND).
+// Volume one master volume for the whole system and one per
+// context.
+// FM the FM synthesizer chip (OPL2 / OPL3), when there is one:
+// two-operator voices shared by every program (tones use
+// them too), or the whole chip for one program at a time.
+// MIDI the MIDI port (MPU-401), for one program at a time.
+// Desktop the sounds DVX plays for its own events (starting, message
+// sounds boxes, ...), chosen in DVX.INI [sounds].
+//
+// The mixer works in 16-bit integers at the rate the sound device
+// achieved (22,050 Hz unless DVX.INI [audio] rate says otherwise),
+// resamples every source with linear interpolation and clips the sum.
+// It mixes from the device's own interrupt (or the timer service for
+// devices without one), so sound keeps playing however busy the
+// programs are. At most the configured voice limit plays at once;
+// starting one more drops the quietest.
+//
+// Square and triangle tones play on a free FM voice when the computer
+// has an FM chip (DVX.INI [audio] fmTones), else - or when every FM voice
+// is taken - the mixer makes them.
+//
+// Devices come from driver modules (audioDrv.h) found at start-up:
+// the one named by DVX.INI [audio] driver, else the Sound Blaster
+// family, the PC speaker and finally the silent null driver, which
+// keeps programs working on a computer without sound.
+//
+// Functions returning int32_t return AUDIO_OK or a voice id (> 0) on
+// success and a negative AUDIO_ERR_* code on failure (audioErrorText
+// says what it means). Nothing here may be called from an interrupt.
+#ifndef AUDIO_LIB_H
+#define AUDIO_LIB_H
+
+#include "dvxApp.h"
+#include "dvxTypes.h"
+
+#include
+#include
+
+// Volume of a voice, a context or the master: 0 (silent) .. AUDIO_VOLUME_MAX
+#define AUDIO_VOLUME_MAX 256
+
+// Pan of a voice: AUDIO_PAN_LEFT .. AUDIO_PAN_RIGHT (centre plays at full
+// volume on both sides; panning turns the other side down)
+#define AUDIO_PAN_LEFT (-256)
+#define AUDIO_PAN_CENTER 0
+#define AUDIO_PAN_RIGHT 256
+
+// Pitch of a voice as a speed ratio in 1/256ths: AUDIO_PITCH_NORMAL plays
+// at the recorded speed, twice that an octave higher.
+#define AUDIO_PITCH_NORMAL 256
+#define AUDIO_PITCH_MIN 64
+#define AUDIO_PITCH_MAX 1024
+
+// Sample rates a sound, a stream or the device may have (Hz)
+#define AUDIO_RATE_MIN 4000
+#define AUDIO_RATE_MAX 48000
+#define AUDIO_RATE_DEFAULT 22050
+
+// Tone frequencies (Hz)
+#define AUDIO_TONE_MIN_HZ 20
+#define AUDIO_TONE_MAX_HZ 20000
+
+// Voices: the most that can exist, and the default limit of voices mixed
+// at once (DVX.INI [audio] voices, 1..AUDIO_VOICES_MAX)
+#define AUDIO_VOICES_MAX 32
+#define AUDIO_VOICES_DEFAULT 16
+
+// Note queues: the most entries (tones and rests) waiting to start, and
+// the unit of their frequencies (1/1000 Hz; 0 is a rest)
+#define AUDIO_NOTES_MAX 256
+#define AUDIO_MILLIHZ_PER_HZ 1000
+
+// Stream buffer length (milliseconds of sound)
+#define AUDIO_STREAM_MS_MIN 20
+#define AUDIO_STREAM_MS_DEFAULT 250
+#define AUDIO_STREAM_MS_MAX 2000
+
+// Text sizes in AudioInfoT (with the terminating zero)
+#define AUDIO_NAME_MAX 16
+#define AUDIO_DESC_MAX 64
+
+// Results
+#define AUDIO_OK 0
+#define AUDIO_ERR_ARG (-1) // a bad argument
+#define AUDIO_ERR_NO_MEMORY (-2) // out of memory
+#define AUDIO_ERR_OPEN (-3) // the file could not be read
+#define AUDIO_ERR_FORMAT (-4) // not a WAV or VOC file
+#define AUDIO_ERR_UNSUPPORTED (-5) // compressed or unusual sound data
+#define AUDIO_ERR_CORRUPT (-6) // damaged or incomplete file
+#define AUDIO_ERR_NO_VOICE (-7) // voice limit reached, and this sound is the quietest
+#define AUDIO_ERR_NO_DEVICE (-8) // no sound driver could be started
+#define AUDIO_ERR_VOICE (-9) // no such voice in this context (ended, or never was)
+#define AUDIO_ERR_BUSY (-10) // the synthesizer is in use by another program, or not ready
+#define AUDIO_ERR_NO_SYNTH (-11) // no such synthesizer (no FM chip, no MIDI port)
+#define AUDIO_ERR_COUNT 12 // AUDIO_OK and the eleven errors
+
+// Opaque handles
+typedef struct AudioCtxT AudioCtxT;
+typedef struct AudioNotesT AudioNotesT;
+typedef struct AudioSampleT AudioSampleT;
+typedef struct AudioStreamT AudioStreamT;
+
+// Tone waveforms
+typedef enum {
+ AUDIO_WAVE_SQUARE = 0,
+ AUDIO_WAVE_TRIANGLE = 1,
+ AUDIO_WAVE_NOISE = 2
+} AudioWaveE;
+
+// Why a voice ended
+typedef enum {
+ AUDIO_END_FINISHED = 0, // played to its end (or its tone length)
+ AUDIO_END_STOPPED = 1, // audioVoiceStop, audioStopAll, its sample or stream closed
+ AUDIO_END_DROPPED = 2 // the voice limit dropped it for a louder sound
+} AudioEndE;
+
+// PCM layout: rate in Hz, 8 (unsigned) or 16 (signed, little-endian)
+// bits, 1 or 2 channels (stereo interleaved left, right).
+typedef struct {
+ uint32_t rate;
+ uint32_t bits;
+ uint32_t channels;
+} AudioFormatT;
+
+// The sound device in use (audioGetInfo).
+typedef struct {
+ char driver[AUDIO_NAME_MAX]; // driver name ("sb", "speaker", "null"); "" when none runs
+ char description[AUDIO_DESC_MAX]; // for people, e.g. "Sound Blaster 16 (DSP 4.05)"
+ uint32_t rate; // mixing rate the device achieved (Hz)
+ uint32_t bits; // output bits
+ uint32_t channels; // output channels
+ uint32_t voiceLimit; // voices mixed at once
+ bool running; // a device is playing (some context is open)
+ bool hasDac; // plays sampled sound (else tones only, or nothing)
+ bool hasSynth; // has a hardware music synthesizer
+ bool hasTone; // plays tones on a hardware tone generator
+} AudioInfoT;
+
+// Mixer counters since the device started (audioGetStats).
+typedef struct {
+ uint32_t renders; // blocks the device asked the mixer for
+ uint32_t frames; // frames mixed
+ uint32_t underruns; // stream frames that were due but not yet written
+ uint32_t clipped; // output samples clipped to the 16-bit range
+ uint32_t dropped; // voices dropped (or refused) by the voice limit
+ uint32_t playing; // voices playing now
+} AudioStatsT;
+
+// A voice of ctx ended. Called from audioPump or the shell's idle time,
+// never from an interrupt, as the program that owns ctx.
+typedef void (*AudioEndFnT)(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData);
+
+// A pull stream wants up to frames frames in its own format in buf.
+// Return how many were written (fewer, even 0, is fine: the rest is asked
+// for later). Called from audioPump or the shell's idle time.
+typedef uint32_t (*AudioFillFnT)(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
+
+// ============================================================
+// Contexts, device and volume
+// ============================================================
+
+// Open an audio context for the calling program; the first one opened
+// starts the sound device. NULL when out of memory or when no driver
+// could be started. Every context is closed for its program when the
+// program ends.
+AudioCtxT *audioOpen(void);
+
+// Stop every voice of ctx, free its samples and streams and close it; the
+// last one closed stops the device. NULL is ignored.
+void audioClose(AudioCtxT *ctx);
+
+// Text for an AUDIO_OK / AUDIO_ERR_* code.
+const char *audioErrorText(int32_t err);
+
+// The device in use (running false and driver "" while none is open).
+void audioGetInfo(AudioInfoT *info);
+
+// Mixer counters since the device started.
+void audioGetStats(AudioStatsT *stats);
+
+// Master volume of all sound (0..AUDIO_VOLUME_MAX; DVX.INI [audio]
+// volume, in percent, sets it when the device starts).
+int32_t audioGetMasterVolume(void);
+void audioSetMasterVolume(int32_t volume);
+
+// Volume of everything ctx plays (0..AUDIO_VOLUME_MAX, default the
+// maximum).
+int32_t audioGetVolume(const AudioCtxT *ctx);
+void audioSetVolume(AudioCtxT *ctx, int32_t volume);
+
+// Call fn when a voice of ctx ends (NULL: no calls).
+void audioSetEndFn(AudioCtxT *ctx, AudioEndFnT fn, void *userData);
+
+// Deliver end notifications and fill pull streams now. The shell does
+// this in its idle time; a program that keeps the desktop busy every
+// frame should call it once a frame.
+void audioPump(void);
+
+// Stop every voice of ctx.
+void audioStopAll(AudioCtxT *ctx);
+
+// ============================================================
+// Samples and voices
+// ============================================================
+
+// Load a WAV (PCM, 8 or 16 bits, mono or stereo, any other chunks
+// skipped) or VOC (8-bit, 16-bit, silence and extended blocks) sound from
+// a file or from memory.
+int32_t audioSampleLoad(AudioCtxT *ctx, const char *path, AudioSampleT **out);
+int32_t audioSampleLoadMem(AudioCtxT *ctx, const void *data, uint32_t size, AudioSampleT **out);
+
+// A sample from raw PCM frames in format fmt.
+int32_t audioSampleCreate(AudioCtxT *ctx, const void *pcm, uint32_t frames, const AudioFormatT *fmt, AudioSampleT **out);
+
+// Stop the voices playing it and free it. NULL is ignored.
+void audioSampleFree(AudioSampleT *sample);
+
+// The sample's recorded format and length in frames.
+void audioSampleInfo(const AudioSampleT *sample, AudioFormatT *fmt, uint32_t *frames);
+
+// Play sample on a new voice at volume and pan, from its start, once or
+// looping until stopped. Returns the voice id.
+int32_t audioPlay(AudioSampleT *sample, int32_t volume, int32_t pan, bool loop);
+
+// Load a sound file and play it; the sample is freed when the voice ends.
+int32_t audioPlayFile(AudioCtxT *ctx, const char *path, int32_t volume, int32_t pan, bool loop);
+
+// Play a tone of hz for ms milliseconds (0: until stopped).
+int32_t audioTone(AudioCtxT *ctx, AudioWaveE wave, uint32_t hz, int32_t volume, uint32_t ms);
+
+// Change or stop a voice of ctx; AUDIO_ERR_VOICE when it is not one
+// (any more). Pitch changes a sample's or stream's speed (and a tone's
+// frequency); SetFrequency sets a tone's frequency.
+int32_t audioVoiceSetFrequency(AudioCtxT *ctx, int32_t voice, uint32_t hz);
+int32_t audioVoiceSetPan(AudioCtxT *ctx, int32_t voice, int32_t pan);
+int32_t audioVoiceSetPitch(AudioCtxT *ctx, int32_t voice, int32_t pitch);
+int32_t audioVoiceSetVolume(AudioCtxT *ctx, int32_t voice, int32_t volume);
+int32_t audioVoiceStop(AudioCtxT *ctx, int32_t voice);
+
+// True while the voice of ctx is still playing.
+bool audioVoiceIsPlaying(const AudioCtxT *ctx, int32_t voice);
+
+// ============================================================
+// Streams
+// ============================================================
+
+// Open a stream of format fmt with a buffer of bufferMs milliseconds (0:
+// AUDIO_STREAM_MS_DEFAULT). It starts playing at once on its own voice
+// (silence until samples arrive). With fill, the library asks for
+// samples whenever the buffer has room; without, the program writes them
+// with audioStreamWrite.
+int32_t audioStreamOpen(AudioCtxT *ctx, const AudioFormatT *fmt, uint32_t bufferMs, AudioFillFnT fill, void *userData, AudioStreamT **out);
+
+// Stop and free the stream. NULL is ignored.
+void audioStreamClose(AudioStreamT *stream);
+
+// Frames buffered and not yet played, and frames of room left.
+uint32_t audioStreamQueued(const AudioStreamT *stream);
+uint32_t audioStreamSpace(const AudioStreamT *stream);
+
+// The stream's voice id (for volume, pan and pitch).
+int32_t audioStreamVoice(const AudioStreamT *stream);
+
+// Append up to frames frames in the stream's format; returns how many fit.
+uint32_t audioStreamWrite(AudioStreamT *stream, const void *pcm, uint32_t frames);
+
+// ============================================================
+// Note queues
+// ============================================================
+//
+// Tones and rests played one after the other, each lasting exactly as
+// long as asked (to the output frame, with no drift over a long tune) as
+// long as the next is queued before the one before it ends. A queue
+// plays on its own voice, in the mixer or on the PC speaker's tone
+// generator (never on the FM chip), and the voice limit never drops it;
+// it is silent while empty.
+
+// A queue of wave tones (square, triangle or noise) at volume
+// (0..AUDIO_VOLUME_MAX; audioVoiceSetVolume on audioNotesVoice changes it).
+int32_t audioNotesOpen(AudioCtxT *ctx, AudioWaveE wave, int32_t volume, AudioNotesT **out);
+
+// Stop and free the queue. NULL is ignored.
+void audioNotesClose(AudioNotesT *notes);
+
+// Append a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ ..
+// AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds.
+// AUDIO_ERR_BUSY when AUDIO_NOTES_MAX entries are already waiting.
+int32_t audioNotesAdd(AudioNotesT *notes, uint32_t milliHz, uint32_t us);
+
+// Silence the queue now and forget everything in it.
+void audioNotesClear(AudioNotesT *notes);
+
+// Entries not yet finished (playing or waiting; 0 when the queue has gone
+// quiet); tones, when not NULL, gets how many of them are tones rather
+// than rests.
+uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones);
+
+// Entries that can still be added.
+uint32_t audioNotesSpace(const AudioNotesT *notes);
+
+// The queue's voice id (for volume and pan).
+int32_t audioNotesVoice(const AudioNotesT *notes);
+
+// ============================================================
+// FM synthesizer
+// ============================================================
+//
+// The FM chip's two-operator voices are shared: a program takes one with
+// audioFmAlloc, programs it with audioFmWrite and gives it back with
+// audioFmFree (closing the context gives back all of its voices). Voices
+// are handed out least recently used first, so a released note's tail
+// is cut last. audioFmWrite takes the registers of the voice's own
+// channel, numbered as for the first channel of an OPL2 (AUDIO_FM_*
+// below); the library adds the channel's offsets. On an OPL3 a
+// AUDIO_FM_FEEDBACK value without either output bit plays on both sides.
+//
+// A program may instead take the whole chip (audioFmRawAcquire): every
+// voice of the other programs is silenced and taken back (their tones
+// go on in the mixer), new voices are refused until the chip is given
+// back, and a second program asking for the chip gets AUDIO_ERR_BUSY.
+//
+// audioFmAlloc, audioFmFree, audioFmWrite and audioFmRawWrite may also be
+// called from a timer callback (platformTimerAdd), as music playback does.
+
+// Most voices an FM chip has (OPL3; an OPL2 has 9)
+#define AUDIO_FM_VOICES_MAX 18
+
+// The chip's own sample rate (Hz), which frequency numbers are based on:
+// hz = fnum * AUDIO_FM_CHIP_HZ * 2^block / 2^20
+#define AUDIO_FM_CHIP_HZ 49716
+#define AUDIO_FM_FNUM_MAX 1023
+#define AUDIO_FM_BLOCK_MAX 7
+#define AUDIO_FM_FNUM_HI_SHIFT 8 // fnum bits 8-9 go in AUDIO_FM_KEY_BLOCK bits 0-1
+#define AUDIO_FM_BLOCK_SHIFT 2 // block in AUDIO_FM_KEY_BLOCK bits 2-4
+
+// Channel registers for audioFmWrite (modulator / carrier operator)
+#define AUDIO_FM_MOD_CHAR 0x20 // tremolo, vibrato, sustaining, key scale rate, multiple
+#define AUDIO_FM_CAR_CHAR 0x23
+#define AUDIO_FM_MOD_LEVEL 0x40 // key scale level (bits 6-7), attenuation (bits 0-5, 0.75 dB)
+#define AUDIO_FM_CAR_LEVEL 0x43
+#define AUDIO_FM_MOD_ATTACK 0x60 // attack rate (bits 4-7), decay rate (bits 0-3)
+#define AUDIO_FM_CAR_ATTACK 0x63
+#define AUDIO_FM_MOD_SUSTAIN 0x80 // sustain level (bits 4-7), release rate (bits 0-3)
+#define AUDIO_FM_CAR_SUSTAIN 0x83
+#define AUDIO_FM_FNUM_LOW 0xA0 // frequency number bits 0-7
+#define AUDIO_FM_KEY_BLOCK 0xB0 // key on, block, frequency number bits 8-9
+#define AUDIO_FM_FEEDBACK 0xC0 // outputs, feedback (bits 1-3), additive (bit 0)
+#define AUDIO_FM_MOD_WAVE 0xE0 // waveform
+#define AUDIO_FM_CAR_WAVE 0xE3
+
+// Bits of those registers
+#define AUDIO_FM_KEY_ON 0x20 // AUDIO_FM_KEY_BLOCK: sounding
+#define AUDIO_FM_OUT_LEFT 0x10 // AUDIO_FM_FEEDBACK: left output (OPL3)
+#define AUDIO_FM_OUT_RIGHT 0x20 // AUDIO_FM_FEEDBACK: right output (OPL3)
+#define AUDIO_FM_OUT_BOTH (AUDIO_FM_OUT_LEFT | AUDIO_FM_OUT_RIGHT)
+#define AUDIO_FM_ADDITIVE 0x01 // AUDIO_FM_FEEDBACK: both operators heard
+
+// The FM chip (audioFmGetInfo); voices 0 when there is none.
+typedef struct {
+ char description[AUDIO_DESC_MAX]; // e.g. "OPL3 FM synthesizer at 388h"
+ uint32_t voices; // two-operator voices
+ uint32_t freeVoices; // not taken by any program
+ bool stereo; // voices can be panned (OPL3)
+ bool rawOwned; // one program has the whole chip
+} AudioFmInfoT;
+
+// Take a free FM voice for ctx. Returns its id (> 0), AUDIO_ERR_NO_VOICE
+// when all are taken, AUDIO_ERR_BUSY while a program has the whole chip,
+// AUDIO_ERR_NO_SYNTH without an FM chip.
+int32_t audioFmAlloc(AudioCtxT *ctx);
+
+// Silence the voice (key off) and give it back.
+int32_t audioFmFree(AudioCtxT *ctx, int32_t voice);
+
+// The FM chip in use.
+void audioFmGetInfo(AudioFmInfoT *info);
+
+// Set a register of the voice's channel (AUDIO_FM_*).
+int32_t audioFmWrite(AudioCtxT *ctx, int32_t voice, uint8_t reg, uint8_t value);
+
+// The whole chip for ctx alone; audioFmRawWrite sets any register
+// (bank * AUDIO_FM_BANK_REGS + index, bank 1 on an OPL3 only). Release
+// silences the chip and gives its voices back to everyone.
+int32_t audioFmRawAcquire(AudioCtxT *ctx);
+int32_t audioFmRawRelease(AudioCtxT *ctx);
+int32_t audioFmRawWrite(AudioCtxT *ctx, uint32_t reg, uint8_t value);
+
+// ============================================================
+// MIDI port
+// ============================================================
+//
+// The MIDI port (an MPU-401 in UART mode, used only when DVX.INI [audio]
+// midi = mpu401) belongs to one program at a time. audioMidiPut may
+// also be called from a timer callback; it returns AUDIO_ERR_BUSY when
+// the port cannot take a byte yet (send it again later).
+
+// The MIDI port (audioMidiGetInfo).
+typedef struct {
+ char description[AUDIO_DESC_MAX]; // e.g. "MPU-401 MIDI port at 330h"; "" when none
+ bool present;
+ bool owned; // a program is using it
+} AudioMidiInfoT;
+
+int32_t audioMidiAcquire(AudioCtxT *ctx);
+void audioMidiGetInfo(AudioMidiInfoT *info);
+int32_t audioMidiPut(AudioCtxT *ctx, uint8_t byte);
+int32_t audioMidiRelease(AudioCtxT *ctx);
+
+// ============================================================
+// Desktop sounds
+// ============================================================
+//
+// The sounds DVX plays for its events (dvxPlaySound, DvxSoundE in
+// dvxApp.h). Each event's sound is a WAV or VOC file named in DVX.INI
+// [sounds] under the event's key (a path from the DVX folder; empty or
+// missing: none). The library connects itself to DVX when it loads and
+// plays them on a context of its own, so they belong to no program; with
+// no sound hardware they are silent. The default set lives in
+// AUDIO_SOUNDS_DIR.
+
+#define AUDIO_SOUNDS_SECTION "sounds"
+#define AUDIO_SOUNDS_DIR "config/sounds"
+
+// The DVX.INI key of an event ("startup", "error", ...), and its name for
+// people ("Start DVX", "Error", ...); NULL for an event out of range.
+const char *audioDeskKey(DvxSoundE event);
+const char *audioDeskLabel(DvxSoundE event);
+
+// Play the event's sound (the DvxSoundHookT that DVX calls); with wait,
+// return once it has played (at most a few seconds).
+void audioDeskPlay(DvxSoundE event, bool wait);
+
+// Play a sound file the way desktop sounds play (to try one out).
+// Returns the voice id or an AUDIO_ERR_* code.
+int32_t audioDeskPlayFile(const char *path);
+
+// DVX.INI [sounds] has changed: read it again before the next sound.
+void audioDeskReload(void);
+
+// ============================================================
+// Drivers
+// ============================================================
+
+// The short names of the sound drivers installed (DVX.INI [audio]
+// driver), the one tried first first; returns how many were put in names
+// (at most max).
+int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max);
+
+// ============================================================
+// BASIC (src/include/basic/sound.bas)
+// ============================================================
+
+// Each BASIC program gets its own context on first use. Volumes are in
+// percent (0..100); a voice number is 0 on failure (SoundError$ says why).
+int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume);
+int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume);
+int32_t PlayWave(const char *fileName);
+int32_t PlayWaveLoop(const char *fileName);
+const char *SoundError(void);
+const char *SoundInfo(void);
+int32_t SoundPlaying(int32_t voice);
+void SoundVolume(int32_t volume);
+void StopAllSounds(void);
+void StopWave(int32_t voice);
+
+#endif // AUDIO_LIB_H
diff --git a/src/libs/kpunch/audio/audioBas.c b/src/libs/kpunch/audio/audioBas.c
new file mode 100644
index 00000000..2a8a3c48
--- /dev/null
+++ b/src/libs/kpunch/audio/audioBas.c
@@ -0,0 +1,180 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioBas.c -- DVX BASIC entry points of the sound library
+//
+// Called through DECLARE LIBRARY (src/include/basic/sound.bas), so the
+// signatures use only BASIC-marshalable types. Each program plays
+// through its own context (audioBasicContext), which the library closes
+// when the program ends. Volumes are percent; a voice number is 0 when
+// the sound could not be started, and SoundError$ then says why.
+
+#include "audioInt.h"
+
+#include
+
+#define BAS_TRUE (-1)
+#define BAS_FALSE 0
+#define PERCENT_MAX 100
+#define BAS_INFO_MAX (AUDIO_DESC_MAX + 48)
+
+static char sInfo[BAS_INFO_MAX];
+static int32_t sLastError = AUDIO_OK;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t basPercent(int32_t percent);
+static int32_t basResult(int32_t rc);
+static int32_t basTone(AudioWaveE wave, int32_t frequency, int32_t milliseconds, int32_t volume);
+static int32_t basWave(const char *fileName, bool loop);
+int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume);
+int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume);
+int32_t PlayWave(const char *fileName);
+int32_t PlayWaveLoop(const char *fileName);
+const char *SoundError(void);
+const char *SoundInfo(void);
+int32_t SoundPlaying(int32_t voice);
+void SoundVolume(int32_t volume);
+void StopAllSounds(void);
+void StopWave(int32_t voice);
+
+
+static int32_t basPercent(int32_t percent) {
+ if (percent < 0) {
+ percent = 0;
+ } else if (percent > PERCENT_MAX) {
+ percent = PERCENT_MAX;
+ }
+
+ return percent * AUDIO_VOLUME_MAX / PERCENT_MAX;
+}
+
+
+// A voice id, or 0 with the error kept for SoundError$.
+static int32_t basResult(int32_t rc) {
+ sLastError = rc < 0 ? rc : AUDIO_OK;
+ return rc < 0 ? 0 : rc;
+}
+
+
+static int32_t basTone(AudioWaveE wave, int32_t frequency, int32_t milliseconds, int32_t volume) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ if (!ctx) {
+ return basResult(AUDIO_ERR_NO_DEVICE);
+ }
+
+ if (frequency < 0 || milliseconds < 0) {
+ return basResult(AUDIO_ERR_ARG);
+ }
+
+ return basResult(audioTone(ctx, wave, (uint32_t)frequency, basPercent(volume), (uint32_t)milliseconds));
+}
+
+
+static int32_t basWave(const char *fileName, bool loop) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ if (!ctx) {
+ return basResult(AUDIO_ERR_NO_DEVICE);
+ }
+
+ return basResult(audioPlayFile(ctx, fileName, AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, loop));
+}
+
+
+int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume) {
+ return basTone(AUDIO_WAVE_NOISE, frequency, milliseconds, volume);
+}
+
+
+int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume) {
+ return basTone(AUDIO_WAVE_SQUARE, frequency, milliseconds, volume);
+}
+
+
+int32_t PlayWave(const char *fileName) {
+ return basWave(fileName, false);
+}
+
+
+int32_t PlayWaveLoop(const char *fileName) {
+ return basWave(fileName, true);
+}
+
+
+const char *SoundError(void) {
+ return audioErrorText(sLastError);
+}
+
+
+// Opens the program's context, so the device is started and described.
+const char *SoundInfo(void) {
+ AudioInfoT info;
+
+ audioBasicContext();
+ audioGetInfo(&info);
+
+ if (!info.running) {
+ snprintf(sInfo, sizeof(sInfo), "No sound device");
+ } else {
+ snprintf(sInfo, sizeof(sInfo), "%s, %lu Hz, %lu-bit %s", info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels > 1 ? "stereo" : "mono");
+ }
+
+ return sInfo;
+}
+
+
+int32_t SoundPlaying(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ return (ctx && audioVoiceIsPlaying(ctx, voice)) ? BAS_TRUE : BAS_FALSE;
+}
+
+
+void SoundVolume(int32_t volume) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ if (ctx) {
+ audioSetVolume(ctx, basPercent(volume));
+ }
+}
+
+
+void StopAllSounds(void) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ if (ctx) {
+ audioStopAll(ctx);
+ }
+}
+
+
+void StopWave(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ if (ctx) {
+ audioVoiceStop(ctx, voice);
+ }
+}
diff --git a/src/libs/kpunch/audio/audioDesk.c b/src/libs/kpunch/audio/audioDesk.c
new file mode 100644
index 00000000..a6ddb2c9
--- /dev/null
+++ b/src/libs/kpunch/audio/audioDesk.c
@@ -0,0 +1,181 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioDesk.c -- desktop sounds: what DVX plays for its own events
+//
+// DVX calls audioDeskPlay (the sound hook, set by audioDeskInit) for an
+// event; the event's file comes from DVX.INI [sounds], read at the first
+// sound and again after audioDeskReload. The sounds play on a context
+// opened for no program, so no program's ending cuts one short. Once the
+// exit sound has played DVX is ending: later events (windows closing as
+// the programs are ended) stay quiet.
+
+#include "audioInt.h"
+#include "dvxPrefs.h"
+
+#include
+
+// Longest a caller waits for a sound to play (dvxPlaySoundWait)
+#define DESK_WAIT_MAX_US (5 * PLATFORM_US_PER_SEC)
+
+typedef struct {
+ const char *key; // DVX.INI [sounds] key
+ const char *label; // for people
+} DeskEventT;
+
+static const DeskEventT sEvents[SoundCountE] = {
+ [SoundDefaultE] = { "default", "Default beep" },
+ [SoundStartE] = { "startup", "Start DVX" },
+ [SoundExitE] = { "exit", "Exit DVX" },
+ [SoundErrorE] = { "error", "Error" },
+ [SoundWarningE] = { "warning", "Warning" },
+ [SoundQuestionE] = { "question", "Question" },
+ [SoundInfoE] = { "information", "Information" },
+ [SoundWindowOpenE] = { "windowOpen", "Open window" },
+ [SoundWindowCloseE] = { "windowClose", "Close window" }
+};
+
+static AudioCtxT *sDeskCtx = NULL;
+static char sFiles[SoundCountE][DVX_MAX_PATH];
+static bool sLoaded = false;
+static bool sEnding = false;
+static bool sNoDevice = false; // no sound device started: not tried again until a reload
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void audioDeskInit(void);
+const char *audioDeskKey(DvxSoundE event);
+const char *audioDeskLabel(DvxSoundE event);
+void audioDeskPlay(DvxSoundE event, bool wait);
+int32_t audioDeskPlayFile(const char *path);
+void audioDeskReload(void);
+void audioDeskShutdown(void);
+static bool eventValid(DvxSoundE event);
+static void filesLoad(void);
+
+
+void audioDeskInit(void) {
+ sEnding = false;
+ dvxSetSoundHook(audioDeskPlay);
+}
+
+
+const char *audioDeskKey(DvxSoundE event) {
+ return eventValid(event) ? sEvents[event].key : NULL;
+}
+
+
+const char *audioDeskLabel(DvxSoundE event) {
+ return eventValid(event) ? sEvents[event].label : NULL;
+}
+
+
+void audioDeskPlay(DvxSoundE event, bool wait) {
+ if (!eventValid(event) || sEnding) {
+ return;
+ }
+
+ filesLoad();
+
+ if (!sFiles[event][0]) {
+ return;
+ }
+
+ int32_t voice = audioDeskPlayFile(sFiles[event]);
+
+ sEnding = event == SoundExitE;
+
+ if (!wait || voice <= 0) {
+ return;
+ }
+
+ uint64_t start = platformMicros();
+
+ while (audioVoiceIsPlaying(sDeskCtx, voice) && platformMicros() - start < DESK_WAIT_MAX_US) {
+ audioPump();
+ platformYield();
+ }
+}
+
+
+// Everything is done as no program, so the memory a sound takes is the
+// library's own. When no sound device starts (audioOpen logs why) the
+// sounds stay silent without trying again at every event.
+int32_t audioDeskPlayFile(const char *path) {
+ int32_t prev = audioAppEnter(0);
+ int32_t rc = AUDIO_ERR_NO_DEVICE;
+
+ if (!sDeskCtx && !sNoDevice) {
+ sDeskCtx = audioOpen();
+ sNoDevice = !sDeskCtx;
+ }
+
+ if (sDeskCtx) {
+ rc = audioPlayFile(sDeskCtx, path, AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, false);
+
+ if (rc < 0) {
+ dvxLog("Audio: desktop sound %s: %s", path, audioErrorText(rc));
+ }
+ }
+
+ audioAppEnter(prev);
+ return rc;
+}
+
+
+void audioDeskReload(void) {
+ sLoaded = false;
+ sNoDevice = false;
+}
+
+
+// The library is closing every context (this one too): forget it and
+// take the hook back.
+void audioDeskShutdown(void) {
+ dvxSetSoundHook(NULL);
+ sDeskCtx = NULL;
+ sLoaded = false;
+ sNoDevice = false;
+}
+
+
+static bool eventValid(DvxSoundE event) {
+ return (int32_t)event >= 0 && event < SoundCountE;
+}
+
+
+static void filesLoad(void) {
+ if (sLoaded) {
+ return;
+ }
+
+ PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH);
+
+ for (int32_t i = 0; i < SoundCountE; i++) {
+ snprintf(sFiles[i], sizeof(sFiles[i]), "%s", prefsGetString(prefs, AUDIO_SOUNDS_SECTION, sEvents[i].key, ""));
+ }
+
+ prefsClose(prefs);
+ sLoaded = true;
+}
diff --git a/src/libs/kpunch/audio/audioDrv.h b/src/libs/kpunch/audio/audioDrv.h
new file mode 100644
index 00000000..43eac24e
--- /dev/null
+++ b/src/libs/kpunch/audio/audioDrv.h
@@ -0,0 +1,232 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioDrv.h -- sound driver interface of audio.lib
+//
+// (Synthesizers - FM chips and MIDI ports - have a descriptor of their
+// own; see "Synthesizer drivers" below.)
+//
+// A sound driver is a driver module (dvxDrv.h): a file .drv in
+// AUDIO_DRV_DIR, exporting one AudioDriverT named audioDriver (write it
+// with AUDIO_DRIVER(...)). audio.lib finds the drivers when the first
+// program opens sound - nothing lists them - and tries them in this
+// order: the driver named by DVX.INI [audio] driver (unless "auto"), then
+// the others from the highest priority down. The first whose open and
+// play succeed is used; the others are unloaded.
+//
+// open detects the hardware and decides the output format; it fills in
+// AudioDrvInfoT (the achieved rate: the mixer runs at exactly that rate)
+// and must not make a sound yet. play starts output: from then on the
+// driver calls audioMixRender for every block of output, from its own
+// interrupt or from the timer service (audioClockStart does that for a
+// device that plays nothing it has to feed). close stops output and
+// releases everything; after it returns audioMixRender is not called
+// again. A driver registers a shutdown hook (platformShutdownHookAdd) if
+// its hardware would keep running when DVX ends without closing it.
+//
+// Interrupt-time rules: code and data an interrupt touches must be pinned
+// (audioLockSpan for a module's own functions and data; the mixer and the
+// sound data are pinned by audio.lib), must not call the C library, DVX
+// or DOS, and must not use floating point.
+#ifndef AUDIO_DRV_H
+#define AUDIO_DRV_H
+
+#include "audio.h"
+#include "dvxDrv.h"
+#include "dvxPlat.h"
+#include "dvxPrefs.h"
+
+// Where drivers are found (relative to the DVX directory) and the name
+// of the descriptor each exports.
+#define AUDIO_DRV_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "audio" DVX_PATH_SEP "drivers"
+#define AUDIO_DRV_SYMBOL "audioDriver"
+
+// DVX.INI section holding the sound settings (drivers read their own
+// keys there too)
+#define AUDIO_INI_SECTION "audio"
+
+// DVX.INI [audio] driver: the driver tried first (its short name), or
+// auto; volume: the master volume in percent
+#define AUDIO_INI_DRIVER "driver"
+#define AUDIO_INI_VOLUME "volume"
+#define AUDIO_DRIVER_AUTO "auto"
+
+// DVX.INI [audio] midi: where MIDI music plays. auto: the FM chip when
+// there is one, else the software synthesizer; mpu401: the MPU-401 MIDI
+// port (at mpuPort, hex, default AUDIO_MPU_PORT_DEFAULT; the port is only
+// looked for with this setting, since an unused port would play nothing);
+// fm, software: those.
+#define AUDIO_INI_MIDI "midi"
+#define AUDIO_INI_MPU_PORT "mpuPort"
+#define AUDIO_MIDI_AUTO "auto"
+#define AUDIO_MIDI_MPU401 "mpu401"
+#define AUDIO_MIDI_FM "fm"
+#define AUDIO_MIDI_SOFTWARE "software"
+#define AUDIO_MPU_PORT_DEFAULT "330"
+
+// Driver priorities (tried from the highest down)
+#define AUDIO_PRIORITY_CARD 100 // sound cards
+#define AUDIO_PRIORITY_SPEAKER 20 // the PC speaker
+#define AUDIO_PRIORITY_NULL 0 // plays nothing
+
+// Rate of the timer that drives devices without an interrupt of their own
+// (audioClockStart); each call mixes rate / AUDIO_CLOCK_HZ frames.
+#define AUDIO_CLOCK_HZ 250
+
+// What audio.lib asks of a driver's open. bits / channels 0 mean the
+// best the device can do.
+typedef struct {
+ PrefsHandleT *prefs; // DVX.INI (read-only), section AUDIO_INI_SECTION
+ uint32_t rate; // wanted mixing rate (Hz)
+ uint32_t bits; // wanted output bits: 0, 8 or 16
+ uint32_t channels; // wanted output channels: 0, 1 or 2
+} AudioDrvConfigT;
+
+// Hardware tone generator: sound hz (0: silence). Called at interrupt
+// time.
+typedef void (*AudioToneFnT)(uint32_t hz);
+
+// What a driver's open found.
+typedef struct {
+ char description[AUDIO_DESC_MAX]; // for people (audioGetInfo)
+ uint32_t rate; // achieved output rate (Hz)
+ uint32_t bits; // 8 (unsigned) or 16 (signed)
+ uint32_t channels; // 1 or 2
+ AudioToneFnT tone; // hardware tones, or NULL
+ bool hasDac; // plays sampled sound
+ bool hasSynth; // has a hardware synthesizer
+} AudioDrvInfoT;
+
+// The descriptor every driver exports as AUDIO_DRV_SYMBOL.
+// hdr.name is the short name (DVX.INI driver =, below AUDIO_NAME_MAX),
+// hdr.priority one of AUDIO_PRIORITY_*.
+typedef struct {
+ DvxDriverHeaderT hdr;
+ bool (*open)(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
+ bool (*play)(void);
+ void (*close)(void);
+} AudioDriverT;
+
+// Write a driver's descriptor: AUDIO_DRIVER("x", AUDIO_PRIORITY_CARD,
+// .open = ..., .play = ..., .close = ...);
+#define AUDIO_DRIVER(drvName, drvPriority, ...) extern const AudioDriverT audioDriver; const AudioDriverT audioDriver = { .hdr = DVX_DRIVER_HDR(AudioDriverT, drvName, drvPriority), __VA_ARGS__ }
+
+// One object for audioLockSpan: data, or a function (size 0), which
+// counts as its first AUDIO_LOCK_CODE_BYTES bytes.
+#define AUDIO_LOCK_CODE_BYTES 4096
+
+typedef struct {
+ const void *addr;
+ uint32_t size;
+} AudioLockItemT;
+
+// ============================================================
+// Synthesizer drivers
+// ============================================================
+//
+// A module in AUDIO_DRV_DIR may export an AudioSynthDriverT named
+// AUDIO_SYNTH_SYMBOL instead (write it with AUDIO_SYNTH_DRIVER(...)): a
+// music synthesizer beside the sound device - an FM chip (AUDIO_SYNTH_FM,
+// programmed by register writes) or a MIDI port (AUDIO_SYNTH_MIDI, sent
+// bytes). When the sound device starts, the synth drivers of each kind
+// are opened from the highest priority down and the first that finds its
+// hardware is kept (it is closed with the device). open detects and must
+// leave the hardware silent; close silences it. write and put are called
+// at interrupt time as well (music playback runs from the timer service),
+// so they follow the interrupt-time rules above, and write must make its
+// index/data pair atomic (interrupts off around it).
+
+#define AUDIO_SYNTH_SYMBOL "audioSynth"
+
+// Synthesizer kinds
+#define AUDIO_SYNTH_FM 0 // OPL2 / OPL3 FM chip
+#define AUDIO_SYNTH_MIDI 1 // MIDI port (MPU-401)
+#define AUDIO_SYNTH_KINDS 2
+
+// FM register numbers passed to write: bank (0, or 1 on an OPL3) times
+// AUDIO_FM_BANK_REGS plus the register index.
+#define AUDIO_FM_BANK_REGS 0x100
+
+// What a synth driver's open found.
+typedef struct {
+ char description[AUDIO_DESC_MAX]; // for people, e.g. "OPL3 FM synthesizer at 388h"
+ uint32_t voices; // FM: two-operator voices (9 on an OPL2, 18 on an OPL3)
+ bool stereo; // FM: each voice can play left, right or both (OPL3)
+} AudioSynthDrvInfoT;
+
+// The descriptor every synth driver exports as AUDIO_SYNTH_SYMBOL.
+// hdr.name is a short name below AUDIO_NAME_MAX; a higher hdr.priority is
+// tried first.
+typedef struct {
+ DvxDriverHeaderT hdr;
+ int32_t kind; // AUDIO_SYNTH_*
+ bool (*open)(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
+ void (*close)(void);
+ void (*write)(uint32_t reg, uint8_t value); // FM: set a register
+ bool (*put)(uint8_t byte); // MIDI: send a byte; false when the port is not ready yet
+} AudioSynthDriverT;
+
+// Write a synth driver's descriptor: AUDIO_SYNTH_DRIVER("x", priority,
+// .kind = ..., .open = ..., ...);
+#define AUDIO_SYNTH_DRIVER(drvName, drvPriority, ...) extern const AudioSynthDriverT audioSynth; const AudioSynthDriverT audioSynth = { .hdr = DVX_DRIVER_HDR(AudioSynthDriverT, drvName, drvPriority), __VA_ARGS__ }
+
+// Mix the next frames frames of output into dst in the device format
+// (AudioDrvInfoT bits and channels: 16-bit signed or 8-bit unsigned,
+// stereo interleaved). Interrupt-safe; never blocks.
+void audioMixRender(void *dst, uint32_t frames);
+
+// Drive the mixer from the timer service (AUDIO_CLOCK_HZ) for a device
+// that has no interrupt of its own to do it; the output is thrown away.
+// Tones still reach a hardware tone generator (AudioDrvInfoT tone).
+bool audioClockStart(void);
+void audioClockStop(void);
+
+// Pin (lock) or release the part of the calling module holding the
+// listed functions and data: from the lowest address to the end of the
+// highest object (a module's code comes before its data, so one span
+// covers every function listed). Returns false when pinning failed.
+bool audioLockSpan(const AudioLockItemT *items, int32_t count, bool lock);
+
+// Interrupts off / back as they were, around the few instructions that
+// must not be split by an interrupt (an FM chip's index/data pair, a
+// table an interrupt also changes). Inline, so code that runs at
+// interrupt time calls nothing outside its pinned memory. Nothing to do
+// on the host, which has no interrupts.
+static inline uint32_t audioIrqOff(void) {
+ uint32_t flags = 0;
+
+#ifdef __DJGPP__
+ __asm__ __volatile__ ("pushfl\n\tpopl %0\n\tcli" : "=r"(flags) : : "memory");
+#endif
+ return flags;
+}
+
+
+static inline void audioIrqRestore(uint32_t flags) {
+#ifdef __DJGPP__
+ __asm__ __volatile__ ("pushl %0\n\tpopfl" : : "r"(flags) : "memory", "cc");
+#else
+ (void)flags;
+#endif
+}
+
+#endif // AUDIO_DRV_H
diff --git a/src/libs/kpunch/audio/audioInt.h b/src/libs/kpunch/audio/audioInt.h
new file mode 100644
index 00000000..7f404dbc
--- /dev/null
+++ b/src/libs/kpunch/audio/audioInt.h
@@ -0,0 +1,203 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioInt.h -- private to audio.lib (not in the SDK)
+//
+// The mixer's state, shared by the interrupt-time code (audioMix.c) and
+// the rest of the library. The interrupt side only reads what the
+// program side set up, except where noted: a voice's state goes from
+// MIX_FREE to MIX_PLAYING by the program side once every other field is
+// written, and to MIX_ENDED by either side; a stream's ring and a note
+// queue are written at head by the program side and read at tail by the
+// mixer. On a single
+// CPU no lock is needed for that: the interrupt never sees a half-set-up
+// voice, and single stores (a gain, a step) are atomic.
+#ifndef AUDIO_INT_H
+#define AUDIO_INT_H
+
+#include "audioDrv.h"
+
+// Frames mixed per pass (the accumulator's length)
+// Silence after which the device's output is closed until the next voice
+// (audio.c devIdle)
+#define AUDIO_IDLE_CLOSE_SEC 3
+
+#define MIX_CHUNK_FRAMES 256
+#define MIX_ACC_CHANNELS 2
+
+// Fixed point: gains are 8.8 (AUDIO_VOLUME_MAX = 1.0), positions and
+// steps 16.16
+#define MIX_GAIN_SHIFT 8
+#define MIX_FRAC_BITS 16
+#define MIX_FRAC_ONE (1U << MIX_FRAC_BITS)
+#define MIX_LERP_SHIFT 15 // interpolation weight bits (keeps the product in 32 bits)
+
+// 16-bit output range and the 8-bit unsigned conversion
+#define MIX_SAMPLE_MAX 32767
+#define MIX_SAMPLE_MIN (-32768)
+#define MIX_U8_SHIFT 8
+#define MIX_U8_BIAS 128
+
+// Tone amplitude at full volume (half of full scale leaves headroom)
+#define MIX_TONE_AMPLITUDE 16384
+
+// Noise generator: 15-bit shift register, never zero
+#define MIX_NOISE_BITS 15
+#define MIX_NOISE_SEED 1
+
+// Voice states
+#define MIX_FREE 0
+#define MIX_PLAYING 1
+#define MIX_ENDED 2
+
+// Voice kinds
+#define MIX_KIND_SAMPLE 0
+#define MIX_KIND_STREAM 1
+#define MIX_KIND_TONE 2
+#define MIX_KIND_NOTES 3
+
+// A stream's buffer: frames of int16 samples (channels per frame), a
+// power of two long. head counts frames written, tail frames read.
+typedef struct {
+ int16_t *data;
+ uint32_t mask; // capacity in frames - 1
+ uint32_t channels;
+ volatile uint32_t head; // written by the program side
+ volatile uint32_t tail; // advanced by the mixer
+ volatile bool fed; // samples have arrived at least once
+} AudioRingT;
+
+// A note queue (audioNotesOpen): tones and rests played one after the
+// other. An entry is a tone of phaseStep (hz for a tone generator) for
+// frames output frames; hz 0 is a rest. head counts entries written,
+// tail entries the mixer has started.
+typedef struct {
+ uint32_t phaseStep;
+ uint32_t hz;
+ uint32_t frames;
+} MixNoteT;
+
+typedef struct {
+ MixNoteT *data;
+ uint32_t mask; // capacity - 1 (a power of two)
+ volatile uint32_t head; // written by the program side
+ volatile uint32_t tail; // advanced by the mixer
+} MixNoteRingT;
+
+// One voice as the mixer sees it.
+typedef struct {
+ const int16_t *data; // samples: frames * channels values
+ AudioRingT *ring; // streams
+ MixNoteRingT *notes; // note queues
+ uint32_t frames; // samples: length in frames
+ uint32_t pos; // samples: next frame to fetch
+ uint32_t frac; // position between cur and next (MIX_FRAC_BITS)
+ volatile uint32_t step; // source frames per output frame (16.16)
+ int32_t curL; // source frame being played ...
+ int32_t curR;
+ int32_t nextL; // ... and the one after it
+ int32_t nextR;
+ volatile int32_t gainL; // 0..AUDIO_VOLUME_MAX: volume x pan x context x master
+ volatile int32_t gainR;
+ uint32_t phase; // tones: waveform position (32-bit turn)
+ volatile uint32_t phaseStep; // tones: phase added per output frame
+ uint32_t lfsr; // noise shift register
+ volatile uint32_t remain; // timed tones, note queues: output frames left
+ volatile uint32_t toneHz; // tones: frequency (for a hardware tone generator; 0 silent)
+ uint32_t seq; // start order (newest wins a hardware tone)
+ volatile uint8_t state; // MIX_FREE / MIX_PLAYING / MIX_ENDED
+ volatile uint8_t endReason; // AudioEndE, valid once MIX_ENDED
+ uint8_t kind; // MIX_KIND_*
+ uint8_t channels; // source channels
+ uint8_t wave; // AudioWaveE
+ volatile uint8_t fmSlot; // tones on the FM chip: its channel + 1 (0: the mixer makes the tone)
+ bool loop; // samples: start over at the end
+ bool draining; // the source ran out: cur is its last frame
+ bool timed; // tones: stop when remain reaches 0
+} MixVoiceT;
+
+// The mixer.
+typedef struct {
+ MixVoiceT voices[AUDIO_VOICES_MAX];
+ int32_t acc[MIX_CHUNK_FRAMES * MIX_ACC_CHANNELS];
+ AudioToneFnT hwTone; // hardware tone generator, when the device has no DAC
+ uint32_t rate; // output rate
+ uint32_t bits; // output bits
+ uint32_t channels; // output channels
+ uint32_t hwToneHz; // frequency the tone generator is playing (0: off)
+ volatile uint32_t renders; // AudioStatsT counters kept by the mixer
+ volatile uint32_t frames;
+ volatile uint32_t underruns;
+ volatile uint32_t clipped;
+} AudioMixT;
+
+extern AudioMixT audioMixer;
+
+// Mixer (audioMix.c). Prime loads a voice's first two source frames
+// before it is marked playing; Lock pins the mixer's code and data;
+// Reset empties it for a device with this format.
+bool audioMixLock(bool lock);
+void audioMixPrime(MixVoiceT *v);
+void audioMixReset(uint32_t rate, uint32_t bits, uint32_t channels, AudioToneFnT hwTone);
+
+// Synthesizers (audioSynth.c). Attach / Detach connect the FM chip or
+// the MIDI port of the synth driver the device opened (NULL: none);
+// CtxClosed gives back what a closing context held; Lock pins the part
+// that runs at interrupt time. ChannelOff keys an FM channel off (the
+// mixer ends a timed FM tone with it, at interrupt time). The tone
+// functions put a square or triangle tone on an FM voice of ctx and
+// update its frequency and loudness (gains 0..AUDIO_VOLUME_MAX per side).
+void audioFmAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info);
+int32_t audioFmChannelOf(const AudioCtxT *ctx, int32_t voice);
+void audioFmChannelOff(uint32_t channel);
+void audioFmDetach(void);
+int32_t audioFmToneStart(AudioCtxT *ctx, AudioWaveE wave);
+void audioFmToneUpdate(AudioCtxT *ctx, int32_t voice, uint32_t hz, int32_t gainL, int32_t gainR);
+void audioMidiAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info);
+void audioMidiDetach(void);
+void audioSynthCtxClosed(AudioCtxT *ctx);
+bool audioSynthLock(bool lock);
+
+// Sound files (audioLoad.c): decode a WAV or VOC image into int16 PCM
+// (malloc'd, frames * fmt->channels values; fmt->bits is what the file
+// had). Convert turns count 8-bit unsigned or 16-bit signed samples into
+// int16.
+void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits);
+int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
+
+// The library's end of a program's life, of the shell's idle time and of
+// its own unloading (audio.c; audioShell.c hooks them up to the shell),
+// the calling BASIC program's context (audioBas.c), and making appId the
+// current program (returns the one before).
+void audioAppEnded(int32_t appId);
+int32_t audioAppEnter(int32_t appId);
+AudioCtxT *audioBasicContext(void);
+void audioIdle(void *unused);
+void audioShutdown(void);
+
+// Desktop sounds (audioDesk.c): Init connects them to DVX (the sound
+// hook), Shutdown disconnects them before the library closes its
+// contexts.
+void audioDeskInit(void);
+void audioDeskShutdown(void);
+
+#endif // AUDIO_INT_H
diff --git a/src/libs/kpunch/audio/audioLoad.c b/src/libs/kpunch/audio/audioLoad.c
new file mode 100644
index 00000000..9b00ed68
--- /dev/null
+++ b/src/libs/kpunch/audio/audioLoad.c
@@ -0,0 +1,451 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioLoad.c -- WAV and VOC decoding
+//
+// Both decode into 16-bit signed PCM (what the mixer plays), keeping the
+// file's rate and channel count; audioConvert does the sample conversion
+// for streams too.
+//
+// WAV: a RIFF "WAVE" file. Chunks are walked in order (odd sizes padded)
+// and all but 'fmt ' and 'data' are skipped. Accepted: PCM (format tag
+// 1, or WAVE_FORMAT_EXTENSIBLE with the PCM sub-format), 8-bit unsigned
+// or 16-bit signed, mono or stereo, AUDIO_RATE_MIN..AUDIO_RATE_MAX. A
+// data chunk that claims more bytes than the file has plays what is
+// there (recorders that stopped early leave such files).
+//
+// VOC: Creative Voice File, header checksum verified. Blocks: 1 sound
+// (8-bit unsigned; Creative ADPCM is refused), 2 continuation, 3 silence,
+// 8 extended (rate and stereo for the next sound block), 9 new sound
+// (8-bit unsigned or 16-bit signed PCM); markers, text and repeat blocks
+// are skipped (a repeated section plays once). All sound must share one
+// format.
+
+#include "audioInt.h"
+
+#include
+#include
+
+// RIFF / WAVE layout
+#define RIFF_HEADER_BYTES 12
+#define RIFF_FORM_OFFSET 8
+#define CHUNK_HEADER_BYTES 8
+#define CHUNK_ID_BYTES 4
+#define CHUNK_SIZE_OFFSET 4
+#define WAV_FMT_MIN_BYTES 16
+#define WAV_FMT_CHANNELS 2
+#define WAV_FMT_RATE 4
+#define WAV_FMT_BLOCK_ALIGN 12
+#define WAV_FMT_BITS 14
+#define WAV_EXT_FMT_BYTES 40
+#define WAV_EXT_SUBFORMAT 24
+#define WAV_TAG_PCM 1
+#define WAV_TAG_EXTENSIBLE 0xFFFE
+
+// VOC layout
+#define VOC_SIGNATURE "Creative Voice File\x1A"
+#define VOC_SIGNATURE_BYTES 20
+#define VOC_HEADER_SIZE_OFFSET 20
+#define VOC_VERSION_OFFSET 22
+#define VOC_CHECK_OFFSET 24
+#define VOC_HEADER_MIN_BYTES 26
+#define VOC_CHECK_MAGIC 0x1234
+#define VOC_BLOCK_HEADER_BYTES 4
+#define VOC_BLOCK_END 0
+#define VOC_BLOCK_SOUND 1
+#define VOC_BLOCK_CONTINUE 2
+#define VOC_BLOCK_SILENCE 3
+#define VOC_BLOCK_EXTENDED 8
+#define VOC_BLOCK_NEW_SOUND 9
+#define VOC_SOUND_HEADER_BYTES 2
+#define VOC_SILENCE_BYTES 3
+#define VOC_EXTENDED_BYTES 4
+#define VOC_NEW_HEADER_BYTES 12
+#define VOC_CODEC_PCM8 0
+#define VOC_CODEC_PCM16 4
+#define VOC_TC_BASE 256
+#define VOC_TC_CLOCK 1000000
+#define VOC_TC16_BASE 65536
+#define VOC_TC16_CLOCK 256000000
+#define VOC_MODE_STEREO 1
+
+// Sample formats
+#define BITS_8 8
+#define BITS_16 16
+#define BITS_PER_BYTE 8
+#define U8_BIAS 128
+#define U8_SCALE 256 // 8-bit sample to 16-bit
+#define CHANNELS_MAX 2
+#define BYTE_MASK 0xFF
+#define SHIFT_8 8
+#define SHIFT_16 16
+#define SHIFT_24 24
+
+// VOC output while it is assembled
+typedef struct {
+ int16_t *pcm;
+ uint32_t count; // samples stored
+ uint32_t cap; // samples allocated
+ AudioFormatT fmt;
+ bool haveFmt; // a sound or silence block set the format
+} VocOutT;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits);
+int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
+static int32_t checkFormat(const AudioFormatT *fmt);
+static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
+static int32_t decodeWav(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
+static uint32_t rd16(const uint8_t *p);
+static uint32_t rd24(const uint8_t *p);
+static uint32_t rd32(const uint8_t *p);
+static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint32_t silence);
+static int32_t vocSetFormat(VocOutT *out, uint32_t rate, uint32_t bits, uint32_t channels);
+
+
+void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits) {
+ const uint8_t *s = (const uint8_t *)src;
+
+ if (bits == BITS_8) {
+ for (uint32_t i = 0; i < count; i++) {
+ dst[i] = (int16_t)(((int32_t)s[i] - U8_BIAS) * U8_SCALE);
+ }
+
+ return;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ dst[i] = (int16_t)rd16(&s[i * 2]);
+ }
+}
+
+
+int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt) {
+ *pcm = NULL;
+ *frames = 0;
+
+ if (size >= RIFF_HEADER_BYTES && memcmp(data, "RIFF", CHUNK_ID_BYTES) == 0 && memcmp(data + RIFF_FORM_OFFSET, "WAVE", CHUNK_ID_BYTES) == 0) {
+ return decodeWav(data, size, pcm, frames, fmt);
+ }
+
+ if (size >= VOC_SIGNATURE_BYTES && memcmp(data, VOC_SIGNATURE, VOC_SIGNATURE_BYTES) == 0) {
+ return decodeVoc(data, size, pcm, frames, fmt);
+ }
+
+ return AUDIO_ERR_FORMAT;
+}
+
+
+static int32_t checkFormat(const AudioFormatT *fmt) {
+ if (fmt->channels < 1 || fmt->channels > CHANNELS_MAX || (fmt->bits != BITS_8 && fmt->bits != BITS_16) || fmt->rate < AUDIO_RATE_MIN || fmt->rate > AUDIO_RATE_MAX) {
+ return AUDIO_ERR_UNSUPPORTED;
+ }
+
+ return AUDIO_OK;
+}
+
+
+static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt) {
+ if (size < VOC_HEADER_MIN_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ uint32_t headerBytes = rd16(data + VOC_HEADER_SIZE_OFFSET);
+ uint32_t version = rd16(data + VOC_VERSION_OFFSET);
+
+ if (rd16(data + VOC_CHECK_OFFSET) != ((~version + VOC_CHECK_MAGIC) & 0xFFFF) || headerBytes < VOC_HEADER_MIN_BYTES || headerBytes > size) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ VocOutT out;
+ uint32_t pos = headerBytes;
+ uint32_t extRate = 0;
+ uint32_t extChans = 0;
+ bool extPending = false;
+ int32_t rc = AUDIO_OK;
+
+ memset(&out, 0, sizeof(out));
+
+ // A missing terminator block ends the file where the data ends.
+ while (rc == AUDIO_OK && pos < size && data[pos] != VOC_BLOCK_END) {
+ if (size - pos < VOC_BLOCK_HEADER_BYTES) {
+ rc = AUDIO_ERR_CORRUPT;
+ break;
+ }
+
+ uint32_t type = data[pos];
+ uint32_t len = rd24(data + pos + 1);
+ const uint8_t *body = data + pos + VOC_BLOCK_HEADER_BYTES;
+
+ if (len > size - pos - VOC_BLOCK_HEADER_BYTES) {
+ rc = AUDIO_ERR_CORRUPT;
+ break;
+ }
+
+ switch (type) {
+ case VOC_BLOCK_SOUND: {
+ if (len < VOC_SOUND_HEADER_BYTES) {
+ rc = AUDIO_ERR_CORRUPT;
+ break;
+ }
+
+ uint32_t rate = VOC_TC_CLOCK / (VOC_TC_BASE - body[0]);
+ uint32_t chans = 1;
+
+ if (extPending) {
+ rate = extRate;
+ chans = extChans;
+ extPending = false;
+ }
+
+ if (body[1] != VOC_CODEC_PCM8) {
+ rc = AUDIO_ERR_UNSUPPORTED;
+ break;
+ }
+
+ rc = vocSetFormat(&out, rate, BITS_8, chans);
+
+ if (rc == AUDIO_OK) {
+ rc = vocAppend(&out, body + VOC_SOUND_HEADER_BYTES, len - VOC_SOUND_HEADER_BYTES, 0);
+ }
+
+ break;
+ }
+
+ case VOC_BLOCK_CONTINUE:
+ rc = out.haveFmt ? vocAppend(&out, body, len, 0) : AUDIO_ERR_CORRUPT;
+ break;
+
+ case VOC_BLOCK_SILENCE:
+ if (len < VOC_SILENCE_BYTES) {
+ rc = AUDIO_ERR_CORRUPT;
+ break;
+ }
+
+ if (!out.haveFmt) {
+ rc = vocSetFormat(&out, VOC_TC_CLOCK / (VOC_TC_BASE - body[2]), BITS_8, 1);
+ }
+
+ if (rc == AUDIO_OK) {
+ rc = vocAppend(&out, NULL, 0, rd16(body) + 1);
+ }
+
+ break;
+
+ case VOC_BLOCK_EXTENDED:
+ if (len < VOC_EXTENDED_BYTES || body[3] > VOC_MODE_STEREO) {
+ rc = len < VOC_EXTENDED_BYTES ? AUDIO_ERR_CORRUPT : AUDIO_ERR_UNSUPPORTED;
+ break;
+ }
+
+ extChans = (uint32_t)body[3] + 1;
+ extRate = VOC_TC16_CLOCK / (VOC_TC16_BASE - rd16(body)) / extChans;
+ extPending = true;
+
+ if (body[2] != VOC_CODEC_PCM8) {
+ rc = AUDIO_ERR_UNSUPPORTED;
+ }
+
+ break;
+
+ case VOC_BLOCK_NEW_SOUND: {
+ if (len < VOC_NEW_HEADER_BYTES) {
+ rc = AUDIO_ERR_CORRUPT;
+ break;
+ }
+
+ uint32_t codec = rd16(body + 6);
+ uint32_t bits = body[4];
+
+ if (!((codec == VOC_CODEC_PCM8 && bits == BITS_8) || (codec == VOC_CODEC_PCM16 && bits == BITS_16))) {
+ rc = AUDIO_ERR_UNSUPPORTED;
+ break;
+ }
+
+ rc = vocSetFormat(&out, rd32(body), bits, body[5]);
+
+ if (rc == AUDIO_OK) {
+ rc = vocAppend(&out, body + VOC_NEW_HEADER_BYTES, len - VOC_NEW_HEADER_BYTES, 0);
+ }
+
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ pos += VOC_BLOCK_HEADER_BYTES + len;
+ }
+
+ if (rc == AUDIO_OK && (!out.haveFmt || out.count < out.fmt.channels)) {
+ rc = AUDIO_ERR_CORRUPT;
+ }
+
+ if (rc != AUDIO_OK) {
+ free(out.pcm);
+ return rc;
+ }
+
+ *pcm = out.pcm;
+ *frames = out.count / out.fmt.channels;
+ *fmt = out.fmt;
+ return AUDIO_OK;
+}
+
+
+static int32_t decodeWav(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt) {
+ const uint8_t *fmtChunk = NULL;
+ const uint8_t *dataChunk = NULL;
+ uint32_t fmtBytes = 0;
+ uint32_t dataBytes = 0;
+ uint32_t pos = RIFF_HEADER_BYTES;
+
+ while (size - pos >= CHUNK_HEADER_BYTES && !(fmtChunk && dataChunk)) {
+ const uint8_t *id = data + pos;
+ uint32_t len = rd32(data + pos + CHUNK_SIZE_OFFSET);
+ uint32_t avail = size - pos - CHUNK_HEADER_BYTES;
+
+ if (memcmp(id, "data", CHUNK_ID_BYTES) == 0) {
+ dataChunk = id + CHUNK_HEADER_BYTES;
+ dataBytes = len < avail ? len : avail;
+ } else if (len > avail) {
+ return AUDIO_ERR_CORRUPT;
+ } else if (memcmp(id, "fmt ", CHUNK_ID_BYTES) == 0) {
+ fmtChunk = id + CHUNK_HEADER_BYTES;
+ fmtBytes = len;
+ }
+
+ if (len >= avail) {
+ break;
+ }
+
+ pos += CHUNK_HEADER_BYTES + len + (len & 1);
+ }
+
+ if (!fmtChunk || !dataChunk || fmtBytes < WAV_FMT_MIN_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ uint32_t tag = rd16(fmtChunk);
+
+ if (tag == WAV_TAG_EXTENSIBLE && fmtBytes >= WAV_EXT_FMT_BYTES) {
+ tag = rd16(fmtChunk + WAV_EXT_SUBFORMAT);
+ }
+
+ fmt->channels = rd16(fmtChunk + WAV_FMT_CHANNELS);
+ fmt->rate = rd32(fmtChunk + WAV_FMT_RATE);
+ fmt->bits = rd16(fmtChunk + WAV_FMT_BITS);
+
+ if (tag != WAV_TAG_PCM || checkFormat(fmt) != AUDIO_OK) {
+ return AUDIO_ERR_UNSUPPORTED;
+ }
+
+ uint32_t frameBytes = fmt->channels * fmt->bits / BITS_PER_BYTE;
+
+ if (rd16(fmtChunk + WAV_FMT_BLOCK_ALIGN) != frameBytes || dataBytes < frameBytes) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ uint32_t count = dataBytes / frameBytes * fmt->channels;
+
+ *pcm = (int16_t *)malloc(count * sizeof(int16_t));
+
+ if (!*pcm) {
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ audioConvert(dataChunk, *pcm, count, fmt->bits);
+ *frames = count / fmt->channels;
+ return AUDIO_OK;
+}
+
+
+static uint32_t rd16(const uint8_t *p) {
+ return (uint32_t)p[0] | ((uint32_t)p[1] << SHIFT_8);
+}
+
+
+static uint32_t rd24(const uint8_t *p) {
+ return rd16(p) | ((uint32_t)p[2] << SHIFT_16);
+}
+
+
+static uint32_t rd32(const uint8_t *p) {
+ return rd24(p) | ((uint32_t)p[3] << SHIFT_24);
+}
+
+
+// Append bytes of sound in the current format, or silence frames of
+// silence (src NULL). A stereo frame may continue in the next block, so
+// only the total is cut to whole frames (decodeVoc).
+static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint32_t silence) {
+ uint32_t add = src ? bytes / (out->fmt.bits / BITS_PER_BYTE) : silence * out->fmt.channels;
+
+ if (out->count + add > out->cap) {
+ uint32_t cap = out->cap ? out->cap : add;
+
+ while (cap < out->count + add) {
+ cap *= 2;
+ }
+
+ int16_t *grown = (int16_t *)realloc(out->pcm, cap * sizeof(int16_t));
+
+ if (!grown) {
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ out->pcm = grown;
+ out->cap = cap;
+ }
+
+ if (src) {
+ audioConvert(src, out->pcm + out->count, add, out->fmt.bits);
+ } else {
+ memset(out->pcm + out->count, 0, add * sizeof(int16_t));
+ }
+
+ out->count += add;
+ return AUDIO_OK;
+}
+
+
+// The first sound or silence block sets the format; a later one that
+// differs is refused.
+static int32_t vocSetFormat(VocOutT *out, uint32_t rate, uint32_t bits, uint32_t channels) {
+ AudioFormatT fmt = { rate, bits, channels };
+
+ if (checkFormat(&fmt) != AUDIO_OK) {
+ return AUDIO_ERR_UNSUPPORTED;
+ }
+
+ if (!out->haveFmt) {
+ out->fmt = fmt;
+ out->haveFmt = true;
+ return AUDIO_OK;
+ }
+
+ return (fmt.rate == out->fmt.rate && fmt.bits == out->fmt.bits && fmt.channels == out->fmt.channels) ? AUDIO_OK : AUDIO_ERR_UNSUPPORTED;
+}
diff --git a/src/libs/kpunch/audio/audioMix.c b/src/libs/kpunch/audio/audioMix.c
new file mode 100644
index 00000000..b7b4794d
--- /dev/null
+++ b/src/libs/kpunch/audio/audioMix.c
@@ -0,0 +1,526 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioMix.c -- the mixer: everything audio.lib runs at interrupt time
+//
+// audioMixRender is called by the sound driver for each block of output,
+// from the card's interrupt or from the timer service, so this file keeps
+// to the interrupt rules (audioDrv.h): integer arithmetic only, no calls
+// out of the file but the driver's tone function and audioFmChannelOff
+// (audioSynth.c, pinned too), no library calls (the
+// Makefile stops gcc from turning loops into memset), and everything it
+// touches pinned (audioMixLock; sample and stream buffers are pinned where
+// they are allocated).
+//
+// Every source is played through the same two-frame window: cur and next
+// hold consecutive source frames, frac the position between them. Each
+// output frame is cur + (next - cur) * frac (linear interpolation); frac
+// then advances by step = source rate / output rate (times the pitch),
+// shifting next into cur and fetching a new next for every whole frame
+// crossed. At a step of exactly 1.0 the output is the source, sample for
+// sample. Sums are 32-bit: a 16-bit sample times a gain of at most 256,
+// for AUDIO_VOICES_MAX voices, cannot overflow; the sum is then scaled
+// back and clipped to 16 bits.
+
+#include "audioInt.h"
+
+#include
+
+// Timer-driven output (audioClockStart): the most frames one call mixes
+// and the largest frame (16-bit stereo)
+#define CLOCK_MAX_FRAMES (AUDIO_RATE_MAX / AUDIO_CLOCK_HZ + 1)
+#define CLOCK_FRAME_BYTES 4
+#define CLOCK_TIMER_NONE (-1)
+
+// Triangle wave: the phase's top 16 bits, rising over the first half
+#define TRI_PHASE_SHIFT 16
+#define TRI_HALF 32768
+#define TRI_FALL_OFFSET 98303 // 32767 + 2 * 32768: the falling half's start
+#define TRI_SCALE_SHIFT 15
+#define SQUARE_HIGH_BIT 0x80000000U
+
+// Sample sizes of the output formats (16-bit samples are stored low
+// byte first)
+#define BITS_PER_BYTE 8
+#define BYTE_MASK 0xFF
+#define OUT_BITS_16 16
+
+typedef struct {
+ int32_t timerId;
+ uint32_t acc; // rate * calls not yet mixed
+ uint8_t buf[CLOCK_MAX_FRAMES * CLOCK_FRAME_BYTES]; // output nobody plays
+} MixClockT;
+
+AudioMixT audioMixer;
+
+static MixClockT sClock = { .timerId = CLOCK_TIMER_NONE };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool audioClockStart(void);
+void audioClockStop(void);
+bool audioLockSpan(const AudioLockItemT *items, int32_t count, bool lock);
+bool audioMixLock(bool lock);
+void audioMixPrime(MixVoiceT *v);
+void audioMixRender(void *dst, uint32_t frames);
+void audioMixReset(uint32_t rate, uint32_t bits, uint32_t channels, AudioToneFnT hwTone);
+static int32_t clipSample(int32_t s);
+static void clockTick(void *unused);
+static void endVoice(MixVoiceT *v, AudioEndE reason);
+static bool loadNext(MixVoiceT *v);
+static void mixNotes(MixVoiceT *v, int32_t *acc, uint32_t n);
+static void mixSource(MixVoiceT *v, int32_t *acc, uint32_t n);
+static void mixTone(MixVoiceT *v, int32_t *acc, uint32_t n);
+static int32_t toneSample(MixVoiceT *v);
+static void toneSelect(void);
+static void writeOut(uint8_t *dst, uint32_t n);
+
+
+bool audioClockStart(void) {
+ sClock.acc = 0;
+ sClock.timerId = platformTimerAdd(AUDIO_CLOCK_HZ, clockTick, NULL, 0);
+ return sClock.timerId >= 0;
+}
+
+
+void audioClockStop(void) {
+ platformTimerRemove(sClock.timerId);
+ sClock.timerId = CLOCK_TIMER_NONE;
+}
+
+
+bool audioLockSpan(const AudioLockItemT *items, int32_t count, bool lock) {
+ uintptr_t lo = UINTPTR_MAX;
+ uintptr_t hi = 0;
+
+ for (int32_t i = 0; i < count; i++) {
+ uintptr_t start = (uintptr_t)items[i].addr;
+ uintptr_t end = start + (items[i].size ? items[i].size : AUDIO_LOCK_CODE_BYTES);
+
+ if (start < lo) {
+ lo = start;
+ }
+
+ if (end > hi) {
+ hi = end;
+ }
+ }
+
+ if (hi <= lo) {
+ return false;
+ }
+
+ if (lock) {
+ return platformLockRegion((const void *)lo, (uint32_t)(hi - lo), false) == 0;
+ }
+
+ return platformUnlockRegion((const void *)lo, (uint32_t)(hi - lo), false) == 0;
+}
+
+
+bool audioMixLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)audioMixRender, 0 },
+ { (const void *)clipSample, 0 },
+ { (const void *)clockTick, 0 },
+ { (const void *)endVoice, 0 },
+ { (const void *)loadNext, 0 },
+ { (const void *)mixNotes, 0 },
+ { (const void *)mixSource, 0 },
+ { (const void *)mixTone, 0 },
+ { (const void *)toneSample, 0 },
+ { (const void *)toneSelect, 0 },
+ { (const void *)writeOut, 0 },
+ { &audioMixer, sizeof(audioMixer) },
+ { &sClock, sizeof(sClock) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+// The first source frame becomes cur and the second next; a source of one
+// frame drains at once. Called before the voice is marked playing.
+void audioMixPrime(MixVoiceT *v) {
+ v->frac = 0;
+ v->draining = false;
+ v->nextL = 0;
+ v->nextR = 0;
+
+ if (!loadNext(v)) {
+ v->draining = true;
+ }
+
+ v->curL = v->nextL;
+ v->curR = v->nextR;
+
+ if (!v->draining && !loadNext(v)) {
+ v->draining = true;
+ }
+}
+
+
+void audioMixRender(void *dst, uint32_t frames) {
+ uint8_t *out = (uint8_t *)dst;
+ uint32_t frameBytes = audioMixer.bits / BITS_PER_BYTE * audioMixer.channels;
+
+ audioMixer.renders++;
+ audioMixer.frames += frames;
+
+ while (frames > 0) {
+ uint32_t n = frames < MIX_CHUNK_FRAMES ? frames : MIX_CHUNK_FRAMES;
+
+ for (uint32_t i = 0; i < n * MIX_ACC_CHANNELS; i++) {
+ audioMixer.acc[i] = 0;
+ }
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ MixVoiceT *v = &audioMixer.voices[i];
+
+ if (v->state != MIX_PLAYING) {
+ continue;
+ }
+
+ if (v->kind == MIX_KIND_TONE) {
+ mixTone(v, audioMixer.acc, n);
+ } else if (v->kind == MIX_KIND_NOTES) {
+ mixNotes(v, audioMixer.acc, n);
+ } else {
+ mixSource(v, audioMixer.acc, n);
+ }
+ }
+
+ writeOut(out, n);
+ out += n * frameBytes;
+ frames -= n;
+ }
+
+ if (audioMixer.hwTone) {
+ toneSelect();
+ }
+}
+
+
+// Called with no device running, so nothing reads the mixer meanwhile.
+void audioMixReset(uint32_t rate, uint32_t bits, uint32_t channels, AudioToneFnT hwTone) {
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ audioMixer.voices[i].state = MIX_FREE;
+ }
+
+ audioMixer.rate = rate;
+ audioMixer.bits = bits;
+ audioMixer.channels = channels;
+ audioMixer.hwTone = hwTone;
+ audioMixer.hwToneHz = 0;
+ audioMixer.renders = 0;
+ audioMixer.frames = 0;
+ audioMixer.underruns = 0;
+ audioMixer.clipped = 0;
+}
+
+
+static int32_t clipSample(int32_t s) {
+ if (s > MIX_SAMPLE_MAX) {
+ audioMixer.clipped++;
+ return MIX_SAMPLE_MAX;
+ }
+
+ if (s < MIX_SAMPLE_MIN) {
+ audioMixer.clipped++;
+ return MIX_SAMPLE_MIN;
+ }
+
+ return s;
+}
+
+
+// The timer-service call of a device without an interrupt: mix what is
+// due since the last call (exact on average) into the scratch buffer.
+static void clockTick(void *unused) {
+ (void)unused;
+
+ sClock.acc += audioMixer.rate;
+
+ uint32_t frames = sClock.acc / AUDIO_CLOCK_HZ;
+
+ sClock.acc -= frames * AUDIO_CLOCK_HZ;
+ audioMixRender(sClock.buf, frames);
+}
+
+
+static void endVoice(MixVoiceT *v, AudioEndE reason) {
+ v->endReason = (uint8_t)reason;
+ v->state = MIX_ENDED;
+}
+
+
+// Fetch the source frame after next into next. False when a sample that
+// does not loop has no more frames; a stream with nothing buffered gives
+// silence (counted as an underrun once it has had samples).
+static bool loadNext(MixVoiceT *v) {
+ if (v->kind == MIX_KIND_STREAM) {
+ AudioRingT *r = v->ring;
+
+ if (r->head == r->tail) {
+ v->nextL = 0;
+ v->nextR = 0;
+
+ if (r->fed) {
+ audioMixer.underruns++;
+ }
+
+ return true;
+ }
+
+ const int16_t *f = &r->data[(r->tail & r->mask) * r->channels];
+
+ v->nextL = f[0];
+ v->nextR = f[r->channels - 1];
+ r->tail++;
+ return true;
+ }
+
+ if (v->pos >= v->frames) {
+ if (!v->loop) {
+ return false;
+ }
+
+ v->pos = 0;
+ }
+
+ const int16_t *f = &v->data[v->pos * v->channels];
+
+ v->nextL = f[0];
+ v->nextR = f[v->channels - 1];
+ v->pos++;
+ return true;
+}
+
+
+// A note queue plays its entries back to back, each for exactly its
+// frames; with none left it is silent (toneHz 0) until more arrive.
+static void mixNotes(MixVoiceT *v, int32_t *acc, uint32_t n) {
+ MixNoteRingT *q = v->notes;
+ int32_t gainL = v->gainL;
+ int32_t gainR = v->gainR;
+ uint32_t i = 0;
+
+ while (i < n) {
+ if (v->remain == 0) {
+ if (q->tail == q->head) {
+ break;
+ }
+
+ const MixNoteT *e = &q->data[q->tail & q->mask];
+
+ v->phaseStep = e->phaseStep;
+ v->toneHz = e->hz;
+ v->remain = e->frames;
+ q->tail++;
+ continue;
+ }
+
+ uint32_t k = v->remain < n - i ? v->remain : n - i;
+
+ for (uint32_t j = 0; j < k && v->toneHz; j++) {
+ int32_t smp = toneSample(v);
+
+ acc[(i + j) * MIX_ACC_CHANNELS] += smp * gainL;
+ acc[(i + j) * MIX_ACC_CHANNELS + 1] += smp * gainR;
+ }
+
+ v->remain -= k;
+ i += k;
+ }
+
+ if (v->remain == 0 && q->tail == q->head) {
+ v->toneHz = 0;
+ }
+}
+
+
+static void mixSource(MixVoiceT *v, int32_t *acc, uint32_t n) {
+ int32_t gainL = v->gainL;
+ int32_t gainR = v->gainR;
+ uint32_t step = v->step;
+
+ for (uint32_t i = 0; i < n; i++) {
+ int32_t w = (int32_t)(v->frac >> (MIX_FRAC_BITS - MIX_LERP_SHIFT));
+ int32_t l = v->curL + (((v->nextL - v->curL) * w) >> MIX_LERP_SHIFT);
+ int32_t r = v->curR + (((v->nextR - v->curR) * w) >> MIX_LERP_SHIFT);
+
+ acc[i * MIX_ACC_CHANNELS] += l * gainL;
+ acc[i * MIX_ACC_CHANNELS + 1] += r * gainR;
+ v->frac += step;
+
+ while (v->frac >= MIX_FRAC_ONE) {
+ v->frac -= MIX_FRAC_ONE;
+
+ if (v->draining) {
+ endVoice(v, AUDIO_END_FINISHED);
+ return;
+ }
+
+ v->curL = v->nextL;
+ v->curR = v->nextR;
+
+ if (!loadNext(v)) {
+ v->draining = true;
+ }
+ }
+ }
+}
+
+
+// A tone on the FM chip is only timed here (and keyed off at its end).
+static void mixTone(MixVoiceT *v, int32_t *acc, uint32_t n) {
+ int32_t gainL = v->gainL;
+ int32_t gainR = v->gainR;
+ uint8_t fmSlot = v->fmSlot;
+
+ if (v->timed && v->remain < n) {
+ n = v->remain;
+ }
+
+ for (uint32_t i = 0; i < n && !fmSlot; i++) {
+ int32_t s = toneSample(v);
+
+ acc[i * MIX_ACC_CHANNELS] += s * gainL;
+ acc[i * MIX_ACC_CHANNELS + 1] += s * gainR;
+ }
+
+ if (v->timed) {
+ v->remain -= n;
+
+ if (v->remain == 0) {
+ if (fmSlot) {
+ audioFmChannelOff(fmSlot - 1U);
+ }
+
+ endVoice(v, AUDIO_END_FINISHED);
+ }
+ }
+}
+
+
+// The tone's value for this output frame, then advance its phase. Noise
+// clocks its shift register once per phase turn, so its frequency is the
+// rate the noise changes at.
+static int32_t toneSample(MixVoiceT *v) {
+ uint32_t phase = v->phase;
+ int32_t s;
+
+ v->phase = phase + v->phaseStep;
+
+ switch (v->wave) {
+ case AUDIO_WAVE_TRIANGLE: {
+ int32_t t = (int32_t)(phase >> TRI_PHASE_SHIFT);
+ int32_t tri = t < TRI_HALF ? t * 2 - TRI_HALF : TRI_FALL_OFFSET - t * 2;
+
+ s = (tri * MIX_TONE_AMPLITUDE) >> TRI_SCALE_SHIFT;
+ break;
+ }
+
+ case AUDIO_WAVE_NOISE:
+ if (v->phase < phase) {
+ uint32_t bit = (v->lfsr ^ (v->lfsr >> 1)) & 1;
+
+ v->lfsr = (v->lfsr >> 1) | (bit << (MIX_NOISE_BITS - 1));
+ }
+
+ s = (v->lfsr & 1) ? MIX_TONE_AMPLITUDE : -MIX_TONE_AMPLITUDE;
+ break;
+
+ default:
+ s = (phase & SQUARE_HIGH_BIT) ? -MIX_TONE_AMPLITUDE : MIX_TONE_AMPLITUDE;
+ break;
+ }
+
+ return s;
+}
+
+
+// A device with a tone generator but no DAC plays one tone: the loudest
+// square or triangle tone playing, the newest on a tie.
+static void toneSelect(void) {
+ const MixVoiceT *best = NULL;
+ int32_t bestGain = 0;
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ const MixVoiceT *v = &audioMixer.voices[i];
+
+ if (v->state != MIX_PLAYING || (v->kind != MIX_KIND_TONE && v->kind != MIX_KIND_NOTES) || v->wave == AUDIO_WAVE_NOISE || v->fmSlot || !v->toneHz) {
+ continue;
+ }
+
+ int32_t gain = v->gainL > v->gainR ? v->gainL : v->gainR;
+
+ if (gain > 0 && (!best || gain > bestGain || (gain == bestGain && v->seq > best->seq))) {
+ best = v;
+ bestGain = gain;
+ }
+ }
+
+ uint32_t hz = best ? best->toneHz : 0;
+
+ if (hz != audioMixer.hwToneHz) {
+ audioMixer.hwToneHz = hz;
+ audioMixer.hwTone(hz);
+ }
+}
+
+
+// Scale the sums back to samples, clip, and store them in the device
+// format (mono is the average of both sides), a byte at a time so the
+// buffer needs no alignment.
+static void writeOut(uint8_t *dst, uint32_t n) {
+ const int32_t *acc = audioMixer.acc;
+ bool wide = audioMixer.bits == OUT_BITS_16;
+ bool stereo = audioMixer.channels == MIX_ACC_CHANNELS;
+
+ for (uint32_t i = 0; i < n; i++) {
+ int32_t l = clipSample(acc[i * MIX_ACC_CHANNELS] >> MIX_GAIN_SHIFT);
+ int32_t r = clipSample(acc[i * MIX_ACC_CHANNELS + 1] >> MIX_GAIN_SHIFT);
+
+ if (!stereo) {
+ l = (l + r) >> 1;
+ }
+
+ if (wide) {
+ *dst++ = (uint8_t)(l & BYTE_MASK);
+ *dst++ = (uint8_t)((l >> BITS_PER_BYTE) & BYTE_MASK);
+
+ if (stereo) {
+ *dst++ = (uint8_t)(r & BYTE_MASK);
+ *dst++ = (uint8_t)((r >> BITS_PER_BYTE) & BYTE_MASK);
+ }
+ } else {
+ *dst++ = (uint8_t)((l >> MIX_U8_SHIFT) + MIX_U8_BIAS);
+
+ if (stereo) {
+ *dst++ = (uint8_t)((r >> MIX_U8_SHIFT) + MIX_U8_BIAS);
+ }
+ }
+ }
+}
diff --git a/src/libs/kpunch/audio/audioShell.c b/src/libs/kpunch/audio/audioShell.c
new file mode 100644
index 00000000..e6a0feaf
--- /dev/null
+++ b/src/libs/kpunch/audio/audioShell.c
@@ -0,0 +1,51 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioShell.c -- audio.lib's hooks into the shell
+//
+// Loaded as a module of the shell: the constructor asks the shell for
+// its idle time (end notifications and pull streams) and to be told when
+// a program ends, however it ends (its contexts are closed then), and
+// connects the desktop sounds; the destructor closes everything
+// (stopping the sound device) and takes the hooks back. Registered while no program is current, so they are the
+// library's own and outlive every program. Not in the host tests, which
+// call audioIdle and audioAppEnded directly.
+
+#include "audioInt.h"
+#include "shellApp.h"
+
+static void audioShellInit(void) __attribute__((constructor));
+static void audioShellShutdown(void) __attribute__((destructor));
+
+
+static void audioShellInit(void) {
+ shellRegisterIdle(audioIdle, NULL);
+ shellRegisterAppCleanup(audioAppEnded);
+ audioDeskInit();
+}
+
+
+static void audioShellShutdown(void) {
+ audioShutdown();
+ shellUnregisterAppCleanup(audioAppEnded);
+ shellUnregisterIdle(audioIdle, NULL);
+}
diff --git a/src/libs/kpunch/audio/audioSynth.c b/src/libs/kpunch/audio/audioSynth.c
new file mode 100644
index 00000000..66c80bf4
--- /dev/null
+++ b/src/libs/kpunch/audio/audioSynth.c
@@ -0,0 +1,567 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioSynth.c -- the synthesizers: FM voices shared by every program,
+// the whole FM chip for one program, the MIDI port, tones on FM voices
+//
+// See audio.h ("FM synthesizer", "MIDI port"). The functions a timer
+// callback may call (audioFmAlloc, audioFmFree, audioFmWrite,
+// audioFmRawWrite, audioMidiPut) and audioFmChannelOff, which the mixer
+// calls at interrupt time, keep to the interrupt rules: no calls out of
+// the file but the driver's write / put, interrupts off while they
+// change the voice table, everything pinned (audioSynthLock). The rest
+// runs at program time.
+//
+// A voice id is generation * FM_ID_STRIDE + channel + 1, so an id of a
+// voice given back never reaches the channel's next owner. Free voices
+// are handed out least recently freed first: the channel whose released
+// note has had the longest time to fade.
+
+#include "audioInt.h"
+
+#include
+#include
+
+#define FM_ID_STRIDE 32
+#define FM_GEN_MAX (INT32_MAX / FM_ID_STRIDE - 1)
+#define FM_BANK_VOICES 9 // channels per register bank
+#define FM_OP_GROUP_MASK 0xE0 // operator registers: the group ...
+#define FM_OP_INDEX_MASK 0x1F // ... and the operator (0 modulator, 3 carrier)
+#define FM_CARRIER 3
+#define FM_CHANNEL_REG_LOW 0xA0 // first channel register (A0, B0, C0)
+#define FM_CHANNEL_REG_HIGH 0xC0
+#define FM_CHANNEL_REG_MASK 0x0F
+
+// Chip set-up registers and values
+#define FM_REG_TEST 0x01
+#define FM_TEST_WSE 0x20 // OPL2: let waveform select work
+#define FM_REG_CSM 0x08
+#define FM_REG_RHYTHM 0xBD
+#define FM_REG_OPL3 0x105 // bank 1: OPL3 features on
+#define FM_REG_FOUR_OP 0x104 // bank 1: four-operator pairs off
+#define FM_OPL3_ON 0x01
+#define FM_LEVEL_SILENT 0x3F
+
+// Tones: square = carrier at the pitch, modulator at twice it (odd
+// harmonics only, like a square wave), gently modulated; triangle = the
+// carrier alone (a sine). Instant attack, full sustain, fastest release.
+#define FM_TONE_MOD_SQUARE 0x22 // sustaining, multiple 2
+#define FM_TONE_CAR_CHAR 0x21 // sustaining, multiple 1
+#define FM_TONE_MOD_LEVEL 0x28 // 30 dB below full
+#define FM_TONE_ATTACK 0xF0
+#define FM_TONE_SUSTAIN 0x0F
+#define FM_FREQ_SHIFT 20 // hz = fnum * CHIP_HZ * 2^block / 2^20
+
+// Loudness to attenuation (0.75 dB steps), per AUDIO_VOLUME_MAX / 64 of
+// gain: round(-20 log10(i / 64) / 0.75)
+#define FM_GAIN_TABLE_SHIFT 2
+static const uint8_t sTlForGain[] = {
+ 63, 48, 40, 35, 32, 30, 27, 26, 24, 23, 21, 20, 19, 18, 18, 17,
+ 16, 15, 15, 14, 13, 13, 12, 12, 11, 11, 10, 10, 10, 9, 9, 8,
+ 8, 8, 7, 7, 7, 6, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4,
+ 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0,
+ 0
+};
+
+// Operator register offset of each channel of a bank (modulator; the
+// carrier is FM_CARRIER further)
+static const uint8_t sOpOffset[FM_BANK_VOICES] = { 0, 1, 2, 8, 9, 10, 16, 17, 18 };
+
+typedef struct {
+ AudioCtxT *owner; // NULL: free
+ int32_t id;
+ uint32_t freedAt; // allocation clock when given back (0: never used)
+ uint8_t keyBlock; // last AUDIO_FM_KEY_BLOCK value written
+} FmVoiceT;
+
+typedef struct {
+ void (*write)(uint32_t reg, uint8_t value); // NULL: no FM chip
+ FmVoiceT voices[AUDIO_FM_VOICES_MAX];
+ AudioCtxT *rawOwner;
+ uint32_t count;
+ uint32_t clock;
+ uint32_t generation;
+ bool stereo;
+ char description[AUDIO_DESC_MAX];
+} FmChipT;
+
+typedef struct {
+ bool (*put)(uint8_t byte); // NULL: no MIDI port
+ AudioCtxT *owner;
+ char description[AUDIO_DESC_MAX];
+} MidiPortT;
+
+static FmChipT sFm;
+static MidiPortT sMidi;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+int32_t audioFmAlloc(AudioCtxT *ctx);
+void audioFmAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info);
+int32_t audioFmChannelOf(const AudioCtxT *ctx, int32_t voice);
+void audioFmChannelOff(uint32_t channel);
+void audioFmDetach(void);
+int32_t audioFmFree(AudioCtxT *ctx, int32_t voice);
+void audioFmGetInfo(AudioFmInfoT *info);
+int32_t audioFmRawAcquire(AudioCtxT *ctx);
+int32_t audioFmRawRelease(AudioCtxT *ctx);
+int32_t audioFmRawWrite(AudioCtxT *ctx, uint32_t reg, uint8_t value);
+int32_t audioFmToneStart(AudioCtxT *ctx, AudioWaveE wave);
+void audioFmToneUpdate(AudioCtxT *ctx, int32_t voice, uint32_t hz, int32_t gainL, int32_t gainR);
+int32_t audioFmWrite(AudioCtxT *ctx, int32_t voice, uint8_t reg, uint8_t value);
+int32_t audioMidiAcquire(AudioCtxT *ctx);
+void audioMidiAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info);
+void audioMidiDetach(void);
+void audioMidiGetInfo(AudioMidiInfoT *info);
+int32_t audioMidiPut(AudioCtxT *ctx, uint8_t byte);
+int32_t audioMidiRelease(AudioCtxT *ctx);
+void audioSynthCtxClosed(AudioCtxT *ctx);
+bool audioSynthLock(bool lock);
+static void fmChipInit(void);
+static void fmGiveBack(uint32_t channel);
+static int32_t fmLookup(const AudioCtxT *ctx, int32_t voice);
+static int32_t fmRegister(uint32_t channel, uint8_t reg);
+static void fmWriteKey(uint32_t channel, uint8_t keyBlock);
+
+
+// Interrupt-safe.
+int32_t audioFmAlloc(AudioCtxT *ctx) {
+ if (!ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ if (!sFm.write) {
+ return AUDIO_ERR_NO_SYNTH;
+ }
+
+ uint32_t flags = audioIrqOff();
+ int32_t best = -1;
+ int32_t rc;
+
+ for (uint32_t i = 0; i < sFm.count && !sFm.rawOwner; i++) {
+ const FmVoiceT *v = &sFm.voices[i];
+
+ if (!v->owner && (best < 0 || v->freedAt < sFm.voices[best].freedAt)) {
+ best = (int32_t)i;
+ }
+ }
+
+ if (sFm.rawOwner) {
+ rc = AUDIO_ERR_BUSY;
+ } else if (best < 0) {
+ rc = AUDIO_ERR_NO_VOICE;
+ } else {
+ if (++sFm.generation > FM_GEN_MAX) {
+ sFm.generation = 1;
+ }
+
+ sFm.voices[best].owner = ctx;
+ sFm.voices[best].id = (int32_t)(sFm.generation * FM_ID_STRIDE) + best + 1;
+ rc = sFm.voices[best].id;
+ }
+
+ audioIrqRestore(flags);
+ return rc;
+}
+
+
+// Called while the device starts (no interrupt uses the chip yet).
+void audioFmAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info) {
+ memset(&sFm, 0, sizeof(sFm));
+
+ if (!drv) {
+ return;
+ }
+
+ sFm.write = drv->write;
+ sFm.count = info->voices > AUDIO_FM_VOICES_MAX ? AUDIO_FM_VOICES_MAX : info->voices;
+ sFm.stereo = info->stereo;
+ snprintf(sFm.description, sizeof(sFm.description), "%s", info->description);
+ fmChipInit();
+}
+
+
+int32_t audioFmChannelOf(const AudioCtxT *ctx, int32_t voice) {
+ return fmLookup(ctx, voice);
+}
+
+
+// Interrupt time (the mixer ending a timed FM tone).
+void audioFmChannelOff(uint32_t channel) {
+ fmWriteKey(channel, (uint8_t)(sFm.voices[channel].keyBlock & ~AUDIO_FM_KEY_ON));
+}
+
+
+void audioFmDetach(void) {
+ memset(&sFm, 0, sizeof(sFm));
+}
+
+
+// Interrupt-safe.
+int32_t audioFmFree(AudioCtxT *ctx, int32_t voice) {
+ uint32_t flags = audioIrqOff();
+ int32_t channel = fmLookup(ctx, voice);
+
+ if (channel >= 0) {
+ fmGiveBack((uint32_t)channel);
+ }
+
+ audioIrqRestore(flags);
+ return channel >= 0 ? AUDIO_OK : AUDIO_ERR_VOICE;
+}
+
+
+void audioFmGetInfo(AudioFmInfoT *info) {
+ memset(info, 0, sizeof(*info));
+
+ if (!sFm.write) {
+ return;
+ }
+
+ snprintf(info->description, sizeof(info->description), "%s", sFm.description);
+ info->voices = sFm.count;
+ info->stereo = sFm.stereo;
+ info->rawOwned = sFm.rawOwner != NULL;
+
+ for (uint32_t i = 0; i < sFm.count && !sFm.rawOwner; i++) {
+ if (!sFm.voices[i].owner) {
+ info->freeVoices++;
+ }
+ }
+}
+
+
+// Every voice is taken back: keyed off, and a tone playing on one goes on
+// in the mixer.
+int32_t audioFmRawAcquire(AudioCtxT *ctx) {
+ if (!ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ if (!sFm.write) {
+ return AUDIO_ERR_NO_SYNTH;
+ }
+
+ if (sFm.rawOwner) {
+ return sFm.rawOwner == ctx ? AUDIO_OK : AUDIO_ERR_BUSY;
+ }
+
+ uint32_t flags = audioIrqOff();
+
+ for (uint32_t i = 0; i < sFm.count; i++) {
+ if (sFm.voices[i].owner) {
+ fmGiveBack(i);
+ }
+ }
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ audioMixer.voices[i].fmSlot = 0;
+ }
+
+ sFm.rawOwner = ctx;
+ audioIrqRestore(flags);
+ return AUDIO_OK;
+}
+
+
+int32_t audioFmRawRelease(AudioCtxT *ctx) {
+ if (!ctx || !sFm.write || sFm.rawOwner != ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ fmChipInit();
+ sFm.rawOwner = NULL;
+ return AUDIO_OK;
+}
+
+
+// Interrupt-safe.
+int32_t audioFmRawWrite(AudioCtxT *ctx, uint32_t reg, uint8_t value) {
+ uint32_t regEnd = sFm.stereo ? AUDIO_FM_BANK_REGS * 2 : AUDIO_FM_BANK_REGS;
+
+ if (!ctx || !sFm.write || sFm.rawOwner != ctx || reg >= regEnd) {
+ return AUDIO_ERR_ARG;
+ }
+
+ sFm.write(reg, value);
+ return AUDIO_OK;
+}
+
+
+// The tone's patch; audioFmToneUpdate then sets its pitch and loudness
+// and keys it on.
+int32_t audioFmToneStart(AudioCtxT *ctx, AudioWaveE wave) {
+ int32_t voice = audioFmAlloc(ctx);
+
+ if (voice < 0) {
+ return voice;
+ }
+
+ bool square = wave == AUDIO_WAVE_SQUARE;
+
+ audioFmWrite(ctx, voice, AUDIO_FM_MOD_CHAR, FM_TONE_MOD_SQUARE);
+ audioFmWrite(ctx, voice, AUDIO_FM_CAR_CHAR, FM_TONE_CAR_CHAR);
+ audioFmWrite(ctx, voice, AUDIO_FM_MOD_LEVEL, square ? FM_TONE_MOD_LEVEL : FM_LEVEL_SILENT);
+ audioFmWrite(ctx, voice, AUDIO_FM_CAR_LEVEL, FM_LEVEL_SILENT);
+ audioFmWrite(ctx, voice, AUDIO_FM_MOD_ATTACK, FM_TONE_ATTACK);
+ audioFmWrite(ctx, voice, AUDIO_FM_CAR_ATTACK, FM_TONE_ATTACK);
+ audioFmWrite(ctx, voice, AUDIO_FM_MOD_SUSTAIN, FM_TONE_SUSTAIN);
+ audioFmWrite(ctx, voice, AUDIO_FM_CAR_SUSTAIN, FM_TONE_SUSTAIN);
+ audioFmWrite(ctx, voice, AUDIO_FM_MOD_WAVE, 0);
+ audioFmWrite(ctx, voice, AUDIO_FM_CAR_WAVE, 0);
+ return voice;
+}
+
+
+// The lowest block that holds hz (above the chip's range: its highest
+// note), the loudness of the louder side, and on an OPL3 the side(s) with
+// any gain.
+void audioFmToneUpdate(AudioCtxT *ctx, int32_t voice, uint32_t hz, int32_t gainL, int32_t gainR) {
+ uint32_t block = 0;
+ uint64_t fnum = ((uint64_t)hz << FM_FREQ_SHIFT) / AUDIO_FM_CHIP_HZ;
+
+ while (fnum > AUDIO_FM_FNUM_MAX && block < AUDIO_FM_BLOCK_MAX) {
+ block++;
+ fnum = ((uint64_t)hz << (FM_FREQ_SHIFT - block)) / AUDIO_FM_CHIP_HZ;
+ }
+
+ if (fnum > AUDIO_FM_FNUM_MAX) {
+ fnum = AUDIO_FM_FNUM_MAX;
+ }
+
+ int32_t gain = gainL > gainR ? gainL : gainR;
+ uint8_t out = 0;
+
+ if (gainL > 0) {
+ out |= AUDIO_FM_OUT_LEFT;
+ }
+
+ if (gainR > 0) {
+ out |= AUDIO_FM_OUT_RIGHT;
+ }
+
+ audioFmWrite(ctx, voice, AUDIO_FM_CAR_LEVEL, sTlForGain[gain >> FM_GAIN_TABLE_SHIFT]);
+ audioFmWrite(ctx, voice, AUDIO_FM_FEEDBACK, out ? out : AUDIO_FM_OUT_BOTH);
+ audioFmWrite(ctx, voice, AUDIO_FM_FNUM_LOW, (uint8_t)fnum);
+ audioFmWrite(ctx, voice, AUDIO_FM_KEY_BLOCK, (uint8_t)(AUDIO_FM_KEY_ON | (block << AUDIO_FM_BLOCK_SHIFT) | (fnum >> AUDIO_FM_FNUM_HI_SHIFT)));
+}
+
+
+// A feedback value without output bits plays on both sides of an OPL3.
+// Interrupt-safe.
+int32_t audioFmWrite(AudioCtxT *ctx, int32_t voice, uint8_t reg, uint8_t value) {
+ uint32_t flags = audioIrqOff();
+ int32_t channel = fmLookup(ctx, voice);
+ int32_t abs = channel >= 0 ? fmRegister((uint32_t)channel, reg) : -1;
+ int32_t rc = AUDIO_OK;
+
+ if (channel < 0) {
+ rc = AUDIO_ERR_VOICE;
+ } else if (abs < 0) {
+ rc = AUDIO_ERR_ARG;
+ } else if (reg == AUDIO_FM_KEY_BLOCK) {
+ fmWriteKey((uint32_t)channel, value);
+ } else {
+ if (reg == AUDIO_FM_FEEDBACK && sFm.stereo && !(value & AUDIO_FM_OUT_BOTH)) {
+ value |= AUDIO_FM_OUT_BOTH;
+ }
+
+ sFm.write((uint32_t)abs, value);
+ }
+
+ audioIrqRestore(flags);
+ return rc;
+}
+
+
+int32_t audioMidiAcquire(AudioCtxT *ctx) {
+ if (!ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ if (!sMidi.put) {
+ return AUDIO_ERR_NO_SYNTH;
+ }
+
+ if (sMidi.owner && sMidi.owner != ctx) {
+ return AUDIO_ERR_BUSY;
+ }
+
+ sMidi.owner = ctx;
+ return AUDIO_OK;
+}
+
+
+void audioMidiAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info) {
+ memset(&sMidi, 0, sizeof(sMidi));
+
+ if (drv) {
+ sMidi.put = drv->put;
+ snprintf(sMidi.description, sizeof(sMidi.description), "%s", info->description);
+ }
+}
+
+
+void audioMidiDetach(void) {
+ memset(&sMidi, 0, sizeof(sMidi));
+}
+
+
+void audioMidiGetInfo(AudioMidiInfoT *info) {
+ memset(info, 0, sizeof(*info));
+ snprintf(info->description, sizeof(info->description), "%s", sMidi.description);
+ info->present = sMidi.put != NULL;
+ info->owned = sMidi.owner != NULL;
+}
+
+
+// Interrupt-safe.
+int32_t audioMidiPut(AudioCtxT *ctx, uint8_t byte) {
+ if (!ctx || !sMidi.put || sMidi.owner != ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ return sMidi.put(byte) ? AUDIO_OK : AUDIO_ERR_BUSY;
+}
+
+
+int32_t audioMidiRelease(AudioCtxT *ctx) {
+ if (!ctx || sMidi.owner != ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ sMidi.owner = NULL;
+ return AUDIO_OK;
+}
+
+
+void audioSynthCtxClosed(AudioCtxT *ctx) {
+ uint32_t flags = audioIrqOff();
+
+ for (uint32_t i = 0; i < sFm.count; i++) {
+ if (sFm.voices[i].owner == ctx) {
+ fmGiveBack(i);
+ }
+ }
+
+ audioIrqRestore(flags);
+ audioFmRawRelease(ctx);
+ audioMidiRelease(ctx);
+}
+
+
+bool audioSynthLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)audioFmAlloc, 0 },
+ { (const void *)audioFmChannelOff, 0 },
+ { (const void *)audioFmFree, 0 },
+ { (const void *)audioFmRawWrite, 0 },
+ { (const void *)audioFmWrite, 0 },
+ { (const void *)audioMidiPut, 0 },
+ { (const void *)fmGiveBack, 0 },
+ { (const void *)fmLookup, 0 },
+ { (const void *)fmRegister, 0 },
+ { (const void *)fmWriteKey, 0 },
+ { &sFm, sizeof(sFm) },
+ { &sMidi, sizeof(sMidi) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+// Waveforms on, rhythm and timers off, on an OPL3 its own features on;
+// every channel keyed off and silent, playing on both sides.
+static void fmChipInit(void) {
+ sFm.write(FM_REG_TEST, FM_TEST_WSE);
+ sFm.write(FM_REG_CSM, 0);
+ sFm.write(FM_REG_RHYTHM, 0);
+
+ if (sFm.stereo) {
+ sFm.write(FM_REG_OPL3, FM_OPL3_ON);
+ sFm.write(FM_REG_FOUR_OP, 0);
+ }
+
+ for (uint32_t i = 0; i < sFm.count; i++) {
+ fmWriteKey(i, 0);
+ sFm.write((uint32_t)fmRegister(i, AUDIO_FM_MOD_LEVEL), FM_LEVEL_SILENT);
+ sFm.write((uint32_t)fmRegister(i, AUDIO_FM_CAR_LEVEL), FM_LEVEL_SILENT);
+ sFm.write((uint32_t)fmRegister(i, AUDIO_FM_FEEDBACK), sFm.stereo ? AUDIO_FM_OUT_BOTH : 0);
+ }
+}
+
+
+// Key off, free, remember when (interrupts are off).
+static void fmGiveBack(uint32_t channel) {
+ FmVoiceT *v = &sFm.voices[channel];
+
+ fmWriteKey(channel, (uint8_t)(v->keyBlock & ~AUDIO_FM_KEY_ON));
+ v->owner = NULL;
+ v->id = 0;
+ v->freedAt = ++sFm.clock;
+}
+
+
+// The channel of ctx's voice with this id, else -1.
+static int32_t fmLookup(const AudioCtxT *ctx, int32_t voice) {
+ if (!ctx || voice <= 0) {
+ return -1;
+ }
+
+ int32_t channel = (voice - 1) % FM_ID_STRIDE;
+
+ if ((uint32_t)channel >= sFm.count || sFm.voices[channel].owner != ctx || sFm.voices[channel].id != voice) {
+ return -1;
+ }
+
+ return channel;
+}
+
+
+// The chip register for a channel register (AUDIO_FM_*), or -1 when reg
+// is not one.
+static int32_t fmRegister(uint32_t channel, uint8_t reg) {
+ uint32_t bank = channel / FM_BANK_VOICES * AUDIO_FM_BANK_REGS;
+ uint32_t local = channel % FM_BANK_VOICES;
+
+ if (reg >= FM_CHANNEL_REG_LOW && reg <= FM_CHANNEL_REG_HIGH) {
+ return (reg & FM_CHANNEL_REG_MASK) ? -1 : (int32_t)(bank + reg + local);
+ }
+
+ uint8_t group = reg & FM_OP_GROUP_MASK;
+ uint8_t op = reg & FM_OP_INDEX_MASK;
+ bool isOp = group == AUDIO_FM_MOD_CHAR || group == AUDIO_FM_MOD_LEVEL || group == AUDIO_FM_MOD_ATTACK || group == AUDIO_FM_MOD_SUSTAIN || group == AUDIO_FM_MOD_WAVE;
+
+ if (!isOp || (op != 0 && op != FM_CARRIER)) {
+ return -1;
+ }
+
+ return (int32_t)(bank + group + sOpOffset[local] + op);
+}
+
+
+static void fmWriteKey(uint32_t channel, uint8_t keyBlock) {
+ sFm.voices[channel].keyBlock = keyBlock;
+ sFm.write((uint32_t)fmRegister(channel, AUDIO_FM_KEY_BLOCK), keyBlock);
+}
diff --git a/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c b/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
new file mode 100644
index 00000000..ea416a23
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
@@ -0,0 +1,182 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// mpuDrv.c -- the MPU-401 MIDI port driver (mpu401.drv)
+//
+// A synth driver (audioDrv.h "Synthesizer drivers") for an external MIDI
+// synthesizer (a Roland MT-32 or SC-55, a General MIDI module) on an
+// MPU-401 or compatible port, in UART mode: bytes written to the data
+// port go out on the MIDI cable as they are. Looked for only when
+// DVX.INI [audio] midi = mpu401 (at mpuPort, hex, default 330h), because
+// a port with nothing plugged in would play nothing.
+//
+// Status port (data port + 1): bit 6 clear = ready to take a byte, bit 7
+// clear = a byte to read. Reset: FFh to the command (status) port, the
+// port answers FEh; UART mode: 3Fh, answered FEh too (UART-only clones
+// may not answer that one). Bytes the port sends (active sensing, input)
+// are read and dropped. put waits a little for the port to be ready and
+// otherwise returns false, so the caller sends the byte later; it runs at
+// interrupt time too and is pinned. Close sends All Notes Off on every
+// channel and resets the port (out of UART mode).
+
+#include "audioDrv.h"
+
+#include
+#include
+#include
+#include
+
+#define PORT_BASE_HEX 16
+#define MPU_STATUS 1 // status / command port: data port + 1
+#define MPU_NOT_READY 0x40 // status: cannot take a byte
+#define MPU_NO_DATA 0x80 // status: nothing to read
+#define MPU_CMD_RESET 0xFF
+#define MPU_CMD_UART 0x3F
+#define MPU_ACK 0xFE
+#define MPU_ACK_READS 50000 // status reads to wait for an answer
+#define MPU_PUT_READS 64 // status reads put waits for the port
+#define MPU_DRAIN_MAX 16 // input bytes dropped per put
+
+// Sent on close: All Notes Off (controller 123) on each channel
+#define MIDI_CONTROL 0xB0
+#define MIDI_CHANNELS 16
+#define MIDI_ALL_NOTES_OFF 123
+
+typedef struct {
+ uint16_t data;
+ uint16_t status;
+} MpuT;
+
+static MpuT sMpu;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void mpuClose(void);
+static bool mpuCommand(uint8_t cmd);
+static bool mpuLock(bool lock);
+static bool mpuOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
+static bool mpuPut(uint8_t byte);
+static void mpuPutWait(uint8_t byte);
+
+AUDIO_SYNTH_DRIVER("mpu401", AUDIO_PRIORITY_CARD,
+ .kind = AUDIO_SYNTH_MIDI,
+ .open = mpuOpen,
+ .close = mpuClose,
+ .put = mpuPut
+);
+
+
+static void mpuClose(void) {
+ for (uint8_t ch = 0; ch < MIDI_CHANNELS; ch++) {
+ mpuPutWait((uint8_t)(MIDI_CONTROL | ch));
+ mpuPutWait(MIDI_ALL_NOTES_OFF);
+ mpuPutWait(0);
+ }
+
+ mpuCommand(MPU_CMD_RESET);
+ mpuLock(false);
+}
+
+
+// Send a command and wait for its acknowledgement (other bytes read on
+// the way are dropped).
+static bool mpuCommand(uint8_t cmd) {
+ int32_t reads = 0;
+
+ while ((inportb(sMpu.status) & MPU_NOT_READY) && reads < MPU_ACK_READS) {
+ reads++;
+ }
+
+ outportb(sMpu.status, cmd);
+
+ for (reads = 0; reads < MPU_ACK_READS; reads++) {
+ if (!(inportb(sMpu.status) & MPU_NO_DATA) && inportb(sMpu.data) == MPU_ACK) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+static bool mpuLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)mpuPut, 0 },
+ { &sMpu, sizeof(sMpu) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+// A port left in UART mode ignores the first reset (it takes FFh as
+// data); the second one is answered.
+static bool mpuOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) {
+ if (strcasecmp(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO), AUDIO_MIDI_MPU401) != 0) {
+ return false;
+ }
+
+ sMpu.data = (uint16_t)strtoul(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MPU_PORT, AUDIO_MPU_PORT_DEFAULT), NULL, PORT_BASE_HEX);
+ sMpu.status = (uint16_t)(sMpu.data + MPU_STATUS);
+
+ if (!mpuCommand(MPU_CMD_RESET) && !mpuCommand(MPU_CMD_RESET)) {
+ return false;
+ }
+
+ // UART-only clones may not acknowledge this.
+ mpuCommand(MPU_CMD_UART);
+
+ if (!mpuLock(true)) {
+ return false;
+ }
+
+ snprintf(info->description, sizeof(info->description), "MPU-401 MIDI port at %Xh", (unsigned)sMpu.data);
+ return true;
+}
+
+
+// Drop what the port has sent, then send the byte if the port gets ready
+// soon enough.
+static bool mpuPut(uint8_t byte) {
+ for (int32_t i = 0; i < MPU_DRAIN_MAX && !(inportb(sMpu.status) & MPU_NO_DATA); i++) {
+ (void)inportb(sMpu.data);
+ }
+
+ for (int32_t i = 0; i < MPU_PUT_READS; i++) {
+ if (!(inportb(sMpu.status) & MPU_NOT_READY)) {
+ outportb(sMpu.data, byte);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// put, retried for as long as an acknowledgement may take.
+static void mpuPutWait(uint8_t byte) {
+ for (int32_t i = 0; i < MPU_ACK_READS / MPU_PUT_READS && !mpuPut(byte); i++) {
+ // the port is busy
+ }
+}
diff --git a/src/libs/kpunch/audio/drivers/null/nullDrv.c b/src/libs/kpunch/audio/drivers/null/nullDrv.c
new file mode 100644
index 00000000..56304c4a
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/null/nullDrv.c
@@ -0,0 +1,72 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// nullDrv.c -- the null sound driver (null.drv)
+//
+// Plays nothing and accepts everything, so programs work the same on a
+// computer without sound hardware: voices still run their course (and
+// report their end) because the timer service drives the mixer, at a
+// low rate and into a buffer nobody hears. Tried last.
+
+#include "audioDrv.h"
+
+#include
+
+// Mix as little as the voices need to keep time
+#define NULL_RATE 11025
+#define NULL_BITS 8
+#define NULL_CHANNELS 1
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void nullClose(void);
+static bool nullOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
+static bool nullPlay(void);
+
+AUDIO_DRIVER("null", AUDIO_PRIORITY_NULL,
+ .open = nullOpen,
+ .play = nullPlay,
+ .close = nullClose
+);
+
+
+static void nullClose(void) {
+ audioClockStop();
+}
+
+
+static bool nullOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
+ (void)cfg;
+
+ snprintf(info->description, sizeof(info->description), "No sound device");
+ info->rate = NULL_RATE;
+ info->bits = NULL_BITS;
+ info->channels = NULL_CHANNELS;
+ return true;
+}
+
+
+static bool nullPlay(void) {
+ return audioClockStart();
+}
diff --git a/src/libs/kpunch/audio/drivers/opl/oplDrv.c b/src/libs/kpunch/audio/drivers/opl/oplDrv.c
new file mode 100644
index 00000000..945fd4c6
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/opl/oplDrv.c
@@ -0,0 +1,214 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// oplDrv.c -- the OPL2 / OPL3 FM synthesizer driver (opl.drv)
+//
+// A synth driver (audioDrv.h "Synthesizer drivers"): finds the FM chip
+// of an AdLib or Sound Blaster at 388h (DVX.INI [audio] oplPort, hex)
+// and writes its registers for audio.lib, which shares its voices
+// between programs. Detection is the AdLib method: timer 1 started, its
+// flag must come up in the status register; the status bits that an
+// OPL2 always sets tell it from an OPL3 (18 voices, stereo, a second
+// register bank at base + 2).
+//
+// A register write is an index write, a short wait, a data write and a
+// longer wait (an OPL2 needs 3.3 us and 23 us; an OPL3 hardly any), timed
+// by reading the status port, with interrupts off so a timer callback
+// cannot slip its own write in between. write runs at interrupt time
+// too and is pinned. Close zeroes every register (silence).
+
+#include "audioDrv.h"
+
+#include
+#include
+#include
+
+#define OPL_PORT_DEFAULT "388"
+#define INI_OPL_PORT "oplPort"
+#define PORT_BASE_HEX 16
+#define OPL_DATA 1 // data port: index port + 1
+#define OPL_BANK_PORTS 2 // bank 1 index port: base + 2
+
+// Detection (timer 1 at its fastest rate overflows within 80 us)
+#define REG_TIMER1 0x02
+#define REG_TIMER_CTRL 0x04
+#define TIMER_RESET 0x60 // mask both timers
+#define TIMER_IRQ_RESET 0x80 // clear the flags
+#define TIMER1_START 0x21
+#define TIMER1_FASTEST 0xFF
+#define STATUS_FLAGS 0xE0
+#define STATUS_IDLE 0x00
+#define STATUS_TIMER1 0xC0
+#define STATUS_OPL2_BITS 0x06 // set on an OPL2, clear on an OPL3
+#define DETECT_WAIT_READS 200 // status reads: well over 80 us
+
+// Waits after the index and data writes, in status reads
+#define OPL2_INDEX_READS 6
+#define OPL2_DATA_READS 35
+#define OPL3_INDEX_READS 1
+#define OPL3_DATA_READS 2
+
+#define OPL2_VOICES 9
+#define OPL3_VOICES 18
+
+// Registers zeroed by close, and the OPL3 mode register
+#define REG_FIRST 0x01
+#define REG_LAST 0xF5
+#define REG_OPL3_MODE 0x105
+
+typedef struct {
+ uint16_t base;
+ uint16_t indexReads;
+ uint16_t dataReads;
+ bool opl3;
+} OplT;
+
+static OplT sOpl;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void oplClose(void);
+static bool oplDetect(uint16_t base, bool *opl3);
+static bool oplLock(bool lock);
+static bool oplOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
+static void oplWait(uint16_t port, uint16_t reads);
+static void oplWrite(uint32_t reg, uint8_t value);
+static void oplWriteBank(uint16_t port, uint8_t reg, uint8_t value);
+
+AUDIO_SYNTH_DRIVER("opl", AUDIO_PRIORITY_CARD,
+ .kind = AUDIO_SYNTH_FM,
+ .open = oplOpen,
+ .close = oplClose,
+ .write = oplWrite
+);
+
+
+// Silence: every register of both banks to zero, then the OPL3 back to
+// OPL2 mode.
+static void oplClose(void) {
+ for (uint32_t reg = REG_FIRST; reg <= REG_LAST; reg++) {
+ oplWrite(reg, 0);
+
+ if (sOpl.opl3) {
+ oplWrite(AUDIO_FM_BANK_REGS + reg, 0);
+ }
+ }
+
+ if (sOpl.opl3) {
+ oplWrite(REG_OPL3_MODE, 0);
+ }
+
+ oplLock(false);
+}
+
+
+// The AdLib test, with a register write that needs no timing yet (the
+// chip type is not known): generous waits.
+static bool oplDetect(uint16_t base, bool *opl3) {
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER_RESET);
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER_IRQ_RESET);
+
+ uint8_t before = inportb(base);
+
+ oplWriteBank(base, REG_TIMER1, TIMER1_FASTEST);
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER1_START);
+ oplWait(base, DETECT_WAIT_READS);
+
+ uint8_t after = inportb(base);
+
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER_RESET);
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER_IRQ_RESET);
+
+ if ((before & STATUS_FLAGS) != STATUS_IDLE || (after & STATUS_FLAGS) != STATUS_TIMER1) {
+ return false;
+ }
+
+ *opl3 = (before & STATUS_OPL2_BITS) == 0;
+ return true;
+}
+
+
+static bool oplLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)oplWait, 0 },
+ { (const void *)oplWrite, 0 },
+ { (const void *)oplWriteBank, 0 },
+ { &sOpl, sizeof(sOpl) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+static bool oplOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) {
+ const char *portText = prefsGetString(cfg->prefs, AUDIO_INI_SECTION, INI_OPL_PORT, OPL_PORT_DEFAULT);
+ uint16_t base = (uint16_t)strtoul(portText, NULL, PORT_BASE_HEX);
+ bool opl3 = false;
+
+ // Slow waits while detecting (an OPL2 needs them).
+ sOpl.base = base;
+ sOpl.indexReads = OPL2_INDEX_READS;
+ sOpl.dataReads = OPL2_DATA_READS;
+
+ if (!oplDetect(base, &opl3) || !oplLock(true)) {
+ return false;
+ }
+
+ sOpl.opl3 = opl3;
+ sOpl.indexReads = opl3 ? OPL3_INDEX_READS : OPL2_INDEX_READS;
+ sOpl.dataReads = opl3 ? OPL3_DATA_READS : OPL2_DATA_READS;
+
+ snprintf(info->description, sizeof(info->description), "%s FM synthesizer at %Xh", opl3 ? "OPL3" : "OPL2", (unsigned)base);
+ info->voices = opl3 ? OPL3_VOICES : OPL2_VOICES;
+ info->stereo = opl3;
+ return true;
+}
+
+
+static void oplWait(uint16_t port, uint16_t reads) {
+ for (uint16_t i = 0; i < reads; i++) {
+ (void)inportb(port);
+ }
+}
+
+
+// reg: bank * AUDIO_FM_BANK_REGS + index. Interrupts are off around the
+// index/data pair.
+static void oplWrite(uint32_t reg, uint8_t value) {
+ uint16_t port = (uint16_t)(sOpl.base + (reg / AUDIO_FM_BANK_REGS) * OPL_BANK_PORTS);
+ uint32_t flags = audioIrqOff();
+
+ oplWriteBank(port, (uint8_t)(reg % AUDIO_FM_BANK_REGS), value);
+ audioIrqRestore(flags);
+}
+
+
+// The waits read the status port (bank 0's index port) whichever bank
+// is written.
+static void oplWriteBank(uint16_t port, uint8_t reg, uint8_t value) {
+ outportb(port, reg);
+ oplWait(sOpl.base, sOpl.indexReads);
+ outportb((uint16_t)(port + OPL_DATA), value);
+ oplWait(sOpl.base, sOpl.dataReads);
+}
diff --git a/src/libs/kpunch/audio/drivers/sb/sbCalc.c b/src/libs/kpunch/audio/drivers/sb/sbCalc.c
new file mode 100644
index 00000000..7f9a1790
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/sb/sbCalc.c
@@ -0,0 +1,218 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// sbCalc.c -- Sound Blaster driver arithmetic; see sbCalc.h
+//
+// Rates (Creative Labs, "Sound Blaster Series Hardware Programming
+// Guide", table 3-2): SB16 5,000-44,100 Hz set exactly; earlier cards run
+// from a time constant TC = 256 - 1,000,000 / (channels x rate), so the
+// achieved rate is 1,000,000 / (channels x (256 - TC)): 22,050 Hz mono
+// becomes 22,222 Hz. Mono tops out at 23,000 Hz in normal mode and
+// 44,100 Hz in high-speed mode (DSP 2.01 and later); SB Pro stereo is
+// 11,025 or 22,050 Hz, the latter in high-speed mode.
+
+#include "sbCalc.h"
+
+#include
+
+#define TC_BASE 256
+#define TC_CLOCK 1000000
+#define RATE_SB16_MIN 5000
+#define RATE_SB16_MAX 44100
+#define RATE_MIN 4000
+#define RATE_NORMAL_MAX 23000
+#define RATE_HIGH_MAX 44100
+#define RATE_STEREO_LOW 11025
+#define RATE_STEREO_HIGH 22050
+#define BITS_8 8
+#define BITS_16 16
+#define CHANNELS_MONO 1
+#define CHANNELS_STEREO 2
+#define HEX_BASE 16
+#define DEC_BASE 10
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static uint32_t clampRate(uint32_t rate, uint32_t lo, uint32_t hi);
+static uint32_t parseNumber(const char **p, uint32_t base);
+uint32_t sbDmaPlace(uint32_t phys, uint32_t allocBytes, uint32_t bufBytes, uint32_t boundary);
+const char *sbModelName(uint32_t dspVersion);
+bool sbParseBlaster(const char *text, SbBlasterT *out);
+void sbPlan(uint32_t dspVersion, uint32_t rate, uint32_t bits, uint32_t channels, bool have16Dma, SbPlanT *plan);
+
+
+static uint32_t clampRate(uint32_t rate, uint32_t lo, uint32_t hi) {
+ if (rate < lo) {
+ return lo;
+ }
+
+ return rate > hi ? hi : rate;
+}
+
+
+static uint32_t parseNumber(const char **p, uint32_t base) {
+ uint32_t v = 0;
+
+ for (;;) {
+ char c = **p;
+ uint32_t d;
+
+ if (c >= '0' && c <= '9') {
+ d = (uint32_t)(c - '0');
+ } else if (base == HEX_BASE && c >= 'A' && c <= 'F') {
+ d = (uint32_t)(c - 'A' + DEC_BASE);
+ } else if (base == HEX_BASE && c >= 'a' && c <= 'f') {
+ d = (uint32_t)(c - 'a' + DEC_BASE);
+ } else {
+ return v;
+ }
+
+ v = v * base + d;
+ (*p)++;
+ }
+}
+
+
+uint32_t sbDmaPlace(uint32_t phys, uint32_t allocBytes, uint32_t bufBytes, uint32_t boundary) {
+ uint32_t start = phys;
+
+ if (start / boundary != (start + bufBytes - 1) / boundary) {
+ start = (start / boundary + 1) * boundary;
+ }
+
+ if (start + bufBytes > phys + allocBytes) {
+ return SB_NONE;
+ }
+
+ return start - phys;
+}
+
+
+const char *sbModelName(uint32_t dspVersion) {
+ if (dspVersion >= SB_DSP_SB16) {
+ return "Sound Blaster 16";
+ }
+
+ if (dspVersion >= SB_DSP_PRO) {
+ return "Sound Blaster Pro";
+ }
+
+ if (dspVersion >= SB_DSP_HIGH_SPEED) {
+ return "Sound Blaster 2.0";
+ }
+
+ return "Sound Blaster";
+}
+
+
+// Letters followed by a value, separated by blanks: A (hex), I, D, H, T
+// (decimal). Unknown letters (M, P, E) are skipped.
+bool sbParseBlaster(const char *text, SbBlasterT *out) {
+ out->base = SB_NONE;
+ out->irq = SB_NONE;
+ out->dma8 = SB_NONE;
+ out->dma16 = SB_NONE;
+ out->type = SB_NONE;
+
+ const char *p = text;
+
+ while (p && *p) {
+ char key = *p++;
+
+ switch (key) {
+ case 'A':
+ case 'a':
+ out->base = parseNumber(&p, HEX_BASE);
+ break;
+
+ case 'I':
+ case 'i':
+ out->irq = parseNumber(&p, DEC_BASE);
+ break;
+
+ case 'D':
+ case 'd':
+ out->dma8 = parseNumber(&p, DEC_BASE);
+ break;
+
+ case 'H':
+ case 'h':
+ out->dma16 = parseNumber(&p, DEC_BASE);
+ break;
+
+ case 'T':
+ case 't':
+ out->type = parseNumber(&p, DEC_BASE);
+ break;
+
+ default:
+ while (*p && *p != ' ' && *p != '\t') {
+ p++;
+ }
+
+ break;
+ }
+
+ while (*p == ' ' || *p == '\t') {
+ p++;
+ }
+ }
+
+ return out->base != SB_NONE && out->base != 0;
+}
+
+
+void sbPlan(uint32_t dspVersion, uint32_t rate, uint32_t bits, uint32_t channels, bool have16Dma, SbPlanT *plan) {
+ if (dspVersion >= SB_DSP_SB16) {
+ plan->mode = SB_MODE_SB16;
+ plan->channels = channels ? channels : CHANNELS_STEREO;
+ plan->bits = (bits == BITS_8 || !have16Dma) ? BITS_8 : BITS_16;
+ plan->rate = clampRate(rate, RATE_SB16_MIN, RATE_SB16_MAX);
+ plan->timeConstant = 0;
+ return;
+ }
+
+ plan->bits = BITS_8;
+ plan->channels = (dspVersion >= SB_DSP_PRO && channels != CHANNELS_MONO) ? CHANNELS_STEREO : CHANNELS_MONO;
+
+ if (plan->channels == CHANNELS_STEREO) {
+ rate = rate >= (RATE_STEREO_LOW + RATE_STEREO_HIGH) / 2 ? RATE_STEREO_HIGH : RATE_STEREO_LOW;
+ } else {
+ rate = clampRate(rate, RATE_MIN, dspVersion >= SB_DSP_HIGH_SPEED ? RATE_HIGH_MAX : RATE_NORMAL_MAX);
+ }
+
+ uint32_t total = rate * plan->channels;
+ uint32_t period = (TC_CLOCK + total / 2) / total;
+
+ plan->timeConstant = TC_BASE - period;
+ plan->rate = TC_CLOCK / (period * plan->channels);
+
+ if (dspVersion < SB_DSP_AUTO_INIT) {
+ plan->mode = SB_MODE_SINGLE;
+ } else if (total > RATE_NORMAL_MAX) {
+ plan->mode = SB_MODE_HIGH;
+ } else {
+ plan->mode = SB_MODE_AUTO;
+ }
+}
diff --git a/src/libs/kpunch/audio/drivers/sb/sbCalc.h b/src/libs/kpunch/audio/drivers/sb/sbCalc.h
new file mode 100644
index 00000000..e166a65e
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/sb/sbCalc.h
@@ -0,0 +1,90 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// sbCalc.h -- Sound Blaster driver arithmetic (no hardware access)
+//
+// The parts of sb.drv that decide rather than touch hardware, kept apart
+// so the host tests run them: the BLASTER variable, the playback plan
+// for a DSP version (mode, achieved rate, time constant) and where the
+// DMA buffer goes in a conventional-memory block.
+#ifndef SB_CALC_H
+#define SB_CALC_H
+
+#include
+#include
+
+// DSP versions (major << 8 | minor) where the card families begin
+#define SB_DSP_AUTO_INIT 0x0200 // SB 1.5 / 2.0: 8-bit auto-init DMA
+#define SB_DSP_HIGH_SPEED 0x0201 // SB 2.0: high-speed mode (up to 44,100 Hz)
+#define SB_DSP_PRO 0x0300 // SB Pro: stereo
+#define SB_DSP_SB16 0x0400 // SB16: 16-bit, any rate
+#define SB_DSP_MAJOR_SHIFT 8
+
+// BLASTER values that were not given
+#define SB_NONE 0xFFFFFFFFU
+
+// Buffer boundaries a DMA transfer must not cross
+#define SB_DMA_BOUNDARY_8 0x10000U
+#define SB_DMA_BOUNDARY_16 0x20000U
+
+// Playback methods
+typedef enum {
+ SB_MODE_SINGLE = 0, // DSP 1.x: 8-bit single-cycle, restarted every block
+ SB_MODE_AUTO = 1, // 8-bit auto-init
+ SB_MODE_HIGH = 2, // 8-bit auto-init high-speed (stopped by a DSP reset)
+ SB_MODE_SB16 = 3 // SB16 8- or 16-bit auto-init at any rate
+} SbModeE;
+
+// The BLASTER environment variable ("A220 I5 D1 H5 T6"); SB_NONE for a
+// value not given.
+typedef struct {
+ uint32_t base; // A: I/O base
+ uint32_t irq; // I
+ uint32_t dma8; // D
+ uint32_t dma16; // H
+ uint32_t type; // T: card type
+} SbBlasterT;
+
+// How a card plays what the mixer wants.
+typedef struct {
+ SbModeE mode;
+ uint32_t rate; // achieved rate per channel (Hz)
+ uint32_t bits; // 8 or 16
+ uint32_t channels; // 1 or 2
+ uint32_t timeConstant; // before the SB16: the DSP's sample clock setting
+} SbPlanT;
+
+// Name of the card family for a DSP version.
+const char *sbModelName(uint32_t dspVersion);
+
+// Offset in a block at physical address phys, allocBytes long, of a
+// bufBytes buffer that does not cross a boundary; SB_NONE when none fits.
+uint32_t sbDmaPlace(uint32_t phys, uint32_t allocBytes, uint32_t bufBytes, uint32_t boundary);
+
+// Parse a BLASTER value (NULL allowed). False when it names no I/O base.
+bool sbParseBlaster(const char *text, SbBlasterT *out);
+
+// The plan for wanted rate, bits and channels (0: the best) on a card
+// with this DSP version; have16Dma says whether a 16-bit channel is known.
+void sbPlan(uint32_t dspVersion, uint32_t rate, uint32_t bits, uint32_t channels, bool have16Dma, SbPlanT *plan);
+
+#endif // SB_CALC_H
diff --git a/src/libs/kpunch/audio/drivers/sb/sbDrv.c b/src/libs/kpunch/audio/drivers/sb/sbDrv.c
new file mode 100644
index 00000000..fe07c1e5
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/sb/sbDrv.c
@@ -0,0 +1,773 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// sbDrv.c -- the Sound Blaster sound driver (sb.drv)
+//
+// One driver for the whole family, chosen by DSP version (sbCalc.c
+// plans the mode): SB 1.x single-cycle 8-bit; SB 1.5/2.0 8-bit auto-init,
+// high-speed above 23,000 Hz; SB Pro / Pro 2 the same plus stereo; SB16
+// 8- or 16-bit auto-init, mono or stereo, at the exact rate.
+//
+// Where the card is: the BLASTER variable (A, I, D, H); without it the
+// usual bases 220h and 240h are probed, which only finds an SB16 (its
+// mixer reports the IRQ and DMA channels; older cards need BLASTER). On
+// an SB16 the mixer's IRQ and DMA settings (80h, 81h) win over BLASTER.
+//
+// Output is double-buffered: one auto-init DMA transfer loops over a
+// buffer of two halves in conventional memory (below 1 MB, so below the
+// 16 MB ISA limit; placed so it crosses no 64K / 128K boundary), and the
+// card interrupts at the end of each half. The interrupt handler finds
+// the half the DMA controller is now reading and mixes the other one:
+// audio.lib's mixer runs inside the interrupt, with interrupts enabled
+// again so the timer, keyboard and serial ports are not held up, but with
+// the card's interrupt not yet acknowledged at the PIC, so it cannot
+// re-enter. Mixing in the interrupt keeps sound playing however busy the
+// programs are; the mixer is integer-only and pinned, like everything
+// here the interrupt touches (sSb, the handler, audioMixRender).
+//
+// SB Pro stereo needs its mixer's stereo bit, the output filter off, and
+// one silent byte played first (else left and right come out swapped).
+// Stopping restores the mixer, the PIC mask and the interrupt vector and
+// frees the buffer; audio.lib's shutdown hook does it if DVX dies.
+
+#include "audioDrv.h"
+#include "sbCalc.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+// DSP ports (offsets from the base)
+#define DSP_MIXER_ADDR 0x04
+#define DSP_MIXER_DATA 0x05
+#define DSP_RESET 0x06
+#define DSP_READ 0x0A
+#define DSP_WRITE 0x0C
+#define DSP_READ_STATUS 0x0E // bit 7: data to read; reading acknowledges an 8-bit interrupt
+#define DSP_ACK16 0x0F // reading acknowledges a 16-bit interrupt
+#define DSP_BUSY 0x80
+#define DSP_READY 0xAA // what a DSP says after a reset
+
+// DSP commands
+#define DSP_CMD_SINGLE8 0x14
+#define DSP_CMD_AUTO8 0x1C
+#define DSP_CMD_TIME_CONST 0x40
+#define DSP_CMD_OUT_RATE 0x41
+#define DSP_CMD_BLOCK_SIZE 0x48
+#define DSP_CMD_HIGH_AUTO8 0x90
+#define DSP_CMD_SB16_16 0xB6 // 16-bit output, auto-init, FIFO on
+#define DSP_CMD_SB16_8 0xC6 // 8-bit output, auto-init, FIFO on
+#define DSP_CMD_PAUSE8 0xD0
+#define DSP_CMD_SPEAKER_ON 0xD1
+#define DSP_CMD_SPEAKER_OFF 0xD3
+#define DSP_CMD_PAUSE16 0xD5
+#define DSP_CMD_EXIT16 0xD9
+#define DSP_CMD_EXIT8 0xDA
+#define DSP_CMD_VERSION 0xE1
+#define SB16_MODE_SIGNED 0x10
+#define SB16_MODE_STEREO 0x20
+
+// Mixer registers
+#define MIX_REG_VOICE_PRO 0x04 // SB Pro voice volume (left and right nibbles)
+#define MIX_REG_OUTPUT 0x0E // SB Pro: bit 1 stereo, bit 5 output filter off
+#define MIX_OUT_STEREO 0x02
+#define MIX_OUT_NO_FILTER 0x20
+#define MIX_REG_VOICE16_L 0x32 // SB16 voice volume, 5 bits in the top of the byte
+#define MIX_REG_VOICE16_R 0x33
+#define MIX_REG_IRQ 0x80
+#define MIX_REG_DMA 0x81
+#define MIX_REG_IRQ_STATUS 0x82
+#define MIX_IRQ_STATUS_8 0x01
+#define MIX_IRQ_STATUS_16 0x02
+#define MIX_VOICE_PRO_MAX 0xFF
+#define MIX_VOICE16_MAX 0xF8
+
+// SB16 mixer 80h bits for IRQ 2, 5, 7, 10 and 81h bits for DMA 0..7
+#define SB16_IRQ_COUNT 4
+#define SB16_DMA_16_FIRST 5
+#define DMA_CHANNELS 8
+
+// 8259 interrupt controllers
+#define PIC1_CMD 0x20
+#define PIC1_DATA 0x21
+#define PIC2_CMD 0xA0
+#define PIC2_DATA 0xA1
+#define PIC_EOI 0x20
+#define PIC_READ_ISR 0x0B
+#define IRQ_SLAVE_FIRST 8
+#define IRQ_COUNT 16
+#define IRQ_CASCADE 2
+#define IRQ_AT_REDIRECT 9 // IRQ 2 arrives as 9 on an AT
+#define IRQ_SPURIOUS_MASTER 7
+#define VEC_MASTER_BASE 0x08
+#define VEC_SLAVE_BASE 0x70
+
+// 8237 DMA controllers: channels 0-3 byte-wide, 5-7 word-wide
+#define DMA8_MASK 0x0A
+#define DMA8_MODE 0x0B
+#define DMA8_FLIPFLOP 0x0C
+#define DMA16_MASK 0xD4
+#define DMA16_MODE 0xD6
+#define DMA16_FLIPFLOP 0xD8
+#define DMA_MASK_SET 0x04
+#define DMA_CHANNEL_BITS 0x03
+#define DMA_MODE_AUTO_PLAY 0x58 // single transfer, auto-init, memory to device
+#define DMA_MODE_ONCE_PLAY 0x48 // single transfer, memory to device
+#define DMA_FIRST_16 4
+#define DMA_PAGE_SHIFT 16
+#define DMA_PAGE_MASK16 0xFE
+
+// Buffer: two halves of SB_BLOCK_FRAMES frames (about 23 ms each at
+// 22,050 Hz), allocated twice over so a half-size stretch without a
+// boundary always exists
+#define SB_BLOCK_FRAMES 512
+#define SB_FRAME_BYTES_MAX 4
+#define SB_HALVES 2
+#define PARAGRAPH_BYTES 16
+#define PARAGRAPH_SHIFT 4
+#define WORD_BYTES 4
+#define SILENCE_8 0x80
+#define SILENCE_16 0x00
+
+// Waits (port reads take about a microsecond on the ISA bus)
+#define DSP_POLL_MAX 65536
+#define DSP_RESET_DELAY 8
+#define DSP_RESET_POLL 1000
+#define PRIME_WAIT_US 200000
+#define FIRST_IRQ_WAIT_US 500000 // many blocks: no interrupt by then means a wrong IRQ
+
+// Probed without BLASTER
+static const uint32_t sProbeBases[] = { 0x220, 0x240 };
+
+// Channel registers, by DMA channel number
+static const uint8_t sDmaAddrPort[DMA_CHANNELS] = { 0x00, 0x02, 0x04, 0x06, 0xC0, 0xC4, 0xC8, 0xCC };
+static const uint8_t sDmaCountPort[DMA_CHANNELS] = { 0x01, 0x03, 0x05, 0x07, 0xC2, 0xC6, 0xCA, 0xCE };
+static const uint8_t sDmaPagePort[DMA_CHANNELS] = { 0x87, 0x83, 0x81, 0x82, 0x8F, 0x8B, 0x89, 0x8A };
+
+// SB16 mixer 80h bit n selects this IRQ
+static const uint8_t sSb16Irqs[SB16_IRQ_COUNT] = { 2, 5, 7, 10 };
+
+#define PROBE_COUNT ((int32_t)(sizeof(sProbeBases) / sizeof(sProbeBases[0])))
+#define BITS_16 16
+#define BYTE_MASK 0xFF
+#define BYTE_BITS 8
+#define DSP_MINOR_MASK 0xFF
+
+typedef struct {
+ uint32_t scratch[SB_BLOCK_FRAMES * SB_FRAME_BYTES_MAX / WORD_BYTES]; // one half, mixed here first
+ _go32_dpmi_seginfo oldVector;
+ _go32_dpmi_seginfo newVector;
+ SbPlanT plan;
+ uint32_t base;
+ uint32_t irq;
+ uint32_t dma; // the channel in use
+ uint32_t dspVersion;
+ uint32_t halfBytes;
+ uint32_t halfFrames;
+ uint32_t bufOffset; // of the buffer in the DOS block
+ uint32_t bufPhys;
+ int32_t dosSelector; // the DOS block (-1: none)
+ uint8_t savedOutput; // SB Pro mixer 0Eh
+ uint8_t savedVoiceL; // voice volume registers
+ uint8_t savedVoiceR;
+ uint8_t savedPicBit; // the IRQ was masked before
+ volatile uint32_t irqCount; // block interrupts while playing
+ volatile bool priming; // the SB Pro silent byte is playing
+ volatile bool primed; // ... and has ended
+ bool locked;
+ bool hooked;
+ bool playing;
+} SbStateT;
+
+static SbStateT sSb = { .dosSelector = -1 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static uint32_t dmaHalfPlaying(void);
+static void dmaProgram(uint32_t offset, uint32_t bytes, uint8_t mode);
+static void dmaStop(void);
+static int32_t dspRead(uint32_t base);
+static bool dspReset(uint32_t base);
+static bool dspWrite(uint32_t base, uint8_t value);
+static void irqHook(void);
+static uint32_t irqPicPort(uint32_t irq);
+static void irqUnhook(void);
+static int irqVector(uint32_t irq);
+static uint8_t mixerRead(uint8_t reg);
+static void mixerWrite(uint8_t reg, uint8_t value);
+static void sbClose(void);
+static bool sbFindCard(SbBlasterT *bl);
+static void sbIsr(void);
+static bool sbLock(bool lock);
+static bool sbOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
+static bool sbPlay(void);
+static void sbPrimeStereo(void);
+static void sbRefill(uint32_t half);
+static void sbStartDsp(void);
+
+AUDIO_DRIVER("sb", AUDIO_PRIORITY_CARD,
+ .open = sbOpen,
+ .play = sbPlay,
+ .close = sbClose
+);
+
+
+// The half the DMA controller is reading now, from its remaining count
+// (bytes on channels 0-3, words on 5-7).
+static uint32_t dmaHalfPlaying(void) {
+ uint32_t ch = sSb.dma;
+ bool wide = ch >= DMA_FIRST_16;
+ uint32_t units = wide ? sSb.halfBytes / 2 : sSb.halfBytes;
+ uint32_t count;
+
+ outportb(wide ? DMA16_FLIPFLOP : DMA8_FLIPFLOP, 0);
+ count = inportb(sDmaCountPort[ch]);
+ count |= (uint32_t)inportb(sDmaCountPort[ch]) << BYTE_BITS;
+
+ // count is the units left minus one in the whole buffer
+ uint32_t done = units * SB_HALVES - 1 - count;
+
+ return done < units ? 0 : 1;
+}
+
+
+// Program the channel for bytes of the buffer from offset, in mode.
+static void dmaProgram(uint32_t offset, uint32_t bytes, uint8_t mode) {
+ uint32_t ch = sSb.dma;
+ bool wide = ch >= DMA_FIRST_16;
+ uint32_t sel = ch & DMA_CHANNEL_BITS;
+ uint32_t phys = sSb.bufPhys + offset;
+ uint32_t addr = wide ? (phys >> 1) : phys;
+ uint32_t count = (wide ? bytes / 2 : bytes) - 1;
+ uint32_t page = phys >> DMA_PAGE_SHIFT;
+
+ outportb(wide ? DMA16_MASK : DMA8_MASK, (uint8_t)(DMA_MASK_SET | sel));
+ outportb(wide ? DMA16_FLIPFLOP : DMA8_FLIPFLOP, 0);
+ outportb(wide ? DMA16_MODE : DMA8_MODE, (uint8_t)(mode | sel));
+ outportb(sDmaAddrPort[ch], addr & BYTE_MASK);
+ outportb(sDmaAddrPort[ch], (addr >> BYTE_BITS) & BYTE_MASK);
+ outportb(sDmaPagePort[ch], (uint8_t)(wide ? (page & DMA_PAGE_MASK16) : page));
+ outportb(sDmaCountPort[ch], count & BYTE_MASK);
+ outportb(sDmaCountPort[ch], (count >> BYTE_BITS) & BYTE_MASK);
+ outportb(wide ? DMA16_MASK : DMA8_MASK, (uint8_t)sel);
+}
+
+
+static void dmaStop(void) {
+ bool wide = sSb.dma >= DMA_FIRST_16;
+
+ outportb(wide ? DMA16_MASK : DMA8_MASK, (uint8_t)(DMA_MASK_SET | (sSb.dma & DMA_CHANNEL_BITS)));
+}
+
+
+static int32_t dspRead(uint32_t base) {
+ for (int32_t i = 0; i < DSP_POLL_MAX; i++) {
+ if (inportb(base + DSP_READ_STATUS) & DSP_BUSY) {
+ return inportb(base + DSP_READ);
+ }
+ }
+
+ return -1;
+}
+
+
+// Write 1, wait at least 3 us, write 0, then the DSP answers AAh within
+// about 100 us.
+static bool dspReset(uint32_t base) {
+ outportb(base + DSP_RESET, 1);
+
+ for (int32_t i = 0; i < DSP_RESET_DELAY; i++) {
+ inportb(base + DSP_RESET);
+ }
+
+ outportb(base + DSP_RESET, 0);
+
+ for (int32_t i = 0; i < DSP_RESET_POLL; i++) {
+ if ((inportb(base + DSP_READ_STATUS) & DSP_BUSY) && inportb(base + DSP_READ) == DSP_READY) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+static bool dspWrite(uint32_t base, uint8_t value) {
+ for (int32_t i = 0; i < DSP_POLL_MAX; i++) {
+ if (!(inportb(base + DSP_WRITE) & DSP_BUSY)) {
+ outportb(base + DSP_WRITE, value);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+static void irqHook(void) {
+ uint32_t irq = sSb.irq;
+ int vector = irqVector(irq);
+ uint32_t port = irqPicPort(irq);
+ uint8_t bit = (uint8_t)(1 << (irq & (IRQ_SLAVE_FIRST - 1)));
+
+ _go32_dpmi_get_protected_mode_interrupt_vector(vector, &sSb.oldVector);
+ sSb.newVector.pm_offset = (unsigned long)sbIsr;
+ sSb.newVector.pm_selector = _go32_my_cs();
+
+ if (_go32_dpmi_allocate_iret_wrapper(&sSb.newVector) != 0) {
+ return;
+ }
+
+ uint32_t state = platformIrqSave();
+
+ _go32_dpmi_set_protected_mode_interrupt_vector(vector, &sSb.newVector);
+ sSb.savedPicBit = inportb(port) & bit;
+ outportb(port, inportb(port) & ~bit);
+
+ if (irq >= IRQ_SLAVE_FIRST) {
+ outportb(PIC1_DATA, inportb(PIC1_DATA) & ~(1 << IRQ_CASCADE));
+ }
+
+ sSb.hooked = true;
+ platformIrqRestore(state);
+}
+
+
+// The PIC's mask register for the IRQ.
+static uint32_t irqPicPort(uint32_t irq) {
+ return irq < IRQ_SLAVE_FIRST ? PIC1_DATA : PIC2_DATA;
+}
+
+
+static void irqUnhook(void) {
+ if (!sSb.hooked) {
+ return;
+ }
+
+ uint32_t port = irqPicPort(sSb.irq);
+ uint32_t state = platformIrqSave();
+
+ outportb(port, inportb(port) | sSb.savedPicBit);
+ _go32_dpmi_set_protected_mode_interrupt_vector(irqVector(sSb.irq), &sSb.oldVector);
+ platformIrqRestore(state);
+ _go32_dpmi_free_iret_wrapper(&sSb.newVector);
+ sSb.hooked = false;
+}
+
+
+// The interrupt vector an IRQ arrives on.
+static int irqVector(uint32_t irq) {
+ return irq < IRQ_SLAVE_FIRST ? VEC_MASTER_BASE + (int)irq : VEC_SLAVE_BASE + (int)(irq - IRQ_SLAVE_FIRST);
+}
+
+
+static uint8_t mixerRead(uint8_t reg) {
+ outportb(sSb.base + DSP_MIXER_ADDR, reg);
+ return inportb(sSb.base + DSP_MIXER_DATA);
+}
+
+
+static void mixerWrite(uint8_t reg, uint8_t value) {
+ outportb(sSb.base + DSP_MIXER_ADDR, reg);
+ outportb(sSb.base + DSP_MIXER_DATA, value);
+}
+
+
+// Also runs from audio.lib's shutdown hook, so it logs nothing and copes
+// with any stage of sbOpen / sbPlay.
+static void sbClose(void) {
+ bool sb16 = sSb.dspVersion >= SB_DSP_SB16;
+
+ if (sSb.playing) {
+ if (sb16) {
+ bool wide = sSb.plan.bits == BITS_16;
+
+ dspWrite(sSb.base, wide ? DSP_CMD_PAUSE16 : DSP_CMD_PAUSE8);
+ dspWrite(sSb.base, wide ? DSP_CMD_EXIT16 : DSP_CMD_EXIT8);
+ } else if (sSb.plan.mode == SB_MODE_AUTO) {
+ dspWrite(sSb.base, DSP_CMD_PAUSE8);
+ dspWrite(sSb.base, DSP_CMD_EXIT8);
+ }
+
+ // A reset stops every mode, high-speed and single-cycle included.
+ dspReset(sSb.base);
+ dspWrite(sSb.base, DSP_CMD_SPEAKER_OFF);
+ dmaStop();
+
+ if (sb16) {
+ mixerWrite(MIX_REG_VOICE16_L, sSb.savedVoiceL);
+ mixerWrite(MIX_REG_VOICE16_R, sSb.savedVoiceR);
+ } else if (sSb.dspVersion >= SB_DSP_PRO) {
+ mixerWrite(MIX_REG_VOICE_PRO, sSb.savedVoiceL);
+ mixerWrite(MIX_REG_OUTPUT, sSb.savedOutput);
+ }
+
+ sSb.playing = false;
+ }
+
+ irqUnhook();
+
+ if (sSb.locked) {
+ sbLock(false);
+ sSb.locked = false;
+ }
+
+ if (sSb.dosSelector >= 0) {
+ __dpmi_free_dos_memory(sSb.dosSelector);
+ sSb.dosSelector = -1;
+ }
+}
+
+
+// BLASTER, else probe for an SB16 (whose mixer knows its IRQ and DMA).
+static bool sbFindCard(SbBlasterT *bl) {
+ if (sbParseBlaster(getenv("BLASTER"), bl)) {
+ return dspReset(bl->base);
+ }
+
+ for (int32_t i = 0; i < PROBE_COUNT; i++) {
+ if (dspReset(sProbeBases[i])) {
+ bl->base = sProbeBases[i];
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// The card's interrupt. Acknowledge it at the DSP (an SB16 says in mixer
+// 82h which of its sources it was), then mix the half that just finished
+// with interrupts enabled, and only then acknowledge it at the PIC, so it
+// cannot come back in while mixing. A spurious IRQ 7 (the PIC's in-service
+// bit clear) gets no acknowledgement at all.
+static void sbIsr(void) {
+ uint32_t base = sSb.base;
+ bool mine = true;
+
+ if (sSb.irq == IRQ_SPURIOUS_MASTER) {
+ outportb(PIC1_CMD, PIC_READ_ISR);
+
+ if (!(inportb(PIC1_CMD) & (1 << IRQ_SPURIOUS_MASTER))) {
+ return;
+ }
+ }
+
+ if (sSb.dspVersion >= SB_DSP_SB16) {
+ outportb(base + DSP_MIXER_ADDR, MIX_REG_IRQ_STATUS);
+
+ uint8_t status = inportb(base + DSP_MIXER_DATA);
+
+ if (status & MIX_IRQ_STATUS_16) {
+ inportb(base + DSP_ACK16);
+ }
+
+ if (status & MIX_IRQ_STATUS_8) {
+ inportb(base + DSP_READ_STATUS);
+ }
+
+ mine = (status & (MIX_IRQ_STATUS_8 | MIX_IRQ_STATUS_16)) != 0;
+ } else {
+ inportb(base + DSP_READ_STATUS);
+ }
+
+ if (mine && sSb.priming) {
+ sSb.primed = true;
+ } else if (mine && sSb.playing) {
+ sSb.irqCount++;
+
+ if (sSb.plan.mode == SB_MODE_SINGLE) {
+ uint32_t len = sSb.halfBytes - 1;
+
+ dspWrite(base, DSP_CMD_SINGLE8);
+ dspWrite(base, (uint8_t)(len & BYTE_MASK));
+ dspWrite(base, (uint8_t)((len >> BYTE_BITS) & BYTE_MASK));
+ }
+
+ __asm__ __volatile__ ("sti" : : : "memory");
+ sbRefill(1 - dmaHalfPlaying());
+ __asm__ __volatile__ ("cli" : : : "memory");
+ }
+
+ if (sSb.irq >= IRQ_SLAVE_FIRST) {
+ outportb(PIC2_CMD, PIC_EOI);
+ }
+
+ outportb(PIC1_CMD, PIC_EOI);
+}
+
+
+static bool sbLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)dmaHalfPlaying, 0 },
+ { (const void *)dspWrite, 0 },
+ { (const void *)sbIsr, 0 },
+ { (const void *)sbRefill, 0 },
+ { sDmaCountPort, sizeof(sDmaCountPort) },
+ { &sSb, sizeof(sSb) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+static bool sbOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
+ SbBlasterT bl;
+
+ memset(&sSb, 0, sizeof(sSb));
+ sSb.dosSelector = -1;
+
+ if (!sbFindCard(&bl)) {
+ dvxLog("Audio: sb: no Sound Blaster found (BLASTER %s)", getenv("BLASTER") ? "set" : "not set");
+ return false;
+ }
+
+ sSb.base = bl.base;
+
+ int32_t major = dspWrite(sSb.base, DSP_CMD_VERSION) ? dspRead(sSb.base) : -1;
+ int32_t minor = major >= 0 ? dspRead(sSb.base) : -1;
+
+ if (minor < 0) {
+ dvxLog("Audio: sb: the DSP at %lXh gave no version", (unsigned long)sSb.base);
+ return false;
+ }
+
+ sSb.dspVersion = ((uint32_t)major << SB_DSP_MAJOR_SHIFT) | ((uint32_t)minor & DSP_MINOR_MASK);
+
+ // An SB16 reports the IRQ and DMA channels it is set to.
+ if (sSb.dspVersion >= SB_DSP_SB16) {
+ uint8_t irqBits = mixerRead(MIX_REG_IRQ);
+ uint8_t dmaBits = mixerRead(MIX_REG_DMA);
+
+ for (int32_t i = 0; i < SB16_IRQ_COUNT; i++) {
+ if (irqBits & (1 << i)) {
+ bl.irq = sSb16Irqs[i];
+ }
+ }
+
+ for (uint32_t ch = 0; ch < DMA_CHANNELS; ch++) {
+ if (dmaBits & (1 << ch)) {
+ if (ch >= SB16_DMA_16_FIRST) {
+ bl.dma16 = ch;
+ } else {
+ bl.dma8 = ch;
+ }
+ }
+ }
+ }
+
+ sSb.irq = bl.irq == IRQ_CASCADE ? IRQ_AT_REDIRECT : bl.irq;
+
+ bool have16 = sSb.dspVersion >= SB_DSP_SB16 && bl.dma16 != SB_NONE && bl.dma16 >= SB16_DMA_16_FIRST && bl.dma16 < DMA_CHANNELS;
+
+ sbPlan(sSb.dspVersion, cfg->rate, cfg->bits, cfg->channels, have16, &sSb.plan);
+ sSb.dma = sSb.plan.bits == BITS_16 ? bl.dma16 : bl.dma8;
+
+ bool dmaOk = sSb.plan.bits == BITS_16 ? (sSb.dma >= SB16_DMA_16_FIRST && sSb.dma < DMA_CHANNELS) : sSb.dma < DMA_FIRST_16;
+
+ if (sSb.irq >= IRQ_COUNT || !dmaOk) {
+ dvxLog("Audio: sb: IRQ or DMA channel unknown or unusable (set BLASTER, e.g. A220 I5 D1 H5)");
+ return false;
+ }
+
+ uint32_t frameBytes = sSb.plan.bits / BYTE_BITS * sSb.plan.channels;
+ uint32_t bufBytes = SB_BLOCK_FRAMES * frameBytes * SB_HALVES;
+ uint32_t allocBytes = bufBytes * 2;
+ int selector = 0;
+ int segment = __dpmi_allocate_dos_memory((int)(allocBytes / PARAGRAPH_BYTES), &selector);
+
+ if (segment < 0) {
+ dvxLog("Audio: sb: no conventional memory for the DMA buffer");
+ return false;
+ }
+
+ uint32_t blockPhys = (uint32_t)segment << PARAGRAPH_SHIFT;
+ uint32_t offset = sbDmaPlace(blockPhys, allocBytes, bufBytes, sSb.plan.bits == BITS_16 ? SB_DMA_BOUNDARY_16 : SB_DMA_BOUNDARY_8);
+
+ sSb.dosSelector = selector;
+
+ if (offset == SB_NONE) {
+ dvxLog("Audio: sb: the DMA buffer would cross a DMA boundary");
+ sbClose();
+ return false;
+ }
+
+ sSb.bufOffset = offset;
+ sSb.bufPhys = blockPhys + offset;
+ sSb.halfFrames = SB_BLOCK_FRAMES;
+ sSb.halfBytes = SB_BLOCK_FRAMES * frameBytes;
+
+ snprintf(info->description, sizeof(info->description), "%s (DSP %lu.%02lu) at %lXh", sbModelName(sSb.dspVersion), (unsigned long)major, (unsigned long)minor, (unsigned long)sSb.base);
+ info->rate = sSb.plan.rate;
+ info->bits = sSb.plan.bits;
+ info->channels = sSb.plan.channels;
+ info->hasDac = true;
+ dvxLog("Audio: sb: DSP %d.%02d at %lXh, IRQ %lu, DMA %lu, mode %d", (int)major, (int)minor, (unsigned long)sSb.base, (unsigned long)sSb.irq, (unsigned long)sSb.dma, (int)sSb.plan.mode);
+ return true;
+}
+
+
+static bool sbPlay(void) {
+ uint8_t silence = sSb.plan.bits == BITS_16 ? SILENCE_16 : SILENCE_8;
+
+ sSb.locked = sbLock(true);
+
+ if (!sSb.locked) {
+ return false;
+ }
+
+ _farsetsel((unsigned short)sSb.dosSelector);
+
+ for (uint32_t i = 0; i < sSb.halfBytes * SB_HALVES; i++) {
+ _farnspokeb(sSb.bufOffset + i, silence);
+ }
+
+ irqHook();
+
+ if (!sSb.hooked) {
+ return false;
+ }
+
+ dspWrite(sSb.base, DSP_CMD_SPEAKER_ON);
+
+ // The voice volume at full: DVX's own volumes do the rest.
+ if (sSb.dspVersion >= SB_DSP_SB16) {
+ sSb.savedVoiceL = mixerRead(MIX_REG_VOICE16_L);
+ sSb.savedVoiceR = mixerRead(MIX_REG_VOICE16_R);
+ mixerWrite(MIX_REG_VOICE16_L, MIX_VOICE16_MAX);
+ mixerWrite(MIX_REG_VOICE16_R, MIX_VOICE16_MAX);
+ } else if (sSb.dspVersion >= SB_DSP_PRO) {
+ sSb.savedVoiceL = mixerRead(MIX_REG_VOICE_PRO);
+ sSb.savedOutput = mixerRead(MIX_REG_OUTPUT);
+ mixerWrite(MIX_REG_VOICE_PRO, MIX_VOICE_PRO_MAX);
+
+ if (sSb.plan.channels > 1) {
+ mixerWrite(MIX_REG_OUTPUT, (uint8_t)(sSb.savedOutput | MIX_OUT_STEREO | MIX_OUT_NO_FILTER));
+ sbPrimeStereo();
+ }
+ }
+
+ sSb.playing = true;
+ sbRefill(0);
+ sbRefill(1);
+ dmaProgram(0, sSb.halfBytes * SB_HALVES, DMA_MODE_AUTO_PLAY);
+ sbStartDsp();
+
+ // A card set to another IRQ than BLASTER says plays one buffer and
+ // stops; give up on it so the next driver is tried.
+ uint64_t until = platformMicros() + FIRST_IRQ_WAIT_US;
+
+ while (sSb.irqCount == 0 && platformMicros() < until) {
+ platformYield();
+ }
+
+ if (sSb.irqCount == 0) {
+ dvxLog("Audio: sb: no interrupt from the card on IRQ %lu (check BLASTER)", (unsigned long)sSb.irq);
+ return false;
+ }
+
+ return true;
+}
+
+
+// SB Pro stereo: play one silent byte and wait for its interrupt, so the
+// real transfer starts on the left channel.
+static void sbPrimeStereo(void) {
+ _farsetsel((unsigned short)sSb.dosSelector);
+ _farnspokeb(sSb.bufOffset, SILENCE_8);
+ dmaProgram(0, 1, DMA_MODE_ONCE_PLAY);
+ sSb.primed = false;
+ sSb.priming = true;
+ dspWrite(sSb.base, DSP_CMD_TIME_CONST);
+ dspWrite(sSb.base, (uint8_t)sSb.plan.timeConstant);
+ dspWrite(sSb.base, DSP_CMD_SINGLE8);
+ dspWrite(sSb.base, 0);
+ dspWrite(sSb.base, 0);
+
+ uint64_t until = platformMicros() + PRIME_WAIT_US;
+
+ while (!sSb.primed && platformMicros() < until) {
+ platformYield();
+ }
+
+ sSb.priming = false;
+ dmaStop();
+}
+
+
+// Mix one half into the scratch buffer and copy it to the DMA buffer.
+// Interrupt time.
+static void sbRefill(uint32_t half) {
+ uint32_t offset = sSb.bufOffset + half * sSb.halfBytes;
+
+ audioMixRender(sSb.scratch, sSb.halfFrames);
+ _farsetsel((unsigned short)sSb.dosSelector);
+
+ for (uint32_t i = 0; i < sSb.halfBytes / WORD_BYTES; i++) {
+ _farnspokel(offset + i * WORD_BYTES, sSb.scratch[i]);
+ }
+}
+
+
+// Start the transfer the plan calls for; the DSP interrupts after each
+// half.
+static void sbStartDsp(void) {
+ uint32_t base = sSb.base;
+
+ if (sSb.plan.mode == SB_MODE_SB16) {
+ bool wide = sSb.plan.bits == BITS_16;
+ uint32_t len = sSb.halfFrames * sSb.plan.channels - 1;
+ uint8_t mode = (uint8_t)((sSb.plan.channels > 1 ? SB16_MODE_STEREO : 0) | (wide ? SB16_MODE_SIGNED : 0));
+
+ dspWrite(base, DSP_CMD_OUT_RATE);
+ dspWrite(base, (uint8_t)((sSb.plan.rate >> BYTE_BITS) & BYTE_MASK));
+ dspWrite(base, (uint8_t)(sSb.plan.rate & BYTE_MASK));
+ dspWrite(base, wide ? DSP_CMD_SB16_16 : DSP_CMD_SB16_8);
+ dspWrite(base, mode);
+ dspWrite(base, (uint8_t)(len & BYTE_MASK));
+ dspWrite(base, (uint8_t)((len >> BYTE_BITS) & BYTE_MASK));
+ return;
+ }
+
+ uint32_t len = sSb.halfBytes - 1;
+
+ dspWrite(base, DSP_CMD_TIME_CONST);
+ dspWrite(base, (uint8_t)sSb.plan.timeConstant);
+
+ if (sSb.plan.mode == SB_MODE_SINGLE) {
+ dspWrite(base, DSP_CMD_SINGLE8);
+ dspWrite(base, (uint8_t)(len & BYTE_MASK));
+ dspWrite(base, (uint8_t)((len >> BYTE_BITS) & BYTE_MASK));
+ return;
+ }
+
+ dspWrite(base, DSP_CMD_BLOCK_SIZE);
+ dspWrite(base, (uint8_t)(len & BYTE_MASK));
+ dspWrite(base, (uint8_t)((len >> BYTE_BITS) & BYTE_MASK));
+ dspWrite(base, sSb.plan.mode == SB_MODE_HIGH ? DSP_CMD_HIGH_AUTO8 : DSP_CMD_AUTO8);
+}
diff --git a/src/libs/kpunch/audio/drivers/speaker/spkDrv.c b/src/libs/kpunch/audio/drivers/speaker/spkDrv.c
new file mode 100644
index 00000000..93cdc52f
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/speaker/spkDrv.c
@@ -0,0 +1,202 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// spkDrv.c -- the PC speaker sound driver (speaker.drv)
+//
+// Two ways to play, chosen by DVX.INI [audio] speakerPcm:
+//
+// false (default) Tones only. PIT channel 2 runs as a square-wave
+// generator (mode 3) at the frequency of the loudest
+// square or triangle tone playing: no interrupts, no
+// CPU cost. The mixer still runs from the timer
+// service at a low rate so every voice keeps time and
+// ends; sampled sound and noise are not heard.
+// true Digital sound by pulse-width modulation: the timer
+// service interrupts at PLATFORM_PIT_HZ / SPK_PWM_DIVISOR
+// (22,096 Hz) and each interrupt loads the next sample,
+// scaled to SPK_PWM_LEVELS pulse widths (about 5.7 bits),
+// into channel 2 as a one-shot (mode 0). It costs a lot
+// of CPU: 22,096 interrupts a second, roughly 10-15 % of
+// a 486DX2-66 with a few voices, much more on slower
+// machines or under a memory manager that reflects
+// interrupts (EMM386, Windows).
+//
+// Everything is one speaker, so several programs' sounds are mixed into
+// that one output (sampled sound only with speakerPcm).
+
+#include "audioDrv.h"
+
+#include
+#include
+
+#define SPK_INI_PCM "speakerPcm"
+
+// PIT channel 2 and the speaker gate (port 61h bit 0 gates channel 2, bit
+// 1 connects it to the speaker)
+#define PIT_CH2_PORT 0x42
+#define PIT_CMD_PORT 0x43
+#define PIT_CH2_SQUARE 0xB6 // channel 2, low then high byte, mode 3, binary
+#define PIT_CH2_ONESHOT 0x90 // channel 2, low byte only, mode 0, binary
+#define SPK_CTRL_PORT 0x61
+#define SPK_CTRL_ON 0x03
+#define SPK_BEEP_DIVISOR 0x0533 // the BIOS beep (896 Hz), left in channel 2 afterwards
+#define BYTE_MASK 0xFF
+#define BYTE_BITS 8
+
+// Digital sound: PIT clocks per sample, pulse widths 1..divisor-2, and
+// frames mixed per mixer call
+#define SPK_PWM_DIVISOR 54
+#define SPK_PWM_LEVELS (SPK_PWM_DIVISOR - 2)
+#define SPK_PWM_BLOCK 4
+#define SPK_TIMER_NONE (-1)
+
+// Mixing rate when only tones are played (the output is thrown away)
+#define SPK_TONE_RATE 11025
+#define SPK_BITS 8
+#define SPK_CHANNELS 1
+
+typedef struct {
+ uint8_t buf[SPK_PWM_BLOCK]; // mixed samples waiting to be played
+ uint32_t pos; // next sample of buf
+ int32_t timerId;
+ bool pcm; // digital sound (else tones only)
+ bool locked;
+} SpkStateT;
+
+static SpkStateT sSpk = { .timerId = SPK_TIMER_NONE };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void spkClose(void);
+static bool spkLock(bool lock);
+static bool spkOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
+static bool spkPlay(void);
+static void spkTick(void *unused);
+static void spkTone(uint32_t hz);
+
+AUDIO_DRIVER("speaker", AUDIO_PRIORITY_SPEAKER,
+ .open = spkOpen,
+ .play = spkPlay,
+ .close = spkClose
+);
+
+
+// Silence, and channel 2 back in square-wave mode at the BIOS beep.
+static void spkClose(void) {
+ if (sSpk.pcm) {
+ platformTimerRemove(sSpk.timerId);
+ } else {
+ audioClockStop();
+ }
+
+ sSpk.timerId = SPK_TIMER_NONE;
+ spkTone(0);
+ outportb(PIT_CMD_PORT, PIT_CH2_SQUARE);
+ outportb(PIT_CH2_PORT, SPK_BEEP_DIVISOR & BYTE_MASK);
+ outportb(PIT_CH2_PORT, (SPK_BEEP_DIVISOR >> BYTE_BITS) & BYTE_MASK);
+
+ if (sSpk.locked) {
+ spkLock(false);
+ sSpk.locked = false;
+ }
+}
+
+
+static bool spkLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)spkTick, 0 },
+ { (const void *)spkTone, 0 },
+ { &sSpk, sizeof(sSpk) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+static bool spkOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
+ sSpk.pcm = prefsGetBool(cfg->prefs, AUDIO_INI_SECTION, SPK_INI_PCM, false);
+
+ info->bits = SPK_BITS;
+ info->channels = SPK_CHANNELS;
+
+ if (sSpk.pcm) {
+ snprintf(info->description, sizeof(info->description), "PC speaker (digital sound)");
+ info->rate = PLATFORM_PIT_HZ / SPK_PWM_DIVISOR;
+ info->hasDac = true;
+ } else {
+ snprintf(info->description, sizeof(info->description), "PC speaker (tones)");
+ info->rate = SPK_TONE_RATE;
+ info->tone = spkTone;
+ }
+
+ return true;
+}
+
+
+static bool spkPlay(void) {
+ sSpk.locked = spkLock(true);
+
+ if (!sSpk.locked) {
+ return false;
+ }
+
+ if (!sSpk.pcm) {
+ return audioClockStart();
+ }
+
+ sSpk.pos = 0;
+ outportb(PIT_CMD_PORT, PIT_CH2_ONESHOT);
+ outportb(SPK_CTRL_PORT, inportb(SPK_CTRL_PORT) | SPK_CTRL_ON);
+ sSpk.timerId = platformTimerAddFast(PLATFORM_PIT_HZ / SPK_PWM_DIVISOR, spkTick, NULL, 0);
+ return sSpk.timerId >= 0;
+}
+
+
+// Timer interrupt (digital sound): the next sample becomes the next pulse.
+static void spkTick(void *unused) {
+ (void)unused;
+
+ if (sSpk.pos == 0) {
+ audioMixRender(sSpk.buf, SPK_PWM_BLOCK);
+ }
+
+ outportb(PIT_CH2_PORT, (uint8_t)(1 + ((uint32_t)sSpk.buf[sSpk.pos] * SPK_PWM_LEVELS >> BYTE_BITS)));
+ sSpk.pos = (sSpk.pos + 1) % SPK_PWM_BLOCK;
+}
+
+
+// Tones only: the mixer's choice of tone (0: silence). Interrupt time.
+static void spkTone(uint32_t hz) {
+ if (hz == 0) {
+ outportb(SPK_CTRL_PORT, inportb(SPK_CTRL_PORT) & ~SPK_CTRL_ON);
+ return;
+ }
+
+ uint32_t divisor = PLATFORM_PIT_HZ / hz;
+
+ outportb(PIT_CMD_PORT, PIT_CH2_SQUARE);
+ outportb(PIT_CH2_PORT, divisor & BYTE_MASK);
+ outportb(PIT_CH2_PORT, (divisor >> BYTE_BITS) & BYTE_MASK);
+ outportb(SPK_CTRL_PORT, inportb(SPK_CTRL_PORT) | SPK_CTRL_ON);
+}
diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs
index 354c170e..e27932a7 100644
--- a/src/libs/kpunch/dvxshell/dvxshell.dhs
+++ b/src/libs/kpunch/dvxshell/dvxshell.dhs
@@ -425,8 +425,9 @@ The GUI starts in the [video] mode of DVX.INI (prefsGetVideoMode). If that mode
After the desktop app has loaded:
.list
+.item When the DVX folder cannot be written to (platformIsReadOnly: the loader found it read-only, as on a CD-ROM), a message says once that changed settings last until DVX exits and that the help is used as it is.
.item The program named by DVX.INI [shell] welcome (SHELL_INI_WELCOME), if the key is there. The shipped DVX.INI names the Welcome window, which removes the key when the user ticks "Don't show this again", so the key itself is the "first start" flag.
-.item The program given with /RUN=path on the DVX command line (DVX /RUN=APPS\KPUNCH\SETUP\SETUP.APP is how INSTALL.BAT starts Setup).
+.item The program given with /RUN=path on the DVX command line (for example DVX /RUN=APPS\KPUNCH\NOTEPAD\NOTEPAD.APP).
.endlist
Both are started with shellRunCommand, so a missing program is reported to the user.
diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c
index 71b21164..5f16662c 100644
--- a/src/libs/kpunch/dvxshell/shellApp.c
+++ b/src/libs/kpunch/dvxshell/shellApp.c
@@ -98,9 +98,10 @@ static ShellAppT *findLoadedPath(const char *path);
static ShellAppT *findRunningPath(const char *path);
static void forceKillNow(AppContextT *ctx, ShellAppT *app);
static void killAppTask(ShellAppT *app);
-static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
+static bool makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
static bool pathCharsMatch(char a, char b);
static const char *pathUnderDir(const char *path, const char *dir);
+static int32_t refuseSecondCopy(AppContextT *ctx, const char *path);
static void releaseAppResources(ShellAppT *app);
void shellAppCallEnter(ShellAppT *app);
void shellAppCallLeave(ShellAppT *app);
@@ -480,31 +481,32 @@ static void killAppTask(ShellAppT *app) {
// Build a temp path for a multi-instance copy. Uses the TEMP or TMP
-// environment variable if set, otherwise falls back to the current directory.
-// The slot ID is embedded in the filename to ensure uniqueness.
+// environment variable if set, otherwise falls back to the current directory
+// (the DVX folder). The slot ID is embedded in the filename to ensure
+// uniqueness. False when there is nowhere to put the copy: no TEMP and a
+// read-only DVX folder.
// Example: TEMP=C:\TEMP -> "C:\TEMP\_dvx02.app"
-static void makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize) {
+static bool makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize) {
// Find extension from original path
const char *lastSlash = platformPathDirEnd(origPath);
const char *dot = strrchr(origPath, '.');
+ const char *tmpDir = platformTempDir();
if (!dot || (lastSlash && dot < lastSlash)) {
dot = origPath + strlen(origPath);
}
- const char *tmpDir = getenv("TEMP");
-
- if (!tmpDir) {
- tmpDir = getenv("TMP");
- }
-
// The fallback is anchored to the working directory: a bare file name
// would make dlopen search the library path instead of opening it.
- if (tmpDir && tmpDir[0]) {
+ if (tmpDir) {
snprintf(out, outSize, "%s" DVX_PATH_SEP "_dvx%02ld%s", tmpDir, (long)id, dot);
+ } else if (platformIsReadOnly()) {
+ return false;
} else {
snprintf(out, outSize, "." DVX_PATH_SEP "_dvx%02ld%s", (long)id, dot);
}
+
+ return true;
}
@@ -547,9 +549,19 @@ static const char *pathUnderDir(const char *path, const char *dir) {
}
+// A program that is already running and cannot run twice: say so.
+static int32_t refuseSecondCopy(AppContextT *ctx, const char *path) {
+ char msg[SHELL_MSG_MAX];
+
+ snprintf(msg, sizeof(msg), "%s is already running.", platformPathBaseName(path));
+ dvxErrorBox(ctx, NULL, msg);
+ return -1;
+}
+
+
// Shared teardown tail for both reap and force-kill: release the widget
-// classes and system-wide hotkeys the app registered and its hung-callback
-// bookkeeping (a force kill never
+// classes, system-wide hotkeys and timer-service timers the app registered
+// and its hung-callback bookkeeping (a force kill never
// runs the appShutdown that would unregister them, and they point into the
// module), unmap the DXE,
// delete any multi-instance temp copy, release the heap app context, and
@@ -560,6 +572,7 @@ static void releaseAppResources(ShellAppT *app) {
wgtUnregisterAppClasses(app->appId);
dvxUnregisterAppHotkeys(app->appId);
dvxAppHungForget(app->appId);
+ platformTimerRemoveOwner(app->appId);
for (int32_t i = 0; i < arrlen(sCleanupFns); i++) {
sCleanupFns[i](app->appId);
@@ -720,10 +733,7 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
AppDescriptorT *existDesc = (AppDescriptorT *)dlsym(existing->dxeHandle, DVX_SYM("appDescriptor"));
if (!existDesc || !existDesc->multiInstance) {
- char msg[SHELL_MSG_MAX];
- snprintf(msg, sizeof(msg), "%s is already running.", platformPathBaseName(path));
- dvxErrorBox(ctx, NULL, msg);
- return -1;
+ return refuseSecondCopy(ctx, path);
}
}
@@ -741,7 +751,12 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
if (existing) {
// Multi-instance allowed: copy to a temp file so dlopen gets
// an independent code+data image.
- makeTempPath(path, id, tempPath, sizeof(tempPath));
+ // Nowhere to put a second copy (read-only media, no TEMP): the
+ // program runs once, like a single-instance one.
+ if (!makeTempPath(path, id, tempPath, sizeof(tempPath))) {
+ dvxLog("Shell: no TEMP folder and a read-only DVX folder: one copy of %s only", path);
+ return refuseSecondCopy(ctx, path);
+ }
if (copyFile(path, tempPath) != 0) {
char msg[SHELL_MSG_MAX];
diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c
index 36bc8f13..7b2edb40 100644
--- a/src/libs/kpunch/dvxshell/shellMain.c
+++ b/src/libs/kpunch/dvxshell/shellMain.c
@@ -59,8 +59,8 @@
// (DVX_DEFAULT_VIDEO_*, 640x480) and asks whether to keep it.
//
// After the desktop: the [shell] welcome program (the first-run Welcome
-// window, which removes the key itself) and the /RUN= program (INSTALL.BAT
-// starts Setup this way) are started.
+// window, which removes the key itself) and the /RUN= program are
+// started.
#include "shellApp.h"
#include "shellInf.h"
@@ -97,10 +97,11 @@ DVX_ABI_STAMP(dvxshell);
#define SHELL_EXIT_TITLE "Exit DVX"
// Command-line switches: start in safe video mode; start a program once
-// the desktop is up (DVX /RUN=APPS\KPUNCH\SETUP\SETUP.APP)
+// the desktop is up (DVX /RUN=APPS\KPUNCH\NOTEPAD\NOTEPAD.APP)
#define SHELL_SWITCH_SAFE "/SAFE"
#define SHELL_SWITCH_RUN "/RUN="
#define SHELL_SAFE_TITLE "Safe Video Mode"
+#define SHELL_READ_ONLY_MSG "DVX is running from a disk it cannot write to, such as a CD-ROM. Settings you change are kept until you exit DVX, and the help is used as it is. To keep your settings, install DVX on your hard disk with SETUP."
#define SHELL_NO_VIDEO_MSG "DVX could not start: no usable graphics screen mode was found (VESA VBE 2.0 or later with a linear frame buffer is needed). The details are in DVX.LOG."
// How the GUI was started (videoStart).
@@ -131,6 +132,12 @@ static jmp_buf sCrashJmp;
// Volatile because it's written from a signal handler context. Tells
// the recovery code which signal fired (for logging/diagnostics).
static volatile int sCrashSignal = 0;
+// A program called exit() or abort() (appExitHook): the recovery point is
+// reached the same way as after a crash, but the program is ended quietly.
+// sRecoveryArmed is set once sCrashJmp holds the main loop's recovery point.
+static volatile bool sAppExitPending = false;
+static volatile int32_t sAppExitStatus = 0;
+static bool sRecoveryArmed = false;
// Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel).
// Each entry records the appId that registered it (captured from
// sCtx.currentAppId at registration time) so shellPurgeAppCallbacks can drop
@@ -188,6 +195,7 @@ static bool sExitRequested = false;
// Prototypes
// ============================================================
+static void appExitHook(int status);
static void appIdRestore(int32_t value);
static int32_t appIdSave(void);
static void compactDesktopFns(void);
@@ -230,6 +238,21 @@ static void videoOfferKeep(VideoStartE how, int32_t savedW, int32_t saved
static VideoStartE videoStart(bool safeAsked, int32_t savedW, int32_t savedH, int32_t savedBpp);
+// exit() / abort() from a loaded module (platformAppExit): while a program
+// is the current app, end just that program, the way End Task would --
+// back to the main loop's recovery point, like a crash but without the
+// fault message. Otherwise return, and the process exits normally.
+static void appExitHook(int status) {
+ if (!sRecoveryArmed || sCtx.currentAppId <= 0) {
+ return;
+ }
+
+ sAppExitStatus = status;
+ sAppExitPending = true;
+ longjmp(sCrashJmp, 1);
+}
+
+
// Task-switch hooks: currentAppId is semantically per-task (each app
// task runs as its own appId; the shell's main task is 0). Without
// save/restore on every switch, the first yield back from an app left
@@ -636,8 +659,10 @@ int shellMain(int argc, char *argv[]) {
tsSetContextHooks(appIdSave, appIdRestore);
// Install crash handler before loading apps so faults during app
- // initialization are caught and recovered from gracefully.
+ // initialization are caught and recovered from gracefully. exit()
+ // and abort() in a program take the same way out.
platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog);
+ platformSetAppExitHook(appExitHook);
// Initialize GUI -- switch from VGA splash to VESA mode as late as possible
// so the VGA loading splash stays visible through all the init above.
@@ -755,6 +780,14 @@ int shellMain(int argc, char *argv[]) {
return 1;
}
+ dvxPlaySound(SoundStartE);
+
+ // Read-only media (the loader found DVX's folder unwritable): said
+ // once, before the other start-up windows.
+ if (platformIsReadOnly()) {
+ dvxInfoBox(&sCtx, NULL, SHELL_READ_ONLY_MSG);
+ }
+
// Then the Welcome window while it is wanted, and a /RUN= program.
const char *welcome = prefsGetString(sPrefs, SHELL_INI_SECTION, SHELL_INI_WELCOME, "");
const char *runProg = findSwitch(argc, argv, SHELL_SWITCH_RUN);
@@ -775,54 +808,57 @@ int shellMain(int argc, char *argv[]) {
// code runs on the main task's stack (restored by longjmp) so it's
// safe to call any shell function.
if (setjmp(sCrashJmp) != 0) {
- // Returned here from crash handler via longjmp.
- // Capture the crashed task's app id BEFORE tsRecoverToMain, which
+ // Returned here from the crash handler or appExitHook via longjmp.
+ // Capture the ended task's app id BEFORE tsRecoverToMain, which
// restores the main task's context value (currentAppId back to 0).
- int32_t crashedAppId = sCtx.currentAppId;
- uint32_t crashedTask = tsCurrentId();
+ int32_t endedAppId = sCtx.currentAppId;
+ uint32_t endedTask = tsCurrentId();
+ bool exited = sAppExitPending;
- // The task switcher's currentIdx still points to the crashed task.
+ // The task switcher's currentIdx still points to the ended task.
// Fix it before doing anything else so the scheduler is consistent.
tsRecoverToMain();
- // The longjmp discarded the crashed task's and the main task's
+ // The longjmp discarded the ended task's and the main task's
// in-flight dispatch frames before their dispatchDepth-- could run;
// only other suspended tasks' frames remain. Otherwise
- // dvxDestroyWindow would defer forever (including the crashed app's
+ // dvxDestroyWindow would defer forever (including the ended app's
// own windows below, past its DXE being closed) and
// flushPendingDestroys would never fire again.
- taskDepthCrashReset(crashedTask);
+ taskDepthCrashReset(endedTask);
+ sAppExitPending = false;
- // Platform handler already logged signal name and register dump.
- // Log app-specific info here.
- dvxLog("Current app ID: %ld", (long)crashedAppId);
+ ShellAppT *app = endedAppId > 0 ? shellGetApp(endedAppId) : NULL;
- if (crashedAppId > 0) {
- ShellAppT *crashedApp = shellGetApp(crashedAppId);
-
- if (crashedApp) {
- dvxLog("App name: %s", crashedApp->name);
- dvxLog("App path: %s", crashedApp->path);
- dvxLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no");
- dvxLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId);
- }
+ if (exited) {
+ dvxLog("Shell: '%s' called exit (status %ld); ending it", app ? app->name : "?", (long)sAppExitStatus);
} else {
- dvxLog("Crashed in shell (task 0)");
- }
+ // Platform handler already logged signal name and register
+ // dump. Log app-specific info here.
+ dvxLog("Current app ID: %ld", (long)endedAppId);
- dvxLog("Recovering from crash, killing app %ld", (long)crashedAppId);
+ if (app) {
+ dvxLog("App name: %s", app->name);
+ dvxLog("App path: %s", app->path);
+ dvxLog("Has main loop: %s", app->hasMainLoop ? "yes" : "no");
+ dvxLog("Task ID: %lu", (unsigned long)app->mainTaskId);
+ } else if (endedAppId <= 0) {
+ dvxLog("Crashed in shell (task 0)");
+ }
+
+ dvxLog("Recovering from crash, killing app %ld", (long)endedAppId);
+ }
// Clear busy cursor so the fault dialog is interactive
dvxSetBusy(&sCtx, false);
- if (crashedAppId > 0) {
- ShellAppT *app = shellGetApp(crashedAppId);
+ if (app) {
+ char msg[SHELL_MSG_MAX];
+ snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
+ shellForceKillApp(&sCtx, app);
+ sCtx.currentAppId = 0;
- if (app) {
- char msg[SHELL_MSG_MAX];
- snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
- shellForceKillApp(&sCtx, app);
- sCtx.currentAppId = 0;
+ if (!exited) {
dvxErrorBox(&sCtx, "Application Error", msg);
}
}
@@ -832,6 +868,8 @@ int shellMain(int argc, char *argv[]) {
shellDesktopUpdate();
}
+ sRecoveryArmed = true;
+
// Main loop -- runs until dvxQuit() sets sCtx.running = false.
// Two yield points per iteration: one explicit (in shellFrame) and one
// via the idle callback inside dvxUpdate. A requested exit runs here,
@@ -842,6 +880,7 @@ int shellMain(int argc, char *argv[]) {
}
dvxLog("DVX Shell shutting down...");
+ dvxPlaySoundWait(SoundExitE);
// Clean shutdown: terminate all apps first (destroys windows, kills
// tasks, closes DXE handles), then tear down the task system and GUI
@@ -977,6 +1016,8 @@ int32_t shellTaskDepthShare(uint32_t taskId) {
// Shared by the normal exit and the failed desktop load: unhook and save
// the RNG seed, then tear down tasks, GUI and preferences.
static void shellTeardown(void) {
+ platformSetAppExitHook(NULL);
+ sRecoveryArmed = false;
entropyShutdown();
tsShutdown();
dvxShutdown(&sCtx);
diff --git a/src/libs/kpunch/game/Makefile b/src/libs/kpunch/game/Makefile
new file mode 100644
index 00000000..c873bf91
--- /dev/null
+++ b/src/libs/kpunch/game/Makefile
@@ -0,0 +1,64 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# Game Library Makefile for DJGPP cross-compilation
+#
+# Game helpers library (game.lib): frame pacing, focus-aware held keys,
+# app-owned timers, nearest-colour lookup for 8-bit palettes and the
+# BASIC functions. See game.h.
+
+include ../../../common.mk
+
+CFLAGS = $(DOS_CFLAGS) -I. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../libtasks
+
+OBJDIR = ../../../../obj/game
+LIBSDIR = ../../../../bin/libs
+
+SRCS = $(wildcard *.c)
+OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
+TARGETDIR = $(LIBSDIR)/kpunch/game
+TARGET = $(TARGETDIR)/game.lib
+
+.PHONY: all clean
+
+all: $(TARGET) $(TARGETDIR)/game.dep
+
+$(TARGETDIR)/game.dep: game.dep | $(TARGETDIR)
+ sed 's/$$/\r/' $< > $@
+
+$(TARGET): $(OBJS) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/game.dxe -U $(OBJS)
+ mv $(TARGETDIR)/game.dxe $@
+
+$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(OBJDIR):
+ mkdir -p $(OBJDIR)
+
+$(TARGETDIR):
+ mkdir -p $(TARGETDIR)
+
+-include $(OBJS:.o=.d)
+
+clean:
+ rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/game.dep $(TARGETDIR)
diff --git a/src/libs/kpunch/game/game.c b/src/libs/kpunch/game/game.c
new file mode 100644
index 00000000..a50471cf
--- /dev/null
+++ b/src/libs/kpunch/game/game.c
@@ -0,0 +1,182 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// game.c -- frame pacing, held keys, app timers and nearest colours
+//
+// See game.h. The pacer computes each frame's start from the frame
+// number (start + frame / hz) instead of adding a rounded period, so a
+// 70 Hz pacer does not drift from the clock however long it runs.
+
+#include "game.h"
+#include "taskSwch.h"
+
+#include
+
+// ABI stamp checked by the loader (see DVX_ABI_VERSION).
+DVX_ABI_STAMP(game);
+
+// Bits dropped from each 8-bit channel to form a cache cell, and the
+// offset of a cell's centre.
+#define PAL_CELL_SHIFT (8 - GAME_PAL_CACHE_BITS)
+#define PAL_CELL_CENTRE (1 << (PAL_CELL_SHIFT - 1))
+#define PAL_KNOWN_SHIFT 3 // cell >> 3 = byte of the known bitmap
+#define PAL_KNOWN_MASK 7 // cell & 7 = bit in that byte
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static uint64_t frameStartUs(const GameFramePacerT *p, uint64_t frame);
+static void frameYield(void);
+void gameFrameInit(GameFramePacerT *p, uint32_t hz);
+uint64_t gameFrameNext(GameFramePacerT *p, uint32_t *missed);
+uint32_t gameFrameWait(GameFramePacerT *p);
+bool gameKeyIsDown(const WindowT *win, int32_t rawCode);
+void gamePalCacheInit(GamePalCacheT *c, const uint8_t *pal);
+uint8_t gamePalCacheLookup(GamePalCacheT *c, uint8_t r, uint8_t g, uint8_t b);
+bool gamePalCacheSync(GamePalCacheT *c, const uint8_t *pal);
+uint8_t gamePalNearest(const uint8_t *pal, uint8_t r, uint8_t g, uint8_t b);
+int32_t gameTimerAdd(const AppContextT *ctx, uint32_t hz, PlatformTimerFnT fn, void *userData);
+
+
+static uint64_t frameStartUs(const GameFramePacerT *p, uint64_t frame) {
+ return p->startUs + frame * PLATFORM_US_PER_SEC / p->hz;
+}
+
+
+// Let the other programs run while waiting; with no other task there is
+// only the platform's idle yield.
+static void frameYield(void) {
+ if (tsActiveCount() > 1) {
+ tsYield();
+ } else {
+ platformYield();
+ }
+}
+
+
+void gameFrameInit(GameFramePacerT *p, uint32_t hz) {
+ if (hz < 1) {
+ hz = 1;
+ } else if (hz > GAME_FRAME_MAX_HZ) {
+ hz = GAME_FRAME_MAX_HZ;
+ }
+
+ memset(p, 0, sizeof(*p));
+ p->hz = hz;
+ p->startUs = platformMicros();
+}
+
+
+// When the frame after the next one has already begun, the frames in
+// between are dropped: the caller continues with the frame now running.
+uint64_t gameFrameNext(GameFramePacerT *p, uint32_t *missed) {
+ uint64_t next = p->frame + 1;
+ uint64_t now = platformMicros();
+
+ *missed = 0;
+
+ if (now >= frameStartUs(p, next + 1)) {
+ uint64_t current = (now - p->startUs) * p->hz / PLATFORM_US_PER_SEC;
+
+ *missed = (uint32_t)(current - next);
+ next = current;
+ }
+
+ p->frame = next;
+ p->missed += *missed;
+ return frameStartUs(p, next);
+}
+
+
+uint32_t gameFrameWait(GameFramePacerT *p) {
+ uint32_t missed = 0;
+ uint64_t due = gameFrameNext(p, &missed);
+
+ while (platformMicros() < due) {
+ frameYield();
+ }
+
+ return missed;
+}
+
+
+bool gameKeyIsDown(const WindowT *win, int32_t rawCode) {
+ return win && win->focused && platformKeyIsDown(rawCode);
+}
+
+
+void gamePalCacheInit(GamePalCacheT *c, const uint8_t *pal) {
+ memcpy(c->pal, pal, sizeof(c->pal));
+ memset(c->known, 0, sizeof(c->known));
+}
+
+
+uint8_t gamePalCacheLookup(GamePalCacheT *c, uint8_t r, uint8_t g, uint8_t b) {
+ uint32_t rc = (uint32_t)r >> PAL_CELL_SHIFT;
+ uint32_t gc = (uint32_t)g >> PAL_CELL_SHIFT;
+ uint32_t bc = (uint32_t)b >> PAL_CELL_SHIFT;
+ uint32_t cell = (((rc << GAME_PAL_CACHE_BITS) | gc) << GAME_PAL_CACHE_BITS) | bc;
+ uint8_t bit = (uint8_t)(1U << (cell & PAL_KNOWN_MASK));
+
+ if (!(c->known[cell >> PAL_KNOWN_SHIFT] & bit)) {
+ c->index[cell] = gamePalNearest(c->pal, (uint8_t)((rc << PAL_CELL_SHIFT) | PAL_CELL_CENTRE), (uint8_t)((gc << PAL_CELL_SHIFT) | PAL_CELL_CENTRE), (uint8_t)((bc << PAL_CELL_SHIFT) | PAL_CELL_CENTRE));
+ c->known[cell >> PAL_KNOWN_SHIFT] |= bit;
+ }
+
+ return c->index[cell];
+}
+
+
+bool gamePalCacheSync(GamePalCacheT *c, const uint8_t *pal) {
+ if (memcmp(c->pal, pal, sizeof(c->pal)) == 0) {
+ return false;
+ }
+
+ gamePalCacheInit(c, pal);
+ return true;
+}
+
+
+uint8_t gamePalNearest(const uint8_t *pal, uint8_t r, uint8_t g, uint8_t b) {
+ int32_t best = 0;
+ int32_t bestDist = INT32_MAX;
+
+ for (int32_t i = 0; i < PAL_ENTRIES && bestDist > 0; i++) {
+ int32_t dr = (int32_t)r - pal[i * PAL_CHANNELS];
+ int32_t dg = (int32_t)g - pal[i * PAL_CHANNELS + 1];
+ int32_t db = (int32_t)b - pal[i * PAL_CHANNELS + 2];
+ int32_t dist = dr * dr + dg * dg + db * db;
+
+ if (dist < bestDist) {
+ bestDist = dist;
+ best = i;
+ }
+ }
+
+ return (uint8_t)best;
+}
+
+
+int32_t gameTimerAdd(const AppContextT *ctx, uint32_t hz, PlatformTimerFnT fn, void *userData) {
+ return platformTimerAdd(hz, fn, userData, ctx->currentAppId);
+}
diff --git a/src/libs/kpunch/game/game.dep b/src/libs/kpunch/game/game.dep
new file mode 100644
index 00000000..2afaca0b
--- /dev/null
+++ b/src/libs/kpunch/game/game.dep
@@ -0,0 +1,2 @@
+libtasks
+libdvx
diff --git a/src/libs/kpunch/game/game.dhs b/src/libs/kpunch/game/game.dhs
new file mode 100644
index 00000000..b8859fc5
--- /dev/null
+++ b/src/libs/kpunch/game/game.dhs
@@ -0,0 +1,94 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+.section Technical Reference (for Developers)|Libraries
+.topic lib.game
+.title Game Helpers Library
+.toc 0 Game Helpers Library
+.index game
+.index game.h
+.index gameFrameInit
+.index gameFrameWait
+.index gameFrameNext
+.index FRAMEWAIT
+.index GameFramePacerT
+.index gameKeyIsDown
+.index gameTimerAdd
+.index gamePalNearest
+.index gamePalCacheInit
+.index gamePalCacheLookup
+.index gamePalCacheSync
+.index GamePalCacheT
+.index GameKeyDown
+.index game.bas
+
+.h1 Game Helpers Library
+
+Frame pacing, held keys, program-owned timers and nearest-colour lookup for programs that run like games in a DVX window. It builds on the core's game input windows, held-key table, timer service and high-resolution clock (see Writing Games for DVX).
+
+Header: game/game.h
+
+Loaded as: bin/libs/kpunch/game/game.lib
+
+.h2 Frame Pacing
+
+.code
+GameFramePacerT pacer;
+
+gameFrameInit(&pacer, 70);
+
+while (running) {
+ uint32_t missed = gameFrameWait(&pacer); // 0 when on time
+
+ updateGame(missed + 1); // catch the game logic up
+ drawFrame();
+}
+.endcode
+
+gameFrameInit(p, hz) starts pacing at hz frames a second (1 to GAME_FRAME_MAX_HZ, 1000; out of range is clamped) with frame 0 beginning now. gameFrameWait waits until the next frame begins and returns the number of frames that were missed. While it waits it gives the time to the other programs (tsYield), so call it from a program with its own main loop (hasMainLoop), never from a window callback. Frame n begins at start + n / hz on platformMicros, so the pace does not drift however long it runs. When the frame after the next one has already begun (the caller was busy), the frames in between are skipped, not caught up: gameFrameWait returns at once with how many, and GameFramePacerT.missed keeps the total. A frame that starts late but within its own time counts as on time.
+
+gameFrameNext(p, &missed) does the same without the waiting: it moves on to the next frame (skipping and counting missed ones the same way) and returns the platformMicros time that frame begins, for a caller that waits its own way. The BASIC runtime uses it for the FRAMEWAIT statement, keeping the desktop running while it waits.
+
+.h2 Held Keys
+
+gameKeyIsDown(win, rawCode) is true while the key is held down and win has the keyboard focus. Keys are named by raw code (KEY_RAW_CODE, KEY_RAW_* in dvxTypes.h). The held-key table starts over whenever the focus moves, so a key pressed while another window had the focus is not seen until it repeats.
+
+.h2 Timers
+
+gameTimerAdd(ctx, hz, fn, userData) is platformTimerAdd for the calling program (ctx->currentAppId): the timer is removed when the program ends, however it ends. fn runs inside the timer interrupt; read the rules at the Timer Service in the platform reference before writing one. Remove it with platformTimerRemove.
+
+.h2 Colours on a 256-Colour Desktop
+
+On an 8-bit desktop every window shares the desktop palette (dvxGetDisplay(ctx)->palette; NULL at 15 bits and more). A game with its own colours draws the nearest desktop entries.
+
+.table
+ Function Description
+ -------- -----------
+ gamePalNearest(pal, r, g, b) The entry of the 256-entry palette pal (R, G, B bytes) closest to the colour: smallest squared RGB distance, lowest index on a tie. An exact search of all 256 entries.
+ gamePalCacheInit(c, pal) Record pal in the cache c and empty it. GamePalCacheT is about 37 KB: allocate it.
+ gamePalCacheLookup(c, r, g, b) The entry nearest to the centre of the colour's 15-bit cell (5 bits per channel), searched once per cell and then remembered, so every colour in a cell gets the same answer.
+ gamePalCacheSync(c, pal) Start over (and return true) when pal differs from the recorded palette; call it once a frame if the palette can change.
+.endtable
+
+.h2 BASIC
+
+src/include/basic/game.bas declares GameKeyDown(keyCode), True while the key is held and one of the calling program's windows has the focus, with GAMEKEY_* constants for common keys. The BASIC runtime gives the library the GUI context (gameBasSetContext) when a program starts. Frame pacing from BASIC comes with the BASIC game statements later.
diff --git a/src/libs/kpunch/game/game.h b/src/libs/kpunch/game/game.h
new file mode 100644
index 00000000..d9251e05
--- /dev/null
+++ b/src/libs/kpunch/game/game.h
@@ -0,0 +1,124 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// game.h -- game helpers library (game.lib)
+//
+// Helpers for programs that run like games inside a DVX window:
+//
+// Frame pacing gameFrameInit / gameFrameWait: wait for the next frame
+// at N frames a second on the high-resolution clock,
+// yielding to the other programs while waiting, and count
+// the frames that were missed.
+// Held keys gameKeyIsDown: is a key down while the window has focus.
+// Timers gameTimerAdd: a timer-service callback owned by the
+// calling program (removed when it ends).
+// 8-bit colour gamePalNearest / GamePalCacheT: the palette entry
+// closest to an RGB colour, for games on a 256-colour
+// desktop.
+//
+// Raw key input itself (WindowT.gameInput and onRawKey), the held-key
+// table, the timer service and platformMicros are in the core
+// (dvxTypes.h, dvxPlat.h); this library builds on them.
+#ifndef GAME_LIB_H
+#define GAME_LIB_H
+
+#include "dvxApp.h"
+#include "dvxPal.h"
+#include "dvxPlat.h"
+
+#include
+#include
+
+// Highest frame rate gameFrameInit accepts
+#define GAME_FRAME_MAX_HZ 1000
+
+// Nearest-colour cache: colours are looked up by their top
+// GAME_PAL_CACHE_BITS bits per channel (a 15-bit colour).
+#define GAME_PAL_CACHE_BITS 5
+#define GAME_PAL_CACHE_SIZE (1 << (GAME_PAL_CACHE_BITS * 3))
+#define GAME_PAL_KNOWN_BYTES (GAME_PAL_CACHE_SIZE / 8)
+
+// Frame pacer state (the caller owns it; gameFrameInit fills it in).
+typedef struct {
+ uint64_t startUs; // platformMicros() when frame 0 began
+ uint64_t frame; // frame number gameFrameWait last returned for
+ uint32_t hz; // frames per second
+ uint32_t missed; // frames skipped so far
+} GameFramePacerT;
+
+// Nearest-colour cache for one palette (about 37 KB: allocate it).
+typedef struct {
+ uint8_t pal[PAL_ENTRIES * PAL_CHANNELS]; // the palette the entries were found in
+ uint8_t index[GAME_PAL_CACHE_SIZE]; // nearest entry per 15-bit colour
+ uint8_t known[GAME_PAL_KNOWN_BYTES]; // bit set: that index is filled in
+} GamePalCacheT;
+
+// Start pacing at hz frames a second (1..GAME_FRAME_MAX_HZ; out of range
+// is clamped); frame 0 begins now.
+void gameFrameInit(GameFramePacerT *p, uint32_t hz);
+
+// Wait until the next frame begins, giving the time to the other programs
+// (tsYield; call it from a program with its own main loop). Returns the
+// number of frames missed: 0 when on time, else the frames that already
+// ended while the caller was busy, which are skipped rather than caught
+// up. A frame that starts late but within its own time counts as on
+// time and returns at once.
+uint32_t gameFrameWait(GameFramePacerT *p);
+
+// gameFrameWait without the waiting, for a caller that waits its own way
+// (the BASIC runtime keeps the desktop running meanwhile): moves on to
+// the next frame (skipping frames that already ended, counted in
+// *missed) and returns when it begins, in platformMicros() time.
+uint64_t gameFrameNext(GameFramePacerT *p, uint32_t *missed);
+
+// True while the key with this raw code (KEY_RAW_CODE, dvxTypes.h) is held
+// down and win has the keyboard focus. The held-key table starts over
+// whenever the focus moves, so a key pressed while another window had
+// focus is not seen until it repeats.
+bool gameKeyIsDown(const WindowT *win, int32_t rawCode);
+
+// Nearest-colour cache. Init records the palette (768 bytes, R G B per
+// entry, such as the 8-bit desktop's dvxGetDisplay(ctx)->palette) and
+// empties the cache. Lookup returns the entry nearest to the colour's
+// 15-bit cell (the cell's centre, so every colour in a cell gets the same
+// answer), searching once per cell. Sync starts over when pal differs
+// from the recorded palette and says whether it did; call it once a frame
+// if the palette can change.
+void gamePalCacheInit(GamePalCacheT *c, const uint8_t *pal);
+uint8_t gamePalCacheLookup(GamePalCacheT *c, uint8_t r, uint8_t g, uint8_t b);
+bool gamePalCacheSync(GamePalCacheT *c, const uint8_t *pal);
+
+// The entry of the 256-entry palette pal closest to r, g, b (smallest
+// squared RGB distance; the lowest index on a tie). An exact search.
+uint8_t gamePalNearest(const uint8_t *pal, uint8_t r, uint8_t g, uint8_t b);
+
+// platformTimerAdd for the calling program (ctx->currentAppId): the timer
+// is removed when the program ends, however it ends. fn runs inside the
+// timer interrupt; see PlatformTimerFnT for what it may do.
+int32_t gameTimerAdd(const AppContextT *ctx, uint32_t hz, PlatformTimerFnT fn, void *userData);
+
+// BASIC (src/include/basic/game.bas). The runtime hands over its GUI
+// context when it starts a program.
+void gameBasSetContext(AppContextT *ctx);
+int32_t GameKeyDown(int32_t rawCode);
+
+#endif // GAME_LIB_H
diff --git a/src/libs/kpunch/game/gameBas.c b/src/libs/kpunch/game/gameBas.c
new file mode 100644
index 00000000..88781eb6
--- /dev/null
+++ b/src/libs/kpunch/game/gameBas.c
@@ -0,0 +1,61 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// gameBas.c -- DVX BASIC entry points of the game library
+//
+// Called through DECLARE LIBRARY (src/include/basic/game.bas), so the
+// signatures use only BASIC-marshalable types: LONG in, True (-1) / False
+// (0) out. Held keys are only reported while one of the calling
+// program's windows has the focus, which needs the GUI context the BASIC
+// runtime hands over with gameBasSetContext when it starts a program.
+
+#include "game.h"
+
+#define BAS_TRUE (-1)
+#define BAS_FALSE 0
+
+static AppContextT *sBasCtx = NULL;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void gameBasSetContext(AppContextT *ctx);
+int32_t GameKeyDown(int32_t rawCode);
+
+
+void gameBasSetContext(AppContextT *ctx) {
+ sBasCtx = ctx;
+}
+
+
+// The focused window must belong to the program asking (currentAppId is
+// the calling program while its BASIC code runs).
+int32_t GameKeyDown(int32_t rawCode) {
+ if (!sBasCtx || sBasCtx->stack.focusedIdx < 0) {
+ return BAS_FALSE;
+ }
+
+ const WindowT *win = sBasCtx->stack.windows[sBasCtx->stack.focusedIdx];
+
+ return (win->appId == sBasCtx->currentAppId && gameKeyIsDown(win, rawCode)) ? BAS_TRUE : BAS_FALSE;
+}
diff --git a/src/libs/kpunch/joystick/Makefile b/src/libs/kpunch/joystick/Makefile
new file mode 100644
index 00000000..5bdde717
--- /dev/null
+++ b/src/libs/kpunch/joystick/Makefile
@@ -0,0 +1,73 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# Joystick Library Makefile for DJGPP cross-compilation
+#
+# joystick.lib (the library: devices, calibration, handles, events, BASIC
+# functions) and its drivers: every drivers// directory is one
+# driver module, built from all its sources into drivers/.drv beside
+# the library, where joystick.lib finds it (dvxDrv.h). Nothing lists
+# drivers. See joystick.h.
+
+include ../../../common.mk
+
+CFLAGS = $(DOS_CFLAGS) -I. -I.. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../libtasks -I../dvxshell
+
+OBJDIR = ../../../../obj/joystick
+LIBSDIR = ../../../../bin/libs
+TARGETDIR = $(LIBSDIR)/kpunch/joystick
+TARGET = $(TARGETDIR)/joystick.lib
+
+SRCS = $(wildcard *.c)
+OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
+
+DRVDIR = $(TARGETDIR)/drivers
+DRV_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/*/*.c))
+DRV_TARGETS = $(patsubst %,$(DRVDIR)/%.drv,$(DVX_DRIVERS))
+
+.PHONY: all clean
+
+all: $(TARGET) $(TARGETDIR)/joystick.dep $(DRV_TARGETS)
+
+$(TARGETDIR)/joystick.dep: joystick.dep | $(TARGETDIR)
+ sed 's/$$/\r/' $< > $@
+
+$(TARGET): $(OBJS) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/joystick.dxe -U $(OBJS)
+ mv $(TARGETDIR)/joystick.dxe $@
+
+$(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d)))
+
+$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES)
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(TARGETDIR):
+ mkdir -p $(TARGETDIR)
+
+$(DRVDIR):
+ mkdir -p $(DRVDIR)
+
+-include $(OBJS:.o=.d) $(DRV_OBJS:.o=.d)
+
+clean:
+ rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c b/src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c
new file mode 100644
index 00000000..67b2c1eb
--- /dev/null
+++ b/src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c
@@ -0,0 +1,142 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joyKeys.c -- keyboard joystick driver (joykeys.drv): a joystick made of
+// keys, so games work without a joystick. A fallback driver: it stands in
+// when no other device is found (or always / never, [joystick] fallback).
+// Two axes (left/right, up/down: full deflection while a key is held, the
+// centre when both or neither are) and four buttons, on the keys named in
+// [joystick] keyboard.up ... keyboard.button4 (names from joyKeyName).
+// The library reads it as neutral for a program without the focus.
+
+#include "joystick/joystick.h"
+#include "dvxPlat.h"
+
+#include
+#include
+
+#define KEYS_AXES 2
+#define KEYS_AXIS_MASK 0x03
+#define KEYS_BUTTONS 4
+
+#define KEYS_KEY_MAX 32
+
+// The slots, in JOY_KBD_SLOT_NAMES order
+typedef enum {
+ KeyUpE,
+ KeyDownE,
+ KeyLeftE,
+ KeyRightE,
+ KeyButton1E,
+ KeyCountE = KeyButton1E + KEYS_BUTTONS
+} KeySlotE;
+
+_Static_assert(KeyCountE == JOY_KBD_SLOT_COUNT, "keyboard joystick slots must match JOY_KBD_SLOT_NAMES");
+
+// ============================================================
+// Tables
+// ============================================================
+
+static const char *const sSlotNames[KeyCountE] = { JOY_KBD_SLOT_NAMES };
+static const char *const sDefaultKeys[KeyCountE] = { JOY_KBD_SLOT_DEFAULTS };
+
+// ============================================================
+// Module state
+// ============================================================
+
+static int32_t sCodes[KeyCountE];
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t axisOf(int32_t negSlot, int32_t posSlot);
+static void keysInfo(int32_t unit, JoyInfoT *out);
+static void keysPoll(JoyRawT *raw, int32_t units);
+static int32_t keysProbe(void);
+static void keysSettings(void);
+
+JOY_DRIVER("keyboard", JOY_PRIORITY_KEYBOARD,
+ .flags = JOY_DRIVER_FALLBACK,
+ .probe = keysProbe,
+ .info = keysInfo,
+ .poll = keysPoll,
+ .settingsChanged = keysSettings
+);
+
+
+static int32_t axisOf(int32_t negSlot, int32_t posSlot) {
+ bool neg = platformKeyIsDown(sCodes[negSlot]);
+ bool pos = platformKeyIsDown(sCodes[posSlot]);
+
+ if (neg == pos) {
+ return 0;
+ }
+
+ return neg ? JOY_AXIS_MIN : JOY_AXIS_MAX;
+}
+
+
+static void keysInfo(int32_t unit, JoyInfoT *out) {
+ (void)unit;
+ memset(out, 0, sizeof(*out));
+ snprintf(out->name, sizeof(out->name), "%s", "Keyboard joystick");
+ out->axisCount = KEYS_AXES;
+ out->buttonCount = KEYS_BUTTONS;
+ out->axisPresent = KEYS_AXIS_MASK;
+ out->caps = JOY_CAP_KEYBOARD;
+}
+
+
+static void keysPoll(JoyRawT *raw, int32_t units) {
+ (void)units;
+ raw->axes[0] = axisOf(KeyLeftE, KeyRightE);
+ raw->axes[1] = axisOf(KeyUpE, KeyDownE);
+ raw->axisValid = KEYS_AXIS_MASK;
+ raw->connected = true;
+
+ for (int32_t b = 0; b < KEYS_BUTTONS; b++) {
+ if (platformKeyIsDown(sCodes[KeyButton1E + b])) {
+ raw->buttons |= 1U << b;
+ }
+ }
+}
+
+
+static int32_t keysProbe(void) {
+ keysSettings();
+ return 1;
+}
+
+
+// A key name that is not known falls back to the default key.
+static void keysSettings(void) {
+ char key[KEYS_KEY_MAX];
+
+ for (int32_t i = 0; i < KeyCountE; i++) {
+ snprintf(key, sizeof(key), "%s%s", JOY_KBD_KEY_PREFIX, sSlotNames[i]);
+
+ int32_t code = joyKeyFind(joySettingGet(key, sDefaultKeys[i]));
+
+ sCodes[i] = code >= 0 ? code : joyKeyFind(sDefaultKeys[i]);
+ }
+}
diff --git a/src/libs/kpunch/joystick/drivers/joyport/joyPort.c b/src/libs/kpunch/joystick/drivers/joyport/joyPort.c
new file mode 100644
index 00000000..c96b60e9
--- /dev/null
+++ b/src/libs/kpunch/joystick/drivers/joyport/joyPort.c
@@ -0,0 +1,327 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joyPort.c -- the standard PC game port driver: probing, the one-shot
+// axis timing and the joystick library's driver entry points.
+//
+// The timing runs in program context with interrupts left on: each sample
+// reads the port and the microsecond clock, and an axis whose fall is seen
+// only after a pause longer than JOYPORT_GAP_US (an interrupt handler ran
+// in between) keeps its previous value instead of taking a wrong one. So
+// the game port never holds up the timer, the sound card or the mouse.
+// Buttons are also sampled by the timer interrupt (joyPortTick), so a
+// press shorter than a poll interval is not lost.
+
+#include "joyPort.h"
+#include "dvxPlat.h"
+
+#include
+#include
+#include
+#include
+
+#define TICK_LOCK_BYTES 128 // portTick's code, pinned for the interrupt
+#define NAME_FMT_STICK "Game port joystick %" PRId32
+#define NAME_FOUR_AXES "Game port joystick (4 axes)"
+#define PROBE_LINE_MAX 160
+
+// ============================================================
+// Tables
+// ============================================================
+
+static const char *const sAxisNames[JOYPORT_AXES] = { "A X", "A Y", "B X", "B Y" };
+
+// ============================================================
+// Module state
+// ============================================================
+
+static const JoyPortHwT *sHw = NULL;
+static uint8_t sPresent = 0; // axes found by the probe
+static uint8_t sActive = 0; // axes timed every poll
+static uint8_t sMisses[JOYPORT_AXES]; // timeouts in a row
+static uint32_t sPolls = 0;
+static bool sUnitsFour = false; // one unit with all four axes ([joystick] gameport.mode)
+static int32_t sUnitCount = 0;
+static int32_t sStick[JOYPORT_AXES / JOYPORT_STICK_AXES]; // unit -> stick (0 = A, 1 = B)
+static volatile uint32_t sLatched = 0; // buttons that went down, seen by the interrupt
+static uint32_t sTickHeld = 0;
+
+// platformLockRegion or platformUnlockRegion
+typedef int32_t (*PinFnT)(const void *addr, uint32_t size, bool isCode);
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static char axisMark(int32_t axis);
+static uint8_t heldButtons(uint8_t port);
+void joyPortMeasure(const JoyPortHwT *hw, uint8_t mask, JoyPortReadT *out);
+void joyPortRelease(void);
+void joyPortUse(const JoyPortHwT *hw);
+static void logProbe(uint8_t idle, const JoyPortReadT *r);
+static void pinTickRegions(PinFnT pin);
+static void portInfo(int32_t unit, JoyInfoT *out);
+static void portPoll(JoyRawT *raw, int32_t units);
+static int32_t portProbe(void);
+static void portTick(void);
+static uint8_t unitAxes(int32_t unit);
+static void updateActive(const JoyPortReadT *r);
+
+JOY_DRIVER("gameport", JOY_PRIORITY_PORT,
+ .probe = portProbe,
+ .info = portInfo,
+ .poll = portPoll,
+ .tick = portTick
+);
+
+
+// '1' when the probe found the axis, else '0' (for the log).
+static char axisMark(int32_t axis) {
+ return (sPresent & (1U << axis)) ? '1' : '0';
+}
+
+
+static uint8_t heldButtons(uint8_t port) {
+ return (uint8_t)((~port >> JOYPORT_BUTTON_SHIFT) & JOYPORT_BUTTON_MASK);
+}
+
+
+void joyPortMeasure(const JoyPortHwT *hw, uint8_t mask, JoyPortReadT *out) {
+ uint8_t pending = mask & JOYPORT_ALL_AXES;
+
+ memset(out, 0, sizeof(*out));
+ hw->trigger();
+
+ uint64_t start = hw->micros();
+ uint64_t prev = start;
+ uint64_t prevPrev = start;
+
+ while (pending) {
+ uint8_t bits = hw->read();
+ uint64_t now = hw->micros();
+ uint8_t fell = pending & (uint8_t)~bits;
+
+ if (fell) {
+ // It fell after the read of the previous sample, which lies
+ // between the two clock readings before this one.
+ int32_t us = (int32_t)((prev + now) / 2 - start);
+
+ for (int32_t a = 0; a < JOYPORT_AXES; a++) {
+ if (fell & (1U << a)) {
+ out->us[a] = us;
+ }
+ }
+
+ out->done |= fell;
+ pending &= (uint8_t)~fell;
+
+ if (now - prevPrev > JOYPORT_GAP_US) {
+ out->disturbed |= fell;
+ }
+ }
+
+ if (now - start >= JOYPORT_TIMEOUT_US) {
+ break;
+ }
+
+ prevPrev = prev;
+ prev = now;
+ }
+
+ out->buttons = heldButtons(hw->read());
+}
+
+
+void joyPortRelease(void) {
+ if (sHw) {
+ pinTickRegions(platformUnlockRegion);
+ sHw = NULL;
+ }
+}
+
+
+void joyPortUse(const JoyPortHwT *hw) {
+ joyPortRelease();
+ sHw = hw;
+ pinTickRegions(platformLockRegion);
+}
+
+
+// What the probe saw (for checking a joystick or a game port card): the
+// port before the timers were started and each axis's time.
+static void logProbe(uint8_t idle, const JoyPortReadT *r) {
+ char line[PROBE_LINE_MAX];
+ int32_t pos = dvxStrAppendf(line, sizeof(line), 0, "Joystick: game port probe: port %02" PRIX32, (uint32_t)idle);
+
+ for (int32_t a = 0; a < JOYPORT_AXES; a++) {
+ if (r->done & (1U << a)) {
+ pos = dvxStrAppendf(line, sizeof(line), pos, ", %s %" PRId32 " us", sAxisNames[a], r->us[a]);
+ } else {
+ pos = dvxStrAppendf(line, sizeof(line), pos, ", %s none", sAxisNames[a]);
+ }
+ }
+
+ if (pos >= 0) {
+ dvxLog("%s", line);
+ }
+}
+
+
+// Pin (or release) what the timer interrupt touches, for the current sHw.
+static void pinTickRegions(PinFnT pin) {
+ pin((const void *)portTick, TICK_LOCK_BYTES, true);
+ pin((const void *)heldButtons, TICK_LOCK_BYTES, true);
+ pin((const void *)&sHw, sizeof(sHw), false);
+ pin((const void *)sHw, sizeof(*sHw), false);
+ pin((const void *)&sLatched, sizeof(sLatched), false);
+ pin((const void *)&sTickHeld, sizeof(sTickHeld), false);
+}
+
+
+static void portInfo(int32_t unit, JoyInfoT *out) {
+ memset(out, 0, sizeof(*out));
+ out->caps = JOY_CAP_CALIBRATE;
+ out->axisPresent = (uint32_t)(unitAxes(unit) >> (sUnitsFour ? 0 : sStick[unit] * JOYPORT_STICK_AXES));
+
+ if (sUnitsFour) {
+ snprintf(out->name, sizeof(out->name), "%s", NAME_FOUR_AXES);
+ out->axisCount = JOYPORT_AXES;
+ out->buttonCount = JOYPORT_AXES;
+ } else {
+ snprintf(out->name, sizeof(out->name), NAME_FMT_STICK, sStick[unit] + 1);
+ out->axisCount = JOYPORT_STICK_AXES;
+ out->buttonCount = JOYPORT_STICK_BUTTONS;
+ }
+}
+
+
+// Time the active axes (every probed axis now and then, to notice a stick
+// plugged back in) and hand each unit its axes and buttons.
+static void portPoll(JoyRawT *raw, int32_t units) {
+ JoyPortReadT r;
+ uint8_t mask = (sPolls % JOYPORT_RECHECK_POLLS == 0) ? sPresent : sActive;
+
+ sPolls++;
+ joyPortMeasure(sHw, mask, &r);
+ updateActive(&r);
+
+ uint32_t pressed = __atomic_exchange_n(&sLatched, 0, __ATOMIC_SEQ_CST);
+
+ for (int32_t u = 0; u < units; u++) {
+ int32_t shift = sUnitsFour ? 0 : sStick[u] * JOYPORT_STICK_AXES;
+ uint32_t bMask = sUnitsFour ? JOYPORT_BUTTON_MASK : JOYPORT_STICK_MASK;
+ uint8_t good = r.done & (uint8_t)~r.disturbed;
+
+ for (int32_t a = 0; a < (sUnitsFour ? JOYPORT_AXES : JOYPORT_STICK_AXES); a++) {
+ if (good & (1U << (a + shift))) {
+ raw[u].axes[a] = r.us[a + shift];
+ raw[u].axisValid |= 1U << a;
+ }
+ }
+
+ raw[u].buttons = ((uint32_t)r.buttons >> shift) & bMask;
+ raw[u].pressed = (pressed >> shift) & bMask;
+ raw[u].connected = ((r.done | sActive) & unitAxes(u)) != 0;
+ }
+}
+
+
+// Which axes answer, and the units they make (two sticks, or one with
+// every axis in the four-axis mode).
+static int32_t portProbe(void) {
+ JoyPortReadT r;
+ uint8_t idle = sHw->read();
+
+ joyPortMeasure(sHw, JOYPORT_ALL_AXES, &r);
+ sPresent = 0;
+
+ for (int32_t a = 0; a < JOYPORT_AXES; a++) {
+ if ((r.done & (1U << a)) && r.us[a] >= JOYPORT_MIN_US) {
+ sPresent |= (uint8_t)(1U << a);
+ }
+ }
+
+ sActive = sPresent;
+ sPolls = 0;
+ sUnitsFour = strcasecmp(joySettingGet(JOY_KEY_GAMEPORT_MODE, JOY_GAMEPORT_TWO), JOY_GAMEPORT_FOUR) == 0;
+ sUnitCount = 0;
+ memset(sMisses, 0, sizeof(sMisses));
+
+ for (int32_t s = 0; s < JOYPORT_AXES / JOYPORT_STICK_AXES; s++) {
+ if (sPresent & (JOYPORT_STICK_MASK << (s * JOYPORT_STICK_AXES))) {
+ sStick[sUnitCount] = s;
+ sUnitCount++;
+ }
+ }
+
+ if (sUnitsFour) {
+ sStick[0] = 0;
+ sUnitCount = sPresent ? 1 : 0;
+ }
+
+ dvxLog("Joystick: game port axes found: %c%c%c%c (A X, A Y, B X, B Y)", axisMark(0), axisMark(1), axisMark(2), axisMark(3));
+ logProbe(idle, &r);
+ return sUnitCount;
+}
+
+
+// INSIDE THE TIMER INTERRUPT: latch buttons that went down.
+static void portTick(void) {
+ uint32_t held = heldButtons(sHw->read());
+
+ sLatched |= held & ~sTickHeld;
+ sTickHeld = held;
+}
+
+
+// The probed axes of a unit, as port axis bits.
+static uint8_t unitAxes(int32_t unit) {
+ if (sUnitsFour) {
+ return sPresent;
+ }
+
+ return sPresent & (uint8_t)(JOYPORT_STICK_MASK << (sStick[unit] * JOYPORT_STICK_AXES));
+}
+
+
+// Count timeouts; drop an axis after JOYPORT_DROP_AFTER in a row, take it
+// back as soon as it answers.
+static void updateActive(const JoyPortReadT *r) {
+ for (int32_t a = 0; a < JOYPORT_AXES; a++) {
+ uint8_t bit = (uint8_t)(1U << a);
+
+ if (!(sPresent & bit)) {
+ continue;
+ }
+
+ if (r->done & bit) {
+ sMisses[a] = 0;
+ sActive |= bit;
+ } else if (sActive & bit) {
+ sMisses[a]++;
+
+ if (sMisses[a] >= JOYPORT_DROP_AFTER) {
+ sActive &= (uint8_t)~bit;
+ }
+ }
+ }
+}
diff --git a/src/libs/kpunch/joystick/drivers/joyport/joyPort.h b/src/libs/kpunch/joystick/drivers/joyport/joyPort.h
new file mode 100644
index 00000000..50d4426a
--- /dev/null
+++ b/src/libs/kpunch/joystick/drivers/joyport/joyPort.h
@@ -0,0 +1,88 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joyPort.h -- the standard PC game port driver (joyport.drv), shared by
+// its portable part (joyPort.c), its DOS hardware access (joyPortDos.c) and
+// the host tests.
+//
+// Port 201h: writing any value starts four one-shot timers, one per axis
+// (stick A X/Y, stick B X/Y); bits 0-3 read 1 until each timer runs out,
+// after a time proportional to the axis's resistance (about 24 us plus
+// 11 us per kilohm: 0.02 to 1.1 ms for a 100 kilohm stick). Bits 4-7 are
+// the buttons A1, A2, B1, B2, reading 0 while pressed. An axis with
+// nothing connected never runs out.
+#ifndef JOY_PORT_H
+#define JOY_PORT_H
+
+#include "joystick/joystick.h"
+
+#define JOYPORT_AXES 4
+#define JOYPORT_ALL_AXES 0x0F
+#define JOYPORT_BUTTON_SHIFT 4
+#define JOYPORT_BUTTON_MASK 0x0F
+#define JOYPORT_STICK_AXES 2
+#define JOYPORT_STICK_BUTTONS 2
+#define JOYPORT_STICK_MASK 0x03 // a stick's two axes / buttons, shifted by stick * 2
+
+// A measurement gives up after JOYPORT_TIMEOUT_US (a missing axis); an
+// axis whose fall is only known to lie in a window of more than
+// JOYPORT_GAP_US (the three clock readings round it; an interrupt ran in
+// between) is not trusted; at the probe an axis must
+// take at least JOYPORT_MIN_US (some cards read 0 for a missing axis).
+#define JOYPORT_TIMEOUT_US 3000
+#define JOYPORT_GAP_US 40
+#define JOYPORT_MIN_US 10
+
+// An axis that times out JOYPORT_DROP_AFTER polls in a row is left out of
+// the measurement (so a pulled plug does not cost the timeout every poll)
+// and tried again every JOYPORT_RECHECK_POLLS polls.
+#define JOYPORT_DROP_AFTER 3
+#define JOYPORT_RECHECK_POLLS 60
+
+// How the driver reaches the port: start the timers, read the port, and a
+// microsecond clock. read is also called inside the timer interrupt.
+typedef struct {
+ void (*trigger)(void);
+ uint8_t (*read)(void);
+ uint64_t (*micros)(void);
+} JoyPortHwT;
+
+// One measurement.
+typedef struct {
+ int32_t us[JOYPORT_AXES]; // microseconds until each axis fell
+ uint8_t done; // bit n: axis n fell before the timeout
+ uint8_t disturbed; // bit n: its fall came after a pause (not trusted)
+ uint8_t buttons; // bit n: button n down (A1 A2 B1 B2), read after the timing
+} JoyPortReadT;
+
+// Time the axes in mask (the others are not waited for).
+void joyPortMeasure(const JoyPortHwT *hw, uint8_t mask, JoyPortReadT *out);
+
+// Release what joyPortUse pinned (the module's destructor calls it, so
+// unloading the driver at a rescan leaves nothing locked).
+void joyPortRelease(void);
+
+// Use hw from now on (the DOS port, or simulated hardware in the tests);
+// pins what the timer interrupt touches.
+void joyPortUse(const JoyPortHwT *hw);
+
+#endif // JOY_PORT_H
diff --git a/src/libs/kpunch/joystick/drivers/joyport/joyPortDos.c b/src/libs/kpunch/joystick/drivers/joyport/joyPortDos.c
new file mode 100644
index 00000000..2040e034
--- /dev/null
+++ b/src/libs/kpunch/joystick/drivers/joyport/joyPortDos.c
@@ -0,0 +1,70 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joyPortDos.c -- the game port driver's DOS hardware access (port 201h
+// and the platform's microsecond clock), set up when the module loads.
+
+#include "joyPort.h"
+#include "dvxPlat.h"
+
+#include
+
+#define GAME_PORT 0x201
+#define READ_LOCK_BYTES 64 // portRead's code, pinned for the interrupt
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void joyPortDosFini(void) __attribute__((destructor));
+static void joyPortDosInit(void) __attribute__((constructor));
+static uint8_t portRead(void);
+static void portTrigger(void);
+
+static const JoyPortHwT sDosHw = {
+ portTrigger,
+ portRead,
+ platformMicros
+};
+
+
+static void joyPortDosFini(void) {
+ joyPortRelease();
+ platformUnlockRegion((const void *)portRead, READ_LOCK_BYTES, true);
+}
+
+
+static void joyPortDosInit(void) {
+ platformLockRegion((const void *)portRead, READ_LOCK_BYTES, true);
+ joyPortUse(&sDosHw);
+}
+
+
+static uint8_t portRead(void) {
+ return inportb(GAME_PORT);
+}
+
+
+// Any value written starts the four timers.
+static void portTrigger(void) {
+ outportb(GAME_PORT, 0);
+}
diff --git a/src/libs/kpunch/joystick/joyBas.c b/src/libs/kpunch/joystick/joyBas.c
new file mode 100644
index 00000000..55271c6d
--- /dev/null
+++ b/src/libs/kpunch/joystick/joyBas.c
@@ -0,0 +1,278 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joyBas.c -- DVX BASIC entry points of the joystick library
+//
+// Called through DECLARE LIBRARY (src/include/basic/joystick.bas), so the
+// signatures use only BASIC-marshalable types. Joystick, axis, button and
+// hat numbers start at 1. Each function reads through the calling
+// program's shared handle (joyOpenShared), which closes when the program
+// ends; currentAppId is the calling program while its BASIC code runs.
+// joyBasStick and joyBasStrig are QBasic's STICK and STRIG (joystick.h),
+// called by the BASIC runtime.
+
+#include "joyInt.h"
+#include "shellApp.h"
+
+#include
+
+#define BAS_TRUE (-1)
+#define BAS_FALSE 0
+
+// QBasic's joysticks A and B: two axes and two buttons each, positions
+// 1..200 (the classic game port's range)
+#define QB_STICK_AXES 2
+#define QB_LATCHED 4 // STICK(0)..STICK(3): A X, A Y, B X, B Y
+#define QB_POS_MIN 1
+#define QB_POS_MAX 200
+#define QB_STRIGS 8 // STRIG(0)..STRIG(7)
+#define QB_LATCH_SLOTS 32 // programs whose STICK(0) readings are kept
+
+// A program's positions as its last STICK(0) read them (appId 0: free).
+typedef struct {
+ int32_t appId;
+ int32_t pos[QB_LATCHED];
+} StickLatchT;
+
+static StickLatchT sLatches[QB_LATCH_SLOTS];
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t basHandle(int32_t joy);
+static bool basInfo(int32_t joy, JoyInfoT *info);
+static bool basState(int32_t joy, JoyStateT *st);
+int32_t JoyAxes(int32_t joy);
+int32_t JoyAxis(int32_t joy, int32_t axis);
+void joyBasForget(int32_t appId);
+int32_t joyBasStick(int32_t n);
+int32_t joyBasStrig(int32_t n);
+int32_t JoyButton(int32_t joy, int32_t button);
+int32_t JoyButtons(int32_t joy);
+int32_t JoyCount(void);
+int32_t JoyHat(int32_t joy, int32_t hat);
+const char *JoyName(int32_t joy);
+int32_t JoyX(int32_t joy);
+int32_t JoyY(int32_t joy);
+static StickLatchT *latchOf(int32_t appId, bool create);
+static bool qbStick(int32_t stick, int32_t *device, int32_t *first);
+static int32_t qbStickPos(int32_t stick, int32_t axis);
+
+
+// The calling program's handle on joystick joy (1-based), -1 if none.
+static int32_t basHandle(int32_t joy) {
+ return joyOpenShared(shellContext(), joy - 1);
+}
+
+
+static bool basInfo(int32_t joy, JoyInfoT *info) {
+ return joyGetInfo(joy - 1, info);
+}
+
+
+static bool basState(int32_t joy, JoyStateT *st) {
+ return joyRead(basHandle(joy), st);
+}
+
+
+int32_t JoyAxes(int32_t joy) {
+ JoyInfoT info;
+
+ return basInfo(joy, &info) ? info.axisCount : 0;
+}
+
+
+int32_t JoyAxis(int32_t joy, int32_t axis) {
+ JoyStateT st;
+
+ if (axis < 1 || axis > JOY_MAX_AXES || !basState(joy, &st)) {
+ return 0;
+ }
+
+ return st.axes[axis - 1];
+}
+
+
+void joyBasForget(int32_t appId) {
+ StickLatchT *l = latchOf(appId, false);
+
+ if (l) {
+ l->appId = 0;
+ }
+}
+
+
+// STICK(0) reads all four positions; the others give what it read.
+int32_t joyBasStick(int32_t n) {
+ if (n < 0 || n >= QB_LATCHED) {
+ return 0;
+ }
+
+ StickLatchT *l = latchOf(shellContext()->currentAppId, n == 0);
+
+ if (n == 0) {
+ int32_t pos[QB_LATCHED];
+
+ for (int32_t i = 0; i < QB_LATCHED; i++) {
+ pos[i] = qbStickPos(i / QB_STICK_AXES, i % QB_STICK_AXES);
+ }
+
+ if (l) {
+ memcpy(l->pos, pos, sizeof(pos));
+ }
+
+ return pos[0];
+ }
+
+ return l ? l->pos[n] : 0;
+}
+
+
+// n / 2: 0 A's button 1, 1 B's button 1, 2 A's button 2, 3 B's button 2;
+// even n: went down since, odd: down now.
+int32_t joyBasStrig(int32_t n) {
+ int32_t device = 0;
+ int32_t first = 0;
+ int32_t pair = n / 2;
+ JoyStateT st;
+
+ if (n < 0 || n >= QB_STRIGS || !qbStick(pair % 2, &device, &first)) {
+ return BAS_FALSE;
+ }
+
+ int32_t handle = joyOpenShared(shellContext(), device);
+ int32_t button = first + pair / 2;
+
+ if (n % 2 == 0) {
+ return joyButtonWentDown(handle, button) ? BAS_TRUE : BAS_FALSE;
+ }
+
+ return joyRead(handle, &st) && (st.buttons & (1U << button)) ? BAS_TRUE : BAS_FALSE;
+}
+
+
+int32_t JoyButton(int32_t joy, int32_t button) {
+ return joyButtonPressed(basHandle(joy), button - 1) ? BAS_TRUE : BAS_FALSE;
+}
+
+
+int32_t JoyButtons(int32_t joy) {
+ JoyInfoT info;
+
+ return basInfo(joy, &info) ? info.buttonCount : 0;
+}
+
+
+int32_t JoyCount(void) {
+ return joyCount();
+}
+
+
+int32_t JoyHat(int32_t joy, int32_t hat) {
+ JoyStateT st;
+
+ if (hat < 1 || hat > JOY_MAX_HATS || !basState(joy, &st)) {
+ return JOY_HAT_CENTERED;
+ }
+
+ return st.hats[hat - 1];
+}
+
+
+// The name is copied into a buffer that the next call reuses (the BASIC
+// runtime copies the string at once).
+const char *JoyName(int32_t joy) {
+ static JoyInfoT info;
+
+ return basInfo(joy, &info) ? info.name : "";
+}
+
+
+int32_t JoyX(int32_t joy) {
+ return JoyAxis(joy, 1);
+}
+
+
+int32_t JoyY(int32_t joy) {
+ return JoyAxis(joy, 2);
+}
+
+
+// The program's latch (a free one taken when create; NULL without).
+static StickLatchT *latchOf(int32_t appId, bool create) {
+ StickLatchT *spare = NULL;
+
+ for (int32_t i = 0; i < QB_LATCH_SLOTS; i++) {
+ if (sLatches[i].appId == appId) {
+ return &sLatches[i];
+ }
+
+ if (!spare && sLatches[i].appId == 0) {
+ spare = &sLatches[i];
+ }
+ }
+
+ if (!create || !spare) {
+ return NULL;
+ }
+
+ memset(spare, 0, sizeof(*spare));
+ spare->appId = appId;
+ return spare;
+}
+
+
+// QBasic joystick A (0) or B (1): the device, and its first axis and
+// button (B is the second device, or the first's second pair when it is
+// the only one and has more).
+static bool qbStick(int32_t stick, int32_t *device, int32_t *first) {
+ int32_t count = joyCount();
+ JoyInfoT info;
+
+ if (stick == 0 || count > 1) {
+ *device = stick;
+ *first = 0;
+ return count > stick;
+ }
+
+ *device = 0;
+ *first = QB_STICK_AXES;
+ return count == 1 && joyGetInfo(0, &info) && info.axisCount > QB_STICK_AXES;
+}
+
+
+// Position QB_POS_MIN..QB_POS_MAX of axis 0 (X) or 1 (Y) of joystick A or
+// B; 0 when there is no such joystick.
+static int32_t qbStickPos(int32_t stick, int32_t axis) {
+ int32_t device = 0;
+ int32_t first = 0;
+ JoyStateT st;
+
+ if (!qbStick(stick, &device, &first) || !joyRead(joyOpenShared(shellContext(), device), &st)) {
+ return 0;
+ }
+
+ int64_t v = (int64_t)st.axes[first + axis] - JOY_AXIS_MIN;
+
+ return (int32_t)(v * (QB_POS_MAX - QB_POS_MIN) / ((int64_t)JOY_AXIS_MAX - JOY_AXIS_MIN)) + QB_POS_MIN;
+}
diff --git a/src/libs/kpunch/joystick/joyInt.h b/src/libs/kpunch/joystick/joyInt.h
new file mode 100644
index 00000000..77d72d38
--- /dev/null
+++ b/src/libs/kpunch/joystick/joyInt.h
@@ -0,0 +1,38 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joyInt.h -- shared between the joystick library's own files (not part
+// of the SDK).
+
+#ifndef JOY_INT_H
+#define JOY_INT_H
+
+#include "joystick.h"
+
+// Call a program's event function as that program (joyShell.c): its app id
+// is current and the shell defers unloading it until the call returns.
+void joyCallApp(AppContextT *ctx, int32_t appId, JoyEventFnT fn, int32_t handle, const JoyEventT *evt, void *userData);
+
+// The program ended: forget its STICK(0) readings (joyBas.c).
+void joyBasForget(int32_t appId);
+
+#endif // JOY_INT_H
diff --git a/src/libs/kpunch/joystick/joyShell.c b/src/libs/kpunch/joystick/joyShell.c
new file mode 100644
index 00000000..c1335d10
--- /dev/null
+++ b/src/libs/kpunch/joystick/joyShell.c
@@ -0,0 +1,65 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joyShell.c -- the joystick library's ties to the shell: at load it
+// registers an idle handler (event delivery, joyPump) and an app cleanup
+// hook (a program's handles close when it ends, however it ends), and it
+// calls programs' event functions as those programs.
+
+#include "joyInt.h"
+#include "shellApp.h"
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void joyCallApp(AppContextT *ctx, int32_t appId, JoyEventFnT fn, int32_t handle, const JoyEventT *evt, void *userData);
+static void joyIdle(void *ctx);
+static void joyShellInit(void) __attribute__((constructor));
+
+
+void joyCallApp(AppContextT *ctx, int32_t appId, JoyEventFnT fn, int32_t handle, const JoyEventT *evt, void *userData) {
+ ShellAppT *app = shellGetApp(appId);
+ int32_t prevId = ctx->currentAppId;
+
+ ctx->currentAppId = appId;
+
+ if (app) {
+ SHELL_APP_CALL(app, fn(handle, evt, userData));
+ } else {
+ fn(handle, evt, userData);
+ }
+
+ ctx->currentAppId = prevId;
+}
+
+
+static void joyIdle(void *ctx) {
+ (void)ctx;
+ joyPump();
+}
+
+
+static void joyShellInit(void) {
+ shellRegisterIdle(joyIdle, NULL);
+ shellRegisterAppCleanup(joyAppCleanup);
+}
diff --git a/src/libs/kpunch/joystick/joystick.c b/src/libs/kpunch/joystick/joystick.c
new file mode 100644
index 00000000..8f6c91e8
--- /dev/null
+++ b/src/libs/kpunch/joystick/joystick.c
@@ -0,0 +1,1190 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joystick.c -- joystick library core: drivers, devices, calibration,
+// handles and events (see joystick.h).
+//
+// Devices are found the first time a program asks (ensureReady), after
+// the GUI and the timer service are running: the driver modules in
+// JOY_DRV_DIR are loaded and probed, and the ones that found nothing are
+// unloaded again. While a handle is open a
+// timer-service callback marks a poll due at the poll rate and runs the
+// drivers' tick hooks; the poll itself (which, for a game port, waits up to
+// a few milliseconds for the axis timers) runs in program context, from
+// joyRead / joyPump.
+
+#include "joystick.h"
+#include "joyInt.h"
+#include "dvxPlat.h"
+#include "thirdparty/stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+
+// ABI stamp checked by the loader (see DVX_ABI_VERSION).
+DVX_ABI_STAMP(joystick);
+
+// Automatic range of an axis never calibrated: min = centre / DIV and
+// max = centre * MUL to start with, widening as the stick moves.
+#define AUTO_MIN_DIV 8
+#define AUTO_MAX_MUL 2
+
+#define PERCENT 100
+#define SETTING_BUF 64
+#define CAL_FIELDS 3
+#define CAL_FMT "%" PRId32 ",%" PRId32 ",%" PRId32
+#define CAL_SCAN_FMT "%" SCNd32 ",%" SCNd32 ",%" SCNd32
+#define TICK_LOCK_BYTES 256 // pollTick's code, pinned for the interrupt
+
+// Set-1 make codes of the typing keys without a KEY_SCAN_* name
+#define SC_1 0x02
+#define SC_2 0x03
+#define SC_3 0x04
+#define SC_4 0x05
+#define SC_5 0x06
+#define SC_6 0x07
+#define SC_7 0x08
+#define SC_8 0x09
+#define SC_9 0x0A
+#define SC_0 0x0B
+#define SC_Q 0x10
+#define SC_W 0x11
+#define SC_E 0x12
+#define SC_R 0x13
+#define SC_T 0x14
+#define SC_Y 0x15
+#define SC_U 0x16
+#define SC_I 0x17
+#define SC_O 0x18
+#define SC_P 0x19
+#define SC_A 0x1E
+#define SC_S 0x1F
+#define SC_D 0x20
+#define SC_F 0x21
+#define SC_G 0x22
+#define SC_H 0x23
+#define SC_J 0x24
+#define SC_K 0x25
+#define SC_L 0x26
+#define SC_Z 0x2C
+#define SC_X 0x2D
+#define SC_C 0x2E
+#define SC_V 0x2F
+#define SC_B 0x30
+#define SC_N 0x31
+#define SC_M 0x32
+#define SC_KP5 0x4C
+
+typedef struct {
+ const JoyDriverT *drv;
+ int32_t unit;
+ JoyInfoT info;
+ JoyAxisCalT cal[JOY_MAX_AXES];
+ uint32_t calSet; // bit n: axis n calibrated from the settings
+ uint32_t autoSeeded; // bit n: axis n's automatic range has its centre
+ int32_t deadZone;
+ JoyRawT raw; // last measured values
+ JoyStateT state; // calibrated
+ uint8_t presses[JOY_MAX_BUTTONS]; // press count per button (wraps)
+} JoyDeviceT;
+
+typedef struct {
+ JoyEventFnT eventFn;
+ void *eventData;
+ int32_t owner;
+ int32_t device;
+ JoyStateT lastEvent; // what the events have told so far
+ uint8_t seenRead[JOY_MAX_BUTTONS]; // presses joyRead / joyButtonPressed reported
+ uint8_t seenEvent[JOY_MAX_BUTTONS]; // presses the events reported
+ uint8_t seenDown[JOY_MAX_BUTTONS]; // presses joyButtonWentDown reported
+ bool used;
+ bool shared;
+} JoyHandleT;
+
+typedef struct {
+ const char *name;
+ int32_t code;
+} JoyKeyT;
+
+// ============================================================
+// Tables
+// ============================================================
+
+static const JoyKeyT sKeys[] = {
+ { "Up", KEY_RAW_UP },
+ { "Down", KEY_RAW_DOWN },
+ { "Left", KEY_RAW_LEFT },
+ { "Right", KEY_RAW_RIGHT },
+ { "LCtrl", KEY_RAW_LCTRL },
+ { "RCtrl", KEY_RAW_RCTRL },
+ { "LShift", KEY_RAW_LSHIFT },
+ { "RShift", KEY_RAW_RSHIFT },
+ { "LAlt", KEY_RAW_LALT },
+ { "RAlt", KEY_RAW_RALT },
+ { "Space", KEY_SCAN_SPACE },
+ { "Enter", KEY_SCAN_ENTER },
+ { "Tab", KEY_SCAN_TAB },
+ { "Backspace", KEY_SCAN_BACKSPACE },
+ { "Insert", KEY_SCAN_INSERT | KEY_RAW_EXT },
+ { "Delete", KEY_SCAN_DELETE | KEY_RAW_EXT },
+ { "Home", KEY_SCAN_HOME | KEY_RAW_EXT },
+ { "End", KEY_SCAN_END | KEY_RAW_EXT },
+ { "PgUp", KEY_SCAN_PGUP | KEY_RAW_EXT },
+ { "PgDn", KEY_SCAN_PGDN | KEY_RAW_EXT },
+ { "Keypad0", KEY_SCAN_INSERT },
+ { "Keypad1", KEY_SCAN_END },
+ { "Keypad2", KEY_SCAN_DOWN },
+ { "Keypad3", KEY_SCAN_PGDN },
+ { "Keypad4", KEY_SCAN_LEFT },
+ { "Keypad5", SC_KP5 },
+ { "Keypad6", KEY_SCAN_RIGHT },
+ { "Keypad7", KEY_SCAN_HOME },
+ { "Keypad8", KEY_SCAN_UP },
+ { "Keypad9", KEY_SCAN_PGUP },
+ { "A", SC_A }, { "B", SC_B }, { "C", SC_C }, { "D", SC_D }, { "E", SC_E }, { "F", SC_F },
+ { "G", SC_G }, { "H", SC_H }, { "I", SC_I }, { "J", SC_J }, { "K", SC_K }, { "L", SC_L },
+ { "M", SC_M }, { "N", SC_N }, { "O", SC_O }, { "P", SC_P }, { "Q", SC_Q }, { "R", SC_R },
+ { "S", SC_S }, { "T", SC_T }, { "U", SC_U }, { "V", SC_V }, { "W", SC_W }, { "X", SC_X },
+ { "Y", SC_Y }, { "Z", SC_Z },
+ { "1", SC_1 }, { "2", SC_2 }, { "3", SC_3 }, { "4", SC_4 }, { "5", SC_5 },
+ { "6", SC_6 }, { "7", SC_7 }, { "8", SC_8 }, { "9", SC_9 }, { "0", SC_0 }
+};
+
+// ============================================================
+// Module state
+// ============================================================
+
+static DvxDriverModT *sMods = NULL; // drivers in use (desc NULL: unloaded)
+static void (*sTickFns[JOY_MAX_DRIVERS])(void); // read inside the interrupt
+static int32_t sTickCount = 0;
+static JoyDeviceT sDevices[JOY_MAX_DEVICES];
+static int32_t sDeviceCount = 0;
+static JoyHandleT sHandles[JOY_MAX_HANDLES];
+static int32_t sOpenCount = 0;
+static bool sProbed = false;
+static PrefsHandleT *sSettings = NULL;
+static AppContextT *sCtx = NULL;
+static int32_t sPollHz = JOY_POLL_HZ_DEFAULT;
+static int32_t sTimerId = -1;
+static bool sTickLocked = false;
+static bool sPumping = false;
+static volatile bool sPollDue = false;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void addUnits(const JoyDriverT *drv);
+static void applyState(JoyDeviceT *dev);
+static void autoRange(JoyDeviceT *dev, int32_t axis, int32_t raw);
+static bool axisEventDue(int32_t last, int32_t now);
+static int32_t clampAxis(int64_t v);
+static void copySection(PrefsHandleT *from, PrefsHandleT *to);
+static void deliverEvents(int32_t handle);
+static const JoyDriverT *driverAt(int32_t index);
+static bool driverUsed(const JoyDriverT *drv);
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind);
+static bool emit(int32_t handle, JoyEventKindE kind, int32_t index, int32_t value);
+static void ensureReady(void);
+static void ensureSettings(void);
+static JoyHandleT *handleAt(int32_t handle);
+static bool handleNeutral(const JoyHandleT *h);
+void joyAppCleanup(int32_t appId);
+int32_t joyAxisScale(const JoyAxisCalT *cal, int32_t raw);
+bool joyButtonPressed(int32_t handle, int32_t button);
+bool joyButtonWentDown(int32_t handle, int32_t button);
+void joyClose(int32_t handle);
+int32_t joyCount(void);
+int32_t joyDeadZoneApply(int32_t value, int32_t percent);
+bool joyGetCalibration(int32_t device, int32_t axis, JoyAxisCalT *out);
+int32_t joyGetDeadZone(int32_t device);
+bool joyGetInfo(int32_t device, JoyInfoT *out);
+bool joyGetRaw(int32_t device, JoyRawT *out);
+int32_t joyKeyCode(int32_t index);
+int32_t joyKeyCount(void);
+int32_t joyKeyFind(const char *name);
+const char *joyKeyName(int32_t index);
+const char *joyKeyNameOf(int32_t rawCode);
+int32_t joyOpen(AppContextT *ctx, int32_t device);
+int32_t joyOpenShared(AppContextT *ctx, int32_t device);
+void joyPump(void);
+bool joyRead(int32_t handle, JoyStateT *out);
+int32_t joyRescan(void);
+bool joySetCalibration(int32_t device, int32_t axis, const JoyAxisCalT *cal);
+bool joySetDeadZone(int32_t device, int32_t percent);
+bool joySetEventFn(int32_t handle, JoyEventFnT fn, void *userData);
+const char *joySettingGet(const char *key, const char *defaultVal);
+void joySettingSet(const char *key, const char *value);
+void joySettingsLoad(PrefsHandleT *prefs);
+void joySettingsStore(PrefsHandleT *prefs);
+static void loadDeviceSettings(JoyDeviceT *dev);
+static void mergeRaw(JoyDeviceT *dev, const JoyRawT *r);
+static void neutralState(JoyStateT *st);
+static bool ownerFocused(int32_t owner);
+static void pollAll(void);
+static void pollIfDue(void);
+static void pollTick(void *userData);
+static void probeAll(void);
+static void probeDrivers(bool fallback);
+static void settingsApplied(void);
+static void startPolling(void);
+static void stopPolling(void);
+static bool validCalAxis(int32_t device, int32_t axis);
+static void viewOf(JoyHandleT *h, JoyStateT *out);
+
+// What the library looks for in JOY_DRV_DIR
+static const char *const sDrvSymbols[] = { DVX_SYM(JOY_DRV_SYMBOL) };
+static const uint32_t sDrvSizes[] = { sizeof(JoyDriverT) };
+static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, 1, driverValid };
+
+
+// Add every unit a driver finds (as far as the table goes).
+static void addUnits(const JoyDriverT *drv) {
+ int32_t units = drv->probe();
+
+ for (int32_t u = 0; u < units && sDeviceCount < JOY_MAX_DEVICES; u++) {
+ JoyDeviceT *dev = &sDevices[sDeviceCount];
+
+ memset(dev, 0, sizeof(*dev));
+ dev->drv = drv;
+ dev->unit = u;
+ drv->info(u, &dev->info);
+ snprintf(dev->info.id, sizeof(dev->info.id), "%s%" PRId32, drv->hdr.name, u);
+ neutralState(&dev->state);
+ loadDeviceSettings(dev);
+ sDeviceCount++;
+ dvxLog("Joystick: %s: %s (%" PRId32 " axes, %" PRId32 " buttons, %" PRId32 " hats)", dev->info.id, dev->info.name, dev->info.axisCount, dev->info.buttonCount, dev->info.hatCount);
+ }
+}
+
+
+// The calibrated state from the raw values. Axes that are missing or
+// were never measured read 0; a device that stopped answering reads
+// neutral.
+static void applyState(JoyDeviceT *dev) {
+ JoyStateT *st = &dev->state;
+
+ neutralState(st);
+ st->connected = dev->raw.connected;
+
+ if (!st->connected) {
+ return;
+ }
+
+ for (int32_t a = 0; a < dev->info.axisCount; a++) {
+ uint32_t bit = 1U << a;
+
+ if (!(dev->info.axisPresent & dev->raw.axisValid & bit)) {
+ continue;
+ }
+
+ int32_t v = dev->raw.axes[a];
+
+ if (dev->info.caps & JOY_CAP_CALIBRATE) {
+ if (!(dev->calSet & bit)) {
+ autoRange(dev, a, v);
+ }
+
+ v = joyAxisScale(&dev->cal[a], v);
+ }
+
+ st->axes[a] = joyDeadZoneApply(v, dev->deadZone);
+ }
+
+ memcpy(st->hats, dev->raw.hats, sizeof(int32_t) * (size_t)dev->info.hatCount);
+ st->buttons = dev->raw.buttons;
+}
+
+
+// The automatic range: centred on the first reading, widened by every
+// reading beyond it.
+static void autoRange(JoyDeviceT *dev, int32_t axis, int32_t raw) {
+ JoyAxisCalT *c = &dev->cal[axis];
+ uint32_t bit = 1U << axis;
+
+ if (!(dev->autoSeeded & bit)) {
+ c->center = raw;
+ c->min = raw / AUTO_MIN_DIV;
+ c->max = raw * AUTO_MAX_MUL;
+ dev->autoSeeded |= bit;
+ }
+
+ if (raw < c->min) {
+ c->min = raw;
+ }
+
+ if (raw > c->max) {
+ c->max = raw;
+ }
+}
+
+
+static bool axisEventDue(int32_t last, int32_t now) {
+ if (now == last) {
+ return false;
+ }
+
+ return now == 0 || now == JOY_AXIS_MIN || now == JOY_AXIS_MAX || abs(now - last) >= JOY_EVENT_AXIS_STEP;
+}
+
+
+static int32_t clampAxis(int64_t v) {
+ if (v < JOY_AXIS_MIN) {
+ return JOY_AXIS_MIN;
+ }
+
+ if (v > JOY_AXIS_MAX) {
+ return JOY_AXIS_MAX;
+ }
+
+ return (int32_t)v;
+}
+
+
+// Make to's [joystick] section equal from's: keys only in to go, every key
+// of from is set (changed keys keep their place in the file).
+static void copySection(PrefsHandleT *from, PrefsHandleT *to) {
+ const char *name;
+ int32_t i = 0;
+
+ while ((name = prefsKeyName(to, JOY_INI_SECTION, i)) != NULL) {
+ if (prefsGetString(from, JOY_INI_SECTION, name, NULL)) {
+ i++;
+ continue;
+ }
+
+ char key[SETTING_BUF];
+
+ snprintf(key, sizeof(key), "%s", name);
+ prefsRemove(to, JOY_INI_SECTION, key);
+ }
+
+ for (i = 0; (name = prefsKeyName(from, JOY_INI_SECTION, i)) != NULL; i++) {
+ prefsSetString(to, JOY_INI_SECTION, name, prefsGetString(from, JOY_INI_SECTION, name, ""));
+ }
+}
+
+
+// Tell the handle's event function what changed since the last events.
+// Stops when the function closes the handle or turns its events off.
+static void deliverEvents(int32_t handle) {
+ JoyHandleT *h = &sHandles[handle];
+ const JoyDeviceT *dev = &sDevices[h->device];
+ bool neutral = handleNeutral(h);
+ JoyStateT now;
+
+ if (neutral) {
+ neutralState(&now);
+ } else {
+ now = dev->state;
+ }
+
+ for (int32_t a = 0; a < dev->info.axisCount; a++) {
+ if (axisEventDue(h->lastEvent.axes[a], now.axes[a])) {
+ h->lastEvent.axes[a] = now.axes[a];
+
+ if (!emit(handle, JoyEventAxisE, a, now.axes[a])) {
+ return;
+ }
+ }
+ }
+
+ for (int32_t i = 0; i < dev->info.hatCount; i++) {
+ if (h->lastEvent.hats[i] != now.hats[i]) {
+ h->lastEvent.hats[i] = now.hats[i];
+
+ if (!emit(handle, JoyEventHatE, i, now.hats[i])) {
+ return;
+ }
+ }
+ }
+
+ for (int32_t b = 0; b < dev->info.buttonCount; b++) {
+ uint32_t bit = 1U << b;
+ bool down = (now.buttons & bit) != 0;
+ bool was = (h->lastEvent.buttons & bit) != 0;
+ bool freshHit = !neutral && dev->presses[b] != h->seenEvent[b];
+
+ h->seenEvent[b] = dev->presses[b];
+ h->lastEvent.buttons = (h->lastEvent.buttons & ~bit) | (down ? bit : 0);
+
+ // A press since the last events: report it even when the button
+ // is up again (or was down before and went up and down in between).
+ if (freshHit) {
+ if (was && !emit(handle, JoyEventButtonUpE, b, 0)) {
+ return;
+ }
+
+ if (!emit(handle, JoyEventButtonDownE, b, 0)) {
+ return;
+ }
+
+ was = true;
+ }
+
+ if (down != was && !emit(handle, down ? JoyEventButtonDownE : JoyEventButtonUpE, b, 0)) {
+ return;
+ }
+ }
+}
+
+
+// The driver at sMods[index] (NULL once unloaded).
+static const JoyDriverT *driverAt(int32_t index) {
+ return (const JoyDriverT *)sMods[index].desc;
+}
+
+
+static bool driverUsed(const JoyDriverT *drv) {
+ for (int32_t i = 0; i < sDeviceCount; i++) {
+ if (sDevices[i].drv == drv) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// The library's own check of a driver module's descriptor.
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) {
+ const JoyDriverT *drv = (const JoyDriverT *)desc;
+
+ (void)kind;
+ return drv->probe && drv->info && drv->poll;
+}
+
+
+// Call the handle's event function; false when it closed the handle or
+// stopped its events.
+static bool emit(int32_t handle, JoyEventKindE kind, int32_t index, int32_t value) {
+ JoyHandleT *h = &sHandles[handle];
+ JoyEventT evt;
+
+ evt.kind = kind;
+ evt.device = h->device;
+ evt.index = index;
+ evt.value = value;
+ joyCallApp(sCtx, h->owner, h->eventFn, handle, &evt, h->eventData);
+ return h->used && h->eventFn;
+}
+
+
+// Devices found and settings read, the first time anything asks.
+static void ensureReady(void) {
+ if (sProbed) {
+ return;
+ }
+
+ ensureSettings();
+ probeAll();
+}
+
+
+// The [joystick] section of DVX.INI, read once.
+static void ensureSettings(void) {
+ if (sSettings) {
+ return;
+ }
+
+ PrefsHandleT *ini = prefsLoad(DVX_INI_PATH);
+
+ sSettings = prefsCreate();
+
+ if (ini) {
+ copySection(ini, sSettings);
+ prefsClose(ini);
+ }
+
+ settingsApplied();
+}
+
+
+static JoyHandleT *handleAt(int32_t handle) {
+ if (handle < 0 || handle >= JOY_MAX_HANDLES || !sHandles[handle].used) {
+ return NULL;
+ }
+
+ return &sHandles[handle];
+}
+
+
+// A keyboard device reads neutral for a program without the focus.
+static bool handleNeutral(const JoyHandleT *h) {
+ return (sDevices[h->device].info.caps & JOY_CAP_KEYBOARD) && !ownerFocused(h->owner);
+}
+
+
+void joyAppCleanup(int32_t appId) {
+ if (appId < 1) {
+ return;
+ }
+
+ joyBasForget(appId);
+
+ for (int32_t i = 0; i < JOY_MAX_HANDLES; i++) {
+ if (sHandles[i].used && sHandles[i].owner == appId) {
+ joyClose(i);
+ }
+ }
+}
+
+
+int32_t joyAxisScale(const JoyAxisCalT *cal, int32_t raw) {
+ int64_t span;
+
+ if (raw <= cal->center) {
+ span = (int64_t)cal->center - cal->min;
+
+ if (span <= 0) {
+ return 0;
+ }
+
+ return clampAxis(((int64_t)raw - cal->center) * -(int64_t)JOY_AXIS_MIN / span);
+ }
+
+ span = (int64_t)cal->max - cal->center;
+
+ if (span <= 0) {
+ return 0;
+ }
+
+ return clampAxis(((int64_t)raw - cal->center) * JOY_AXIS_MAX / span);
+}
+
+
+bool joyButtonPressed(int32_t handle, int32_t button) {
+ JoyHandleT *h = handleAt(handle);
+
+ if (!h || button < 0 || button >= JOY_MAX_BUTTONS) {
+ return false;
+ }
+
+ pollIfDue();
+
+ const JoyDeviceT *dev = &sDevices[h->device];
+ bool fresh = dev->presses[button] != h->seenRead[button];
+
+ h->seenRead[button] = dev->presses[button];
+ return !handleNeutral(h) && (fresh || (dev->state.buttons & (1U << button)) != 0);
+}
+
+
+bool joyButtonWentDown(int32_t handle, int32_t button) {
+ JoyHandleT *h = handleAt(handle);
+
+ if (!h || button < 0 || button >= JOY_MAX_BUTTONS) {
+ return false;
+ }
+
+ pollIfDue();
+
+ const JoyDeviceT *dev = &sDevices[h->device];
+ bool fresh = dev->presses[button] != h->seenDown[button];
+
+ h->seenDown[button] = dev->presses[button];
+ return !handleNeutral(h) && fresh;
+}
+
+
+void joyClose(int32_t handle) {
+ JoyHandleT *h = handleAt(handle);
+
+ if (!h) {
+ return;
+ }
+
+ h->used = false;
+ sOpenCount--;
+
+ if (sOpenCount == 0) {
+ stopPolling();
+ }
+}
+
+
+int32_t joyCount(void) {
+ ensureReady();
+ return sDeviceCount;
+}
+
+
+int32_t joyDeadZoneApply(int32_t value, int32_t percent) {
+ int64_t v = clampAxis(value);
+ int64_t pct = percent < 0 ? 0 : (percent > JOY_DEADZONE_MAX ? JOY_DEADZONE_MAX : percent);
+ int64_t end = v >= 0 ? JOY_AXIS_MAX : -(int64_t)JOY_AXIS_MIN;
+ int64_t mag = v >= 0 ? v : -v;
+ int64_t dz = pct * end / PERCENT;
+
+ if (mag <= dz) {
+ return 0;
+ }
+
+ mag = (mag - dz) * end / (end - dz);
+ return clampAxis(v >= 0 ? mag : -mag);
+}
+
+
+bool joyGetCalibration(int32_t device, int32_t axis, JoyAxisCalT *out) {
+ if (!validCalAxis(device, axis)) {
+ return false;
+ }
+
+ *out = sDevices[device].cal[axis];
+ return true;
+}
+
+
+int32_t joyGetDeadZone(int32_t device) {
+ ensureReady();
+
+ if (device < 0 || device >= sDeviceCount) {
+ return -1;
+ }
+
+ return sDevices[device].deadZone;
+}
+
+
+bool joyGetInfo(int32_t device, JoyInfoT *out) {
+ ensureReady();
+
+ if (device < 0 || device >= sDeviceCount) {
+ return false;
+ }
+
+ *out = sDevices[device].info;
+ return true;
+}
+
+
+bool joyGetRaw(int32_t device, JoyRawT *out) {
+ ensureReady();
+
+ if (device < 0 || device >= sDeviceCount) {
+ return false;
+ }
+
+ pollIfDue();
+ *out = sDevices[device].raw;
+ return true;
+}
+
+
+int32_t joyKeyCode(int32_t index) {
+ if (index < 0 || index >= DVX_ARRAY_LEN(sKeys)) {
+ return -1;
+ }
+
+ return sKeys[index].code;
+}
+
+
+int32_t joyKeyCount(void) {
+ return DVX_ARRAY_LEN(sKeys);
+}
+
+
+int32_t joyKeyFind(const char *name) {
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sKeys); i++) {
+ if (strcasecmp(sKeys[i].name, name) == 0) {
+ return sKeys[i].code;
+ }
+ }
+
+ return -1;
+}
+
+
+const char *joyKeyName(int32_t index) {
+ if (index < 0 || index >= DVX_ARRAY_LEN(sKeys)) {
+ return NULL;
+ }
+
+ return sKeys[index].name;
+}
+
+
+const char *joyKeyNameOf(int32_t rawCode) {
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sKeys); i++) {
+ if (sKeys[i].code == rawCode) {
+ return sKeys[i].name;
+ }
+ }
+
+ return NULL;
+}
+
+
+int32_t joyOpen(AppContextT *ctx, int32_t device) {
+ ensureReady();
+
+ if (device < 0 || device >= sDeviceCount) {
+ return -1;
+ }
+
+ for (int32_t i = 0; i < JOY_MAX_HANDLES; i++) {
+ JoyHandleT *h = &sHandles[i];
+
+ if (h->used) {
+ continue;
+ }
+
+ memset(h, 0, sizeof(*h));
+ h->used = true;
+ h->owner = ctx->currentAppId;
+ h->device = device;
+ sCtx = ctx;
+ sOpenCount++;
+
+ if (sOpenCount == 1) {
+ startPolling();
+ }
+
+ pollIfDue();
+ memcpy(h->seenRead, sDevices[device].presses, sizeof(h->seenRead));
+ memcpy(h->seenDown, sDevices[device].presses, sizeof(h->seenDown));
+ joySetEventFn(i, NULL, NULL);
+ return i;
+ }
+
+ return -1;
+}
+
+
+int32_t joyOpenShared(AppContextT *ctx, int32_t device) {
+ for (int32_t i = 0; i < JOY_MAX_HANDLES; i++) {
+ const JoyHandleT *h = &sHandles[i];
+
+ if (h->used && h->shared && h->owner == ctx->currentAppId && h->device == device) {
+ return i;
+ }
+ }
+
+ int32_t handle = joyOpen(ctx, device);
+
+ if (handle >= 0) {
+ sHandles[handle].shared = true;
+ }
+
+ return handle;
+}
+
+
+// Not reentrant: an event function that runs a modal loop (which pumps
+// again) gets its remaining events after it returns.
+void joyPump(void) {
+ if (sOpenCount == 0 || sPumping) {
+ return;
+ }
+
+ sPumping = true;
+ pollIfDue();
+
+ for (int32_t i = 0; i < JOY_MAX_HANDLES; i++) {
+ if (sHandles[i].used && sHandles[i].eventFn) {
+ deliverEvents(i);
+ }
+ }
+
+ sPumping = false;
+}
+
+
+bool joyRead(int32_t handle, JoyStateT *out) {
+ JoyHandleT *h = handleAt(handle);
+
+ if (!h) {
+ return false;
+ }
+
+ pollIfDue();
+ viewOf(h, out);
+ return true;
+}
+
+
+int32_t joyRescan(void) {
+ ensureSettings();
+
+ if (sOpenCount > 0) {
+ return -1;
+ }
+
+ probeAll();
+ return sDeviceCount;
+}
+
+
+bool joySetCalibration(int32_t device, int32_t axis, const JoyAxisCalT *cal) {
+ char key[SETTING_BUF];
+ char val[SETTING_BUF];
+
+ if (!validCalAxis(device, axis) || (cal && !(cal->min < cal->center && cal->center < cal->max))) {
+ return false;
+ }
+
+ JoyDeviceT *dev = &sDevices[device];
+
+ snprintf(key, sizeof(key), JOY_KEY_AXIS_FMT, dev->info.id, axis);
+
+ if (!cal) {
+ joySettingSet(key, NULL);
+ dvxLog("Joystick: %s axis %" PRId32 " ranges automatically", dev->info.id, axis);
+ return true;
+ }
+
+ snprintf(val, sizeof(val), CAL_FMT, cal->min, cal->center, cal->max);
+ joySettingSet(key, val);
+ dvxLog("Joystick: %s axis %" PRId32 " calibrated %s", dev->info.id, axis, val);
+ return true;
+}
+
+
+bool joySetDeadZone(int32_t device, int32_t percent) {
+ char key[SETTING_BUF];
+ char val[SETTING_BUF];
+
+ ensureReady();
+
+ if (device < 0 || device >= sDeviceCount || percent < 0 || percent > JOY_DEADZONE_MAX) {
+ return false;
+ }
+
+ snprintf(key, sizeof(key), JOY_KEY_DEADZONE_FMT, sDevices[device].info.id);
+ snprintf(val, sizeof(val), "%" PRId32, percent);
+ joySettingSet(key, val);
+ return true;
+}
+
+
+// Events report changes from now on.
+bool joySetEventFn(int32_t handle, JoyEventFnT fn, void *userData) {
+ JoyHandleT *h = handleAt(handle);
+
+ if (!h) {
+ return false;
+ }
+
+ h->eventFn = fn;
+ h->eventData = userData;
+
+ if (handleNeutral(h)) {
+ neutralState(&h->lastEvent);
+ } else {
+ h->lastEvent = sDevices[h->device].state;
+ }
+
+ memcpy(h->seenEvent, sDevices[h->device].presses, sizeof(h->seenEvent));
+ return true;
+}
+
+
+const char *joySettingGet(const char *key, const char *defaultVal) {
+ ensureSettings();
+ return prefsGetString(sSettings, JOY_INI_SECTION, key, defaultVal);
+}
+
+
+void joySettingSet(const char *key, const char *value) {
+ ensureSettings();
+
+ if (value) {
+ prefsSetString(sSettings, JOY_INI_SECTION, key, value);
+ } else {
+ prefsRemove(sSettings, JOY_INI_SECTION, key);
+ }
+
+ settingsApplied();
+}
+
+
+void joySettingsLoad(PrefsHandleT *prefs) {
+ ensureSettings();
+ copySection(prefs, sSettings);
+ settingsApplied();
+}
+
+
+void joySettingsStore(PrefsHandleT *prefs) {
+ ensureSettings();
+ copySection(sSettings, prefs);
+}
+
+
+// A device's calibrations and dead zone from the settings. An axis whose
+// calibration went away ranges automatically again from its next reading.
+static void loadDeviceSettings(JoyDeviceT *dev) {
+ char key[SETTING_BUF];
+ uint32_t wasSet = dev->calSet;
+
+ dev->calSet = 0;
+
+ for (int32_t a = 0; a < dev->info.axisCount; a++) {
+ int32_t min;
+ int32_t center;
+ int32_t max;
+
+ snprintf(key, sizeof(key), JOY_KEY_AXIS_FMT, dev->info.id, a);
+
+ const char *val = prefsGetString(sSettings, JOY_INI_SECTION, key, NULL);
+
+ if (val && sscanf(val, CAL_SCAN_FMT, &min, ¢er, &max) == CAL_FIELDS && min < center && center < max) {
+ dev->cal[a].min = min;
+ dev->cal[a].center = center;
+ dev->cal[a].max = max;
+ dev->calSet |= 1U << a;
+ }
+ }
+
+ dev->autoSeeded &= ~(wasSet & ~dev->calSet);
+ snprintf(key, sizeof(key), JOY_KEY_DEADZONE_FMT, dev->info.id);
+
+ int32_t dz = prefsGetInt(sSettings, JOY_INI_SECTION, key, JOY_DEADZONE_DEFAULT);
+
+ dev->deadZone = dz < 0 ? 0 : (dz > JOY_DEADZONE_MAX ? JOY_DEADZONE_MAX : dz);
+}
+
+
+// Fold a driver's poll into the device: measured axes, buttons (a press
+// seen by the driver's tick or as an edge here counts once) and hats.
+static void mergeRaw(JoyDeviceT *dev, const JoyRawT *r) {
+ uint32_t edges = r->pressed | (r->buttons & ~dev->raw.buttons);
+
+ for (int32_t a = 0; a < JOY_MAX_AXES; a++) {
+ if (r->axisValid & (1U << a)) {
+ dev->raw.axes[a] = r->axes[a];
+ }
+ }
+
+ memcpy(dev->raw.hats, r->hats, sizeof(dev->raw.hats));
+ dev->raw.axisValid |= r->axisValid;
+ dev->raw.buttons = r->buttons;
+ dev->raw.connected = r->connected;
+
+ for (int32_t b = 0; b < JOY_MAX_BUTTONS; b++) {
+ if (edges & (1U << b)) {
+ dev->presses[b]++;
+ }
+ }
+
+ applyState(dev);
+}
+
+
+static void neutralState(JoyStateT *st) {
+ memset(st, 0, sizeof(*st));
+
+ for (int32_t i = 0; i < JOY_MAX_HATS; i++) {
+ st->hats[i] = JOY_HAT_CENTERED;
+ }
+}
+
+
+// One of owner's windows has the keyboard focus.
+static bool ownerFocused(int32_t owner) {
+ return sCtx && sCtx->stack.focusedIdx >= 0 && sCtx->stack.windows[sCtx->stack.focusedIdx]->appId == owner;
+}
+
+
+// Every driver reads all its units in one call (a game port times all its
+// axes at once). A driver's units are next to each other in sDevices.
+static void pollAll(void) {
+ JoyRawT raw[JOY_MAX_DEVICES];
+ int32_t first = 0;
+
+ while (first < sDeviceCount) {
+ const JoyDriverT *drv = sDevices[first].drv;
+ int32_t units = 0;
+
+ while (first + units < sDeviceCount && sDevices[first + units].drv == drv) {
+ units++;
+ }
+
+ memset(raw, 0, sizeof(raw[0]) * (size_t)units);
+ drv->poll(raw, units);
+
+ for (int32_t u = 0; u < units; u++) {
+ mergeRaw(&sDevices[first + u], &raw[u]);
+ }
+
+ first += units;
+ }
+}
+
+
+// Poll when the timer said so, or always when there is no timer (no
+// handle open, or the timer service is not running).
+static void pollIfDue(void) {
+ if (sTimerId >= 0 && !sPollDue) {
+ return;
+ }
+
+ sPollDue = false;
+ pollAll();
+}
+
+
+// INSIDE THE TIMER INTERRUPT: mark a poll due and let the drivers sample.
+static void pollTick(void *userData) {
+ (void)userData;
+ sPollDue = true;
+
+ for (int32_t i = 0; i < sTickCount; i++) {
+ sTickFns[i]();
+ }
+}
+
+
+// Find the devices: load the drivers (again: a controller may have been
+// plugged in or a driver added), take every driver's units, then the
+// fallback drivers' when the fallback setting asks for them, unload the
+// drivers that found nothing and read the devices once (the automatic
+// ranges centre on that reading). Never while a handle is open, so the
+// timer never calls into a driver being unloaded.
+static void probeAll(void) {
+ const char *policy = joySettingGet(JOY_KEY_FALLBACK, JOY_FALLBACK_AUTO);
+
+ sDeviceCount = 0;
+ sTickCount = 0;
+ dvxDriversFree(sMods);
+ sMods = dvxDriversLoad(JOY_DRV_DIR, &sDrvClass);
+ probeDrivers(false);
+
+ if (strcasecmp(policy, JOY_FALLBACK_ALWAYS) == 0 || (sDeviceCount == 0 && strcasecmp(policy, JOY_FALLBACK_NEVER) != 0)) {
+ probeDrivers(true);
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sMods); i++) {
+ const JoyDriverT *drv = driverAt(i);
+
+ if (!driverUsed(drv)) {
+ dvxLog("Joystick: driver '%s' not used; unloaded", drv->hdr.name);
+ dvxDriverUnload(&sMods[i]);
+ } else if (drv->tick && sTickCount < JOY_MAX_DRIVERS) {
+ sTickFns[sTickCount] = drv->tick;
+ sTickCount++;
+ }
+ }
+
+ sProbed = true;
+ dvxLog("Joystick: %" PRId32 " device(s) found", sDeviceCount);
+ pollAll();
+}
+
+
+// Add the units of every fallback driver, or of every other one.
+static void probeDrivers(bool fallback) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sMods); i++) {
+ const JoyDriverT *drv = driverAt(i);
+
+ if (((drv->flags & JOY_DRIVER_FALLBACK) != 0) == fallback) {
+ addUnits(drv);
+ }
+ }
+}
+
+
+// The settings changed: poll rate, drivers' own settings, every device's
+// calibration and dead zone.
+static void settingsApplied(void) {
+ int32_t hz = prefsGetInt(sSettings, JOY_INI_SECTION, JOY_KEY_POLL_HZ, JOY_POLL_HZ_DEFAULT);
+
+ hz = hz < JOY_POLL_HZ_MIN ? JOY_POLL_HZ_MIN : (hz > JOY_POLL_HZ_MAX ? JOY_POLL_HZ_MAX : hz);
+
+ if (hz != sPollHz) {
+ sPollHz = hz;
+
+ if (sTimerId >= 0) {
+ stopPolling();
+ startPolling();
+ }
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sMods); i++) {
+ const JoyDriverT *drv = driverAt(i);
+
+ if (drv && drv->settingsChanged) {
+ drv->settingsChanged();
+ }
+ }
+
+ for (int32_t i = 0; i < sDeviceCount; i++) {
+ loadDeviceSettings(&sDevices[i]);
+ applyState(&sDevices[i]);
+ }
+}
+
+
+// The poll timer (first handle opened). What the interrupt touches is
+// pinned once.
+static void startPolling(void) {
+ if (!sTickLocked) {
+ platformLockRegion((const void *)pollTick, TICK_LOCK_BYTES, true);
+ platformLockRegion((const void *)&sPollDue, sizeof(sPollDue), false);
+ platformLockRegion((const void *)sTickFns, sizeof(sTickFns), false);
+ platformLockRegion((const void *)&sTickCount, sizeof(sTickCount), false);
+ sTickLocked = true;
+ }
+
+ sTimerId = platformTimerAdd((uint32_t)sPollHz, pollTick, NULL, 0);
+ sPollDue = true;
+}
+
+
+static void stopPolling(void) {
+ platformTimerRemove(sTimerId);
+ sTimerId = -1;
+}
+
+
+static bool validCalAxis(int32_t device, int32_t axis) {
+ ensureReady();
+
+ return device >= 0 && device < sDeviceCount && axis >= 0 && axis < sDevices[device].info.axisCount && (sDevices[device].info.caps & JOY_CAP_CALIBRATE);
+}
+
+
+// What a handle reads: the device's state (neutral for a keyboard device
+// without the focus) plus the presses it has not seen yet.
+static void viewOf(JoyHandleT *h, JoyStateT *out) {
+ const JoyDeviceT *dev = &sDevices[h->device];
+ bool neutral = handleNeutral(h);
+
+ if (neutral) {
+ neutralState(out);
+ out->connected = dev->state.connected;
+ } else {
+ *out = dev->state;
+ }
+
+ for (int32_t b = 0; b < JOY_MAX_BUTTONS && !neutral; b++) {
+ if (dev->presses[b] != h->seenRead[b]) {
+ out->buttons |= 1U << b;
+ }
+ }
+
+ memcpy(h->seenRead, dev->presses, sizeof(h->seenRead));
+}
diff --git a/src/libs/kpunch/joystick/joystick.dep b/src/libs/kpunch/joystick/joystick.dep
new file mode 100644
index 00000000..46fd48c5
--- /dev/null
+++ b/src/libs/kpunch/joystick/joystick.dep
@@ -0,0 +1,2 @@
+libdvx
+dvxshell
diff --git a/src/libs/kpunch/joystick/joystick.dhs b/src/libs/kpunch/joystick/joystick.dhs
new file mode 100644
index 00000000..12d8c863
--- /dev/null
+++ b/src/libs/kpunch/joystick/joystick.dhs
@@ -0,0 +1,156 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+.section Technical Reference (for Developers)|Libraries
+.topic lib.joystick
+.title Joystick Library
+.toc 0 Joystick Library
+.index joystick
+.index joystick.h
+.index joyCount
+.index joyGetInfo
+.index joyOpen
+.index joyOpenShared
+.index joyClose
+.index joyRead
+.index joyButtonPressed
+.index joyButtonWentDown
+.index joyBasStick
+.index STICK
+.index STRIG
+.index joySetEventFn
+.index joyPump
+.index joyRescan
+.index joyGetRaw
+.index joyGetCalibration
+.index joySetCalibration
+.index joyGetDeadZone
+.index joySetDeadZone
+.index joySettingGet
+.index joySettingSet
+.index joySettingsLoad
+.index joySettingsStore
+.index joyAxisScale
+.index joyDeadZoneApply
+.index JOY_DRIVER
+.index JoyStateT
+.index JoyInfoT
+.index JoyEventT
+.index JoyRawT
+.index JoyDriverT
+.index JoyAxisCalT
+.index joystick.bas
+.index game port
+
+.h1 Joystick Library
+
+Game controllers for DVX programs behind one API, whatever the hardware: the devices the drivers found, axes calibrated to -32768..32767 with a dead zone round the centre, buttons, hats, read when the program wants (polled) or delivered as events. A program's handles close when it ends, however it ends. Calibration and the other settings live in DVX.INI [joystick]; the Control Panel's Game Controllers page sets them.
+
+Header: joystick/joystick.h
+
+Loaded as: bin/libs/kpunch/joystick/joystick.lib, drivers in bin/libs/kpunch/joystick/drivers/*.drv (joyport.drv, joykeys.drv), loaded only when a program first asks for devices.
+
+.h2 Reading a Joystick
+
+.code
+if (joyCount() > 0) {
+ int32_t stick = joyOpen(ctx, 0); // device 0, owned by the calling program
+ JoyStateT st;
+
+ if (joyRead(stick, &st) && st.connected) {
+ shipX += st.axes[0] / 4096; // -8..7
+ firing = (st.buttons & 1) != 0;
+ }
+}
+.endcode
+
+joyCount() is the number of devices (the first call of any joystick function finds them); joyGetInfo(device, &info) describes one: name, id (the driver's name and unit number, "gameport0", which prefixes the device's DVX.INI keys), axisCount, buttonCount, hatCount, axisPresent (bit n set when axis n answers; a missing axis reads 0) and caps (JOY_CAP_CALIBRATE for analog axes, JOY_CAP_KEYBOARD for the keyboard joystick).
+
+joyOpen(ctx, device) returns a handle owned by ctx->currentAppId, or -1. joyOpenShared(ctx, device) returns the calling program's shared handle on the device, opening it the first time (for code with no place to keep a handle; the BASIC functions use it). joyClose closes a handle; a program's handles are closed for it when it ends.
+
+joyRead(handle, &st) fills a JoyStateT: axes[] (JOY_AXIS_MIN..JOY_AXIS_MAX, 0 inside the dead zone), hats[] (JOY_HAT_CENTERED or degrees clockwise from up), buttons (bit n = button n) and connected (false when the device stopped answering; everything then reads neutral). A button that went down and up again since the handle last looked reads as down once, so a quick tap between two reads is not lost. joyButtonPressed(handle, button) asks the same about one button. joyButtonWentDown(handle, button) only asks whether the button went down since the last time this function asked about it (a press counter of its own, apart from joyRead's).
+
+The keyboard joystick (JOY_CAP_KEYBOARD) reads neutral for a handle whose program has no window with the focus, so keys typed into another program do not steer the game.
+
+.h2 Events
+
+joySetEventFn(handle, fn, userData) delivers the device's changes from now on: JoyEventAxisE when an axis moved by at least JOY_EVENT_AXIS_STEP or reached the centre or an end, JoyEventButtonDownE / JoyEventButtonUpE (a tap between two polls gives both), JoyEventHatE. fn runs in program context as the handle's program (ctx->currentAppId is its id), so DVX calls are fine. Events are delivered by joyPump, which the shell calls on every idle frame; a program with its own main loop that repaints every frame (so the shell is never idle) calls joyPump itself or reads with joyRead. joyPump is not reentrant: an event function that runs a modal dialog gets its remaining events after it returns. A NULL fn stops the events.
+
+.h2 Polling and CPU Cost
+
+While at least one handle is open, a timer-service callback marks a poll due [joystick] pollHz times a second (JOY_POLL_HZ_DEFAULT 60, JOY_POLL_HZ_MIN 10 to JOY_POLL_HZ_MAX 200) and runs the drivers' tick hooks (the game port latches button presses there). The poll itself runs in program context, the next time a program reads (joyRead, joyButtonPressed, joyGetRaw) or the shell is idle, so however often a program reads there are at most pollHz polls a second. With no handle open nothing is polled and the timer is removed.
+
+A game port poll waits for the slowest connected axis to run out: about 0.6 ms with the stick centred, up to about 1.2 ms at an end. At 60 polls a second that is about 4 to 7 percent of the processor's time on any machine, whatever its speed, while a program has a joystick open; the timer interrupt's part (one port read) is negligible. The keyboard joystick costs nothing measurable.
+
+.h2 Calibration and Settings
+
+joyGetRaw(device, &raw) returns the last raw poll (driver units: microseconds for the game port). joyGetCalibration / joySetCalibration get and set an axis's JoyAxisCalT (min, center, max in raw units; min < center < max): min..center maps to -32768..0, center..max to 0..32767, clamped. A NULL calibration forgets it. An axis never calibrated ranges automatically: centred on the reading taken when the device was found, starting at center / 8 .. center * 2 and widening as the stick moves. joyGetDeadZone / joySetDeadZone get and set the device's dead zone in percent (0..JOY_DEADZONE_MAX, default JOY_DEADZONE_DEFAULT 8). joyAxisScale(cal, raw) and joyDeadZoneApply(value, percent) are the two steps as plain functions.
+
+Settings changed through the library take effect at once and reach DVX.INI only through joySettingsStore(prefs) (the caller saves the file); joySettingsLoad(prefs) replaces them all, e.g. to undo. joySettingGet / joySettingSet read and write single values. The [joystick] keys:
+
+.table
+ Key Value
+ --- -----
+ pollHz Polls a second (10 to 200, default 60).
+ fallback auto (default): the keyboard joystick only when no other device is found; always; never.
+ gameport.mode twoSticks (default) or fourAxes: one joystick using all four axes and buttons (flight sticks with a throttle or rudder).
+ keyboard.up ... keyboard.right, keyboard.button1 ... keyboard.button4 Key names (joyKeyName: Up, Down, Left, Right, LCtrl, RCtrl, LShift, RShift, LAlt, RAlt, Space, Enter, Tab, Backspace, Insert, Delete, Home, End, PgUp, PgDn, Keypad0 to Keypad9, A to Z, 0 to 9). Defaults: the arrows, Left Ctrl, Space, Left Shift, Z.
+ .axis min,center,max: axis n of the device with that id, e.g. gameport0.axis0 = 40,560,1150.
+ .deadZone The device's dead zone in percent.
+.endtable
+
+joyRescan() looks for devices again (a joystick plugged in later, a changed gameport.mode); it works only while no handle is open and returns -1 otherwise.
+
+.h2 The Game Port Driver
+
+joyport.drv drives the standard PC game port at I/O port 201h. Writing to the port starts four one-shot timers, one per axis; each runs for a time proportional to its axis's resistance (about 24 us plus 11 us per kilohm). The driver times them against the platform's microsecond clock (platformMicros) with interrupts left enabled, so the port never holds up the timer, the sound card or the mouse: each sample reads the port and the clock, and an axis whose fall can only be placed in a window wider than JOYPORT_GAP_US (40 us; an interrupt ran in between) keeps its previous value for that poll. A measurement gives up after JOYPORT_TIMEOUT_US (3 ms). At the probe an axis that does not run out, or runs out in under 10 us (some cards read 0 for an unconnected axis), is missing. At run time an axis that times out three polls in a row is left out of the measurement, so an unplugged stick does not cost the timeout every poll, and is tried again every 60 polls; a stick all of whose axes time out reads as not connected. Buttons A1, A2, B1, B2 are read at every poll and sampled by the timer interrupt, so short presses are latched.
+
+In the default mode the port makes up to two joysticks (A with axes 0-1 and buttons 0-1, B with axes 2-3 and buttons 2-3; either may be missing); in the four-axis mode one joystick with four axes and four buttons.
+
+.h2 The Keyboard Joystick
+
+joykeys.drv is a fallback driver: a joystick made of keys (two axes: full deflection while a key is held, the centre with both or neither; four buttons), read from the held-key table, so games work without joystick hardware. It is listed only when no other device is found, unless [joystick] fallback says always or never.
+
+.h2 Writing a Driver
+
+A driver is a driver module (see dvxDrv.h in the API reference): a file .drv in bin/libs/kpunch/joystick/drivers, built by the joystick Makefile from each drivers// folder, that exports one JoyDriverT named joyDriver, written with JOY_DRIVER(name, priority, .flags = ..., .probe = ..., .info = ..., .poll = ..., .tick = ..., .settingsChanged = ...). Nothing lists drivers and nothing loads them at startup: the first time a program asks for devices (the GUI and the timer service are running by then), and at every joyRescan, the library loads them all, probes them from the highest priority down (JOY_PRIORITY_PORT, ..., JOY_PRIORITY_KEYBOARD), probes the fallback drivers only when the fallback setting asks for them, and unloads every driver that found no device. The descriptor carries DVX_ABI_VERSION; a driver built for another version, one without probe, info or poll, or one whose name a higher priority driver already has is skipped and logged.
+
+.table
+ Member Meaning
+ ------ -------
+ hdr.name Lower case, no blanks ("gameport"); the prefix of the device ids and of the driver's own [joystick] keys.
+ hdr.priority Probing order (JOY_PRIORITY_*); devices are numbered in that order.
+ flags JOY_DRIVER_FALLBACK for a stand-in like the keyboard joystick.
+ probe() Find the units; return how many. Called right after the driver is loaded.
+ info(unit, out) Describe one unit (name, counts, axisPresent, caps); the library fills in id.
+ poll(raw, units) Read every unit now, in program context: raw[0..units-1]. Set axisValid for each axis measured (the others keep their last value), buttons held now, pressed for buttons that went down since the last poll, hats (JOY_HAT_CENTERED when not pushed) and connected. Axes are raw units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN..JOY_AXIS_MAX.
+ tick() NULL, or called INSIDE THE TIMER INTERRUPT at the poll rate while a handle is open. The timer service's rules apply; the driver pins the code and data it touches with platformLockRegion and releases them with platformUnlockRegion when its module is unloaded (a destructor), so rescans do not pile up locked memory.
+ settingsChanged() NULL, or re-read the driver's settings (joySettingGet) after they changed.
+.endtable
+
+Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded again at every probe, so it must not rely on state kept from an earlier load; set its hardware up in probe (or a constructor).
+
+.h2 BASIC
+
+src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton and JoyHat. Joystick, axis, button and hat numbers start at 1; each function reads through the calling program's shared handle.
+
+The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.
diff --git a/src/libs/kpunch/joystick/joystick.h b/src/libs/kpunch/joystick/joystick.h
new file mode 100644
index 00000000..37a795af
--- /dev/null
+++ b/src/libs/kpunch/joystick/joystick.h
@@ -0,0 +1,365 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// joystick.h -- joystick library (joystick.lib)
+//
+// Game controllers for DVX programs, behind one API whatever the hardware:
+//
+// Devices joyCount / joyGetInfo: the controllers the drivers found,
+// with their axes, buttons, hats and capabilities.
+// Reading joyOpen gives the calling program a handle; joyRead
+// returns the state (axes calibrated to JOY_AXIS_MIN ..
+// JOY_AXIS_MAX with a dead zone round the centre, buttons,
+// hats); joySetEventFn delivers changes as events instead.
+// Handles belong to the program and close when it ends.
+// Calibration per device and axis in DVX.INI [joystick], set by the
+// Control Panel's Game Controllers page.
+// Drivers driver modules (dvxDrv.h) the library loads when a
+// program first asks for devices (see "Drivers" below).
+//
+// Devices are polled at [joystick] pollHz times a second (the timer service
+// marks a poll due; the reading itself happens outside the interrupt, the
+// next time a program reads or the shell is idle), and only while at least
+// one handle is open.
+#ifndef JOYSTICK_H
+#define JOYSTICK_H
+
+#include "dvxApp.h"
+#include "dvxDrv.h"
+#include "dvxPrefs.h"
+
+#include
+#include
+#include
+
+// Calibrated axis range; 0 is the centre.
+#define JOY_AXIS_MIN (-32768)
+#define JOY_AXIS_MAX 32767
+
+// Table sizes
+#define JOY_MAX_DEVICES 8
+#define JOY_MAX_AXES 8
+#define JOY_MAX_BUTTONS 32
+#define JOY_MAX_HATS 4
+#define JOY_MAX_DRIVERS 8 // drivers with a timer hook (JoyDriverT.tick) in use at once
+#define JOY_MAX_HANDLES 32
+#define JOY_NAME_MAX 48
+#define JOY_ID_MAX 24
+
+// A hat (point of view switch) that is not pushed; otherwise its value is
+// the direction in degrees, clockwise from up (0 = up, 90 = right).
+#define JOY_HAT_CENTERED (-1)
+
+// Dead zone round the centre, in percent of the way to either end.
+#define JOY_DEADZONE_DEFAULT 8
+#define JOY_DEADZONE_MAX 50
+
+// Poll rate ([joystick] pollHz)
+#define JOY_POLL_HZ_DEFAULT 60
+#define JOY_POLL_HZ_MIN 10
+#define JOY_POLL_HZ_MAX 200
+
+// Smallest axis movement reported as an event (the centre and the ends are
+// always reported).
+#define JOY_EVENT_AXIS_STEP 1024
+
+// The DVX.INI section every joystick setting lives in, and its keys (see
+// joySettingGet)
+#define JOY_INI_SECTION "joystick"
+#define JOY_KEY_POLL_HZ "pollHz"
+#define JOY_KEY_FALLBACK "fallback"
+#define JOY_FALLBACK_AUTO "auto"
+#define JOY_FALLBACK_ALWAYS "always"
+#define JOY_FALLBACK_NEVER "never"
+#define JOY_KEY_GAMEPORT_MODE "gameport.mode"
+#define JOY_GAMEPORT_TWO "twoSticks"
+#define JOY_GAMEPORT_FOUR "fourAxes"
+#define JOY_KEY_AXIS_FMT "%s.axis%" PRId32 // device id, axis
+#define JOY_KEY_DEADZONE_FMT "%s.deadZone" // device id
+
+// The keyboard joystick's keys: [joystick] keyboard. = a key name
+// (joyKeyName). Use the lists as array initialisers.
+#define JOY_KBD_KEY_PREFIX "keyboard."
+#define JOY_KBD_SLOT_COUNT 8
+#define JOY_KBD_SLOT_NAMES "up", "down", "left", "right", "button1", "button2", "button3", "button4"
+#define JOY_KBD_SLOT_DEFAULTS "Up", "Down", "Left", "Right", "LCtrl", "Space", "LShift", "Z"
+
+// JoyInfoT.caps
+#define JOY_CAP_CALIBRATE 0x01 // analog axes: raw values need calibrating
+#define JOY_CAP_KEYBOARD 0x02 // made from keys: reads neutral unless the program's window has the focus
+
+// JoyDriverT.flags
+#define JOY_DRIVER_FALLBACK 0x01 // stands in when no other device is found ([joystick] fallback)
+
+// Where joystick drivers are found (relative to the DVX directory) and the
+// name of the descriptor each exports.
+#define JOY_DRV_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "joystick" DVX_PATH_SEP "drivers"
+#define JOY_DRV_SYMBOL "joyDriver"
+
+// Driver priorities (probed from the highest down; devices are numbered
+// in that order)
+#define JOY_PRIORITY_PORT 100 // the game port
+#define JOY_PRIORITY_KEYBOARD 0 // the keyboard joystick
+
+// What a device is. id is the driver's name and the unit number
+// ("gameport0"): the prefix of the device's keys in DVX.INI.
+typedef struct {
+ char name[JOY_NAME_MAX]; // for people: "Game port joystick 1"
+ char id[JOY_ID_MAX]; // filled in by the library
+ int32_t axisCount;
+ int32_t buttonCount;
+ int32_t hatCount;
+ uint32_t axisPresent; // bit n: axis n answers (a missing axis reads 0)
+ uint32_t caps; // JOY_CAP_*
+} JoyInfoT;
+
+// A device's state as a program sees it.
+typedef struct {
+ int32_t axes[JOY_MAX_AXES]; // JOY_AXIS_MIN .. JOY_AXIS_MAX, 0 in the dead zone
+ int32_t hats[JOY_MAX_HATS]; // JOY_HAT_CENTERED or degrees
+ uint32_t buttons; // bit n: button n is down, or went down since this handle last saw it
+ bool connected; // false when the device stopped answering
+} JoyStateT;
+
+// One poll of a device, as a driver reports it.
+typedef struct {
+ int32_t axes[JOY_MAX_AXES]; // driver units with JOY_CAP_CALIBRATE, else JOY_AXIS_MIN .. JOY_AXIS_MAX
+ int32_t hats[JOY_MAX_HATS];
+ uint32_t axisValid; // bit n: axes[n] was measured this time (else the last value stays)
+ uint32_t buttons; // held now
+ uint32_t pressed; // went down at some time since the last poll (may be up again)
+ bool connected;
+} JoyRawT;
+
+// Calibration of one analog axis, in the driver's raw units.
+typedef struct {
+ int32_t min;
+ int32_t center;
+ int32_t max;
+} JoyAxisCalT;
+
+typedef enum {
+ JoyEventAxisE, // index = axis, value = new position
+ JoyEventButtonDownE, // index = button
+ JoyEventButtonUpE, // index = button
+ JoyEventHatE // index = hat, value = JOY_HAT_CENTERED or degrees
+} JoyEventKindE;
+
+typedef struct {
+ JoyEventKindE kind;
+ int32_t device;
+ int32_t index;
+ int32_t value;
+} JoyEventT;
+
+// Called in the program's context (not at interrupt time): DVX calls are fine.
+typedef void (*JoyEventFnT)(int32_t handle, const JoyEventT *evt, void *userData);
+
+// ============================================================
+// Devices
+// ============================================================
+
+// Number of devices (0..JOY_MAX_DEVICES). The first call of any function
+// here finds the devices and reads DVX.INI.
+int32_t joyCount(void);
+
+// Describe device 0..joyCount()-1. False for another number.
+bool joyGetInfo(int32_t device, JoyInfoT *out);
+
+// Look for devices again (a joystick plugged in later, a changed game port
+// mode). Only while no handle is open; returns the new count, or -1 when
+// handles are open (the list stays as it is).
+int32_t joyRescan(void);
+
+// ============================================================
+// Reading
+// ============================================================
+
+// A handle on the device for the calling program (ctx->currentAppId owns
+// it: it closes when the program ends). -1 when the device does not exist
+// or JOY_MAX_HANDLES are open.
+int32_t joyOpen(AppContextT *ctx, int32_t device);
+
+// The calling program's shared handle on the device: the one this function
+// opened for it before, or a new one. For callers without a place to keep
+// a handle (the BASIC functions).
+int32_t joyOpenShared(AppContextT *ctx, int32_t device);
+
+// Close a handle (nothing for a closed or invalid one).
+void joyClose(int32_t handle);
+
+// The device's state (polling it first when a poll is due). A button that
+// went down and up again since this handle last saw it reads as down once.
+// A JOY_CAP_KEYBOARD device reads neutral while no window of the handle's
+// program has the focus. False for an invalid handle.
+bool joyRead(int32_t handle, JoyStateT *out);
+
+// One button: down now, or went down since this handle last saw it (by
+// this function or joyRead). False for an invalid handle or button.
+bool joyButtonPressed(int32_t handle, int32_t button);
+
+// One button went down since this handle last asked this function about
+// it (however briefly; whether it is still down does not matter). Counted
+// apart from joyRead / joyButtonPressed.
+bool joyButtonWentDown(int32_t handle, int32_t button);
+
+// Deliver the device's changes to fn (NULL stops): an axis event when an
+// axis moves by JOY_EVENT_AXIS_STEP or reaches the centre or an end, button
+// down / up, hat changes. Events are delivered by joyPump: the shell calls
+// it every idle frame; a program with its own main loop may call it too.
+bool joySetEventFn(int32_t handle, JoyEventFnT fn, void *userData);
+
+// Poll when due and deliver pending events.
+void joyPump(void);
+
+// Close every handle of a program (the shell calls this when a program
+// ends, however it ends).
+void joyAppCleanup(int32_t appId);
+
+// ============================================================
+// Calibration and settings (the Control Panel)
+// ============================================================
+
+// The device's last raw poll (polling it first when a poll is due).
+bool joyGetRaw(int32_t device, JoyRawT *out);
+
+// An axis's calibration. An axis never calibrated ranges automatically:
+// centred where it rested when the device was found, widening as it moves.
+// Returns false for an invalid device or axis or one without JOY_CAP_CALIBRATE.
+bool joyGetCalibration(int32_t device, int32_t axis, JoyAxisCalT *out);
+
+// Set (cal) or forget (NULL: back to automatic) an axis's calibration. It
+// takes effect at once and is written to DVX.INI by joySettingsStore. min <
+// center < max is required.
+bool joySetCalibration(int32_t device, int32_t axis, const JoyAxisCalT *cal);
+
+// The device's dead zone in percent (0..JOY_DEADZONE_MAX); -1 for an
+// invalid device.
+int32_t joyGetDeadZone(int32_t device);
+bool joySetDeadZone(int32_t device, int32_t percent);
+
+// [joystick] values. Keys: pollHz, fallback (auto = the fallback devices
+// only when nothing else is found, always, never), . for a
+// driver's own settings, .axis = min,center,max and .deadZone.
+// Get returns defaultVal for a missing key; Set with a NULL value removes
+// it; both apply at once (drivers are told) but DVX.INI changes only with
+// joySettingsStore.
+const char *joySettingGet(const char *key, const char *defaultVal);
+void joySettingSet(const char *key, const char *value);
+
+// Replace every [joystick] value with prefs' (e.g. to undo changes), or
+// write them all into prefs (the caller saves it).
+void joySettingsLoad(PrefsHandleT *prefs);
+void joySettingsStore(PrefsHandleT *prefs);
+
+// ============================================================
+// Helpers
+// ============================================================
+
+// A raw axis value calibrated to JOY_AXIS_MIN .. JOY_AXIS_MAX (clamped):
+// min..center maps to JOY_AXIS_MIN..0, center..max to 0..JOY_AXIS_MAX.
+int32_t joyAxisScale(const JoyAxisCalT *cal, int32_t raw);
+
+// A calibrated value with a dead zone of percent round the centre: inside
+// it reads 0, outside it is stretched so the ends stay at the ends.
+int32_t joyDeadZoneApply(int32_t value, int32_t percent);
+
+// Keys a keyboard joystick can use: count, the name ("Up", "LCtrl", "A")
+// and raw key code (KEY_RAW_CODE) of entry index, and lookups both ways
+// (-1 / NULL when unknown).
+int32_t joyKeyCode(int32_t index);
+int32_t joyKeyCount(void);
+int32_t joyKeyFind(const char *name);
+const char *joyKeyName(int32_t index);
+const char *joyKeyNameOf(int32_t rawCode);
+
+// ============================================================
+// Drivers
+// ============================================================
+//
+// A driver is a driver module (dvxDrv.h): a file .drv in
+// JOY_DRV_DIR exporting one JoyDriverT named joyDriver (write it with
+// JOY_DRIVER(...)). Nothing lists drivers and nothing loads them at
+// startup: the first time a program asks for devices (and at every
+// joyRescan) the library loads them, probes them from the highest
+// priority down (fallback drivers last, and only when the fallback
+// setting asks for them) and unloads every driver that found nothing.
+// hdr.name is the driver's short name (lower case, no blanks: device ids
+// and DVX.INI keys start with it).
+//
+// probe find the units; returns how many (0 = none).
+// info describe one unit (id is filled in by the library).
+// poll read every unit now, in program context: raw[0..units-1].
+// tick NULL, or called INSIDE THE TIMER INTERRUPT at the poll rate
+// while a handle is open, for sampling that must not wait
+// (latching button presses). The rules of the timer service
+// apply: the driver locks the code and data it touches.
+// settingsChanged NULL, or re-read the driver's settings
+// (joySettingGet) after they changed.
+typedef struct {
+ DvxDriverHeaderT hdr;
+ uint32_t flags; // JOY_DRIVER_*
+ int32_t (*probe)(void);
+ void (*info)(int32_t unit, JoyInfoT *out);
+ void (*poll)(JoyRawT *raw, int32_t units);
+ void (*tick)(void);
+ void (*settingsChanged)(void);
+} JoyDriverT;
+
+// Write a driver's descriptor: JOY_DRIVER("gameport", JOY_PRIORITY_PORT,
+// .probe = ..., .info = ..., .poll = ...);
+#define JOY_DRIVER(drvName, drvPriority, ...) extern const JoyDriverT joyDriver; const JoyDriverT joyDriver = { .hdr = DVX_DRIVER_HDR(JoyDriverT, drvName, drvPriority), __VA_ARGS__ }
+
+// ============================================================
+// BASIC's STICK and STRIG
+// ============================================================
+//
+// The DVX BASIC runtime calls these for the program running. QBasic's
+// joystick A is the first joystick (axes 1-2, buttons 1 and 2), joystick
+// B the second, or axes 3-4 and buttons 3-4 of the first when it is the
+// only one. STICK(0) reads A's X and remembers A's Y and B's X and Y for
+// STICK(1) to STICK(3); positions are 1 (left, up) to 200 (right, down),
+// 0 without that stick. STRIG(0), (2), (4), (6): A's button 1, B's
+// button 1, A's button 2, B's button 2 went down since the last such call;
+// STRIG(1), (3), (5), (7): that button is down now (a press too short to
+// be seen down counts once, as for joyRead). True is -1.
+int32_t joyBasStick(int32_t n);
+int32_t joyBasStrig(int32_t n);
+
+// ============================================================
+// BASIC (src/include/basic/joystick.bas)
+// ============================================================
+//
+// Joystick, axis, button and hat numbers start at 1; every function reads
+// through the calling program's shared handle.
+
+int32_t JoyAxes(int32_t joy);
+int32_t JoyAxis(int32_t joy, int32_t axis);
+int32_t JoyButton(int32_t joy, int32_t button);
+int32_t JoyButtons(int32_t joy);
+int32_t JoyCount(void);
+int32_t JoyHat(int32_t joy, int32_t hat);
+const char *JoyName(int32_t joy);
+int32_t JoyX(int32_t joy);
+int32_t JoyY(int32_t joy);
+
+#endif // JOYSTICK_H
diff --git a/src/libs/kpunch/libdvx/Makefile b/src/libs/kpunch/libdvx/Makefile
index b8296496..941575b5 100644
--- a/src/libs/kpunch/libdvx/Makefile
+++ b/src/libs/kpunch/libdvx/Makefile
@@ -35,7 +35,7 @@ LIBSDIR = ../../../../bin/libs
# Core sources. dvxPrefs.c is not among them on DOS: the loader needs it
# before libdvx loads, so dvx.exe links it and exports prefs* to every DXE.
-SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c \
+SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c dvxDriver.c \
dvxApp.c dvxDialog.c dvxResource.c \
widgetClass.c widgetCore.c widgetScrollbar.c \
widgetLayout.c widgetEvent.c widgetOps.c
@@ -57,7 +57,7 @@ TARGET = $(TARGETDIR)/libdvx.lib
HOSTOBJDIR = $(HOST_OBJROOT)
HOSTTARGET = $(HOSTOBJDIR)/libdvx.a
HOSTCFLAGS = $(HOST_CFLAGS) -I. -Iplatform -I../libtasks -Ithirdparty
-HOST_SRCS = $(SRCS) dvxPrefs.c platform/dvxPlatformHost.c platform/dvxMemTrack.c platform/dvxPlatformUtil.c thirdparty/stb_ds_impl.c
+HOST_SRCS = $(SRCS) dvxPrefs.c platform/dvxPlatformHost.c platform/dvxPlatformIrq.c platform/dvxMemTrack.c platform/dvxPlatformUtil.c thirdparty/stb_ds_impl.c
HOST_OBJS = $(patsubst %.c,$(HOSTOBJDIR)/libdvx/%.o,$(HOST_SRCS) $(notdir $(TOOL_SRCS)))
.PHONY: all clean host host-clean
diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
index 44d6810c..52051c54 100644
--- a/src/libs/kpunch/libdvx/apiref.dhs
+++ b/src/libs/kpunch/libdvx/apiref.dhs
@@ -50,6 +50,7 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its
.item dvxRes.h -- DXE-embedded resource system
.item dvxPrefs.h -- INI preferences system
.item dvxMem.h -- Per-app memory tracking
+.item dvxDrv.h -- Driver modules
.item dvxWgt.h -- Widget system public API
.item dvxWgtP.h -- Widget plugin API
.item platform/dvxPlat.h -- Platform abstraction layer
@@ -68,6 +69,7 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its
.link api.resource dvxRes.h -- Resource System
.link api.prefs dvxPrefs.h -- Preferences
.link api.mem dvxMem.h -- Memory Tracking
+.link api.driver dvxDrv.h -- Driver Modules
.link api.widget dvxWgt.h -- Widget System
.link api.widgetplugin dvxWgtP.h -- Widget Plugin API
.link api.platform platform/dvxPlat.h -- Platform Layer
@@ -114,6 +116,7 @@ DVX_ABI_VERSION is one number for the whole binary interface between dvx.exe, li
.app AppDescriptorT.abiVersion, set by SHELL_APP_DESCRIPTOR The shell, before appMain. The user sees "This program was built for a different version of DVX and cannot run."; DVX.LOG has both versions.
Widget API WgtApiHeaderT.abiVersion, set by WGT_API_HEADER wgtGetApi (which also checks the table's size).
Widget class WidgetClassT.version (WGT_CLASS_VERSION is DVX_ABI_VERSION) wgtRegisterClass.
+ Driver (.drv) DvxDriverHeaderT.abiVersion, set by DVX_DRIVER_HDR dvxDriversLoad, when the library of its device class loads it (a mismatched driver is skipped and logged).
.endtable
Every library and widget module writes the stamp once at file scope in its main source file; the argument is the module file's base name (libdvx for LIBDVX.LIB, box for BOX.WGT):
@@ -262,6 +265,7 @@ Central window object. Each window owns a persistent content backbuffer and rece
char title[MAX_TITLE_LEN] Window title text (max 128 chars)
bool visible, focused, minimized, maximized, resizable, modal Window state flags
bool iconNeedsRefresh true when contentBuf changed (for minimized icon refresh)
+ bool gameInput Game input: see below
uint8_t paintNeeded PAINT_NONE / PAINT_PARTIAL / PAINT_FULL
int32_t maxW, maxH Maximum dimensions
int32_t preMaxX, preMaxY, preMaxW, preMaxH Saved geometry before maximize
@@ -293,8 +297,11 @@ Callbacks:
int32_t onCursorQuery(WindowT *win, int32_t x, int32_t y) Return CURSOR_* for hit position
onFocus(WindowT *win) Window gained focus
onBlur(WindowT *win) Window lost focus
+ onRawKey(WindowT *win, const RawKeyEventT *evt) Key pressed or released (gameInput windows)
.endtable
+Game input: while a window with gameInput set has the keyboard focus, DVX keeps only Alt+Tab, Ctrl+Esc and Alt+F4 for itself. Every other keystroke, F1, F10, Alt+letter, Ctrl+F12, accelerators and hotkeys included, goes straight to onKey (except while a menu opened with the mouse, or the keyboard move/size mode, is taking keys). onRawKey then receives each key going down and up (RawKeyEventT: set-1 scancode, extended flag, repeat flag, modifiers from the held-key table), after the frame's onKey calls. When the focus moves away the window gets a release for every key still held (with no modifiers), and the held-key table starts over for the next window. onKeyUp keeps working as for any window. Mouse input is unchanged. The game library (game.h) adds gameKeyIsDown, frame pacing and timers.
+
.h3 WindowStackT
Z-ordered window stack (front-to-back: index count-1 is topmost). Owns system-wide drag/resize/scroll interaction state.
@@ -485,6 +492,26 @@ Non-ASCII keys are delivered as (scancode | KEY_EXT_FLAG). The platform layer re
KEY_SCAN_* are the raw BIOS scancodes (without KEY_EXT_FLAG): KEY_SCAN_ESC, KEY_SCAN_TAB, KEY_SCAN_SPACE, KEY_SCAN_F1 .. KEY_SCAN_F12, KEY_SCAN_HOME, KEY_SCAN_END, KEY_SCAN_PGUP, KEY_SCAN_PGDN, KEY_SCAN_INSERT, KEY_SCAN_DELETE, KEY_SCAN_UP, KEY_SCAN_DOWN, KEY_SCAN_LEFT, KEY_SCAN_RIGHT, KEY_SCAN_CTRL_LEFT, KEY_SCAN_CTRL_RIGHT, KEY_SCAN_CTRL_END, KEY_SCAN_CTRL_HOME, KEY_SCAN_ALT_TAB.
+.h3 Raw Key Codes
+
+A raw key code names one physical key: its set-1 make code (1-127), plus KEY_RAW_EXT (0x80) for keys sent behind an 0xE0 prefix (grey arrows and navigation keys, right Ctrl and Alt, keypad Enter). KEY_RAW_CODE(scancode, extended) builds one. The code is the same whatever modifier is held; letters, digits and punctuation use their KEY_SCAN_* / BIOS scancode. Used by the held-key table (platformKeyIsDown, gameKeyIsDown) and RawKeyEventT.
+
+.table
+ Define Value Key
+ ------ ----- ---
+ KEY_RAW_LSHIFT, KEY_RAW_RSHIFT 0x2A, 0x36 Left / right Shift
+ KEY_RAW_LCTRL, KEY_RAW_RCTRL 0x1D, 0x9D Left / right Ctrl
+ KEY_RAW_LALT, KEY_RAW_RALT 0x38, 0xB8 Left / right Alt
+ KEY_RAW_F11, KEY_RAW_F12 0x57, 0x58 F11, F12
+ KEY_RAW_UP, KEY_RAW_DOWN 0xC8, 0xD0 Grey arrows (the keypad's are 0x48, 0x50)
+ KEY_RAW_LEFT, KEY_RAW_RIGHT 0xCB, 0xCD
+ KEY_RAW_KP_ENTER 0x9C Keypad Enter
+ KEY_RAW_PAUSE 0xC5 Pause (a press and a release at once)
+ KEY_RAW_COUNT 256 Size of the held-key snapshot
+.endtable
+
+RawKeyEventT: uint8_t scancode (the make code), bool extended, bool down, bool repeat (a typematic repeat of a key already down), int32_t modifiers (KEY_MOD_* from the held-key table after the event, with KEY_MOD_LSHIFT 0x02 and KEY_MOD_RSHIFT 0x01 apart).
+
.h3 ASCII Key Codes
.table
@@ -2226,6 +2253,12 @@ Install an icon that is already in display pixel format (for example from dvxLoa
.index dvxRestoreWindow
.index dvxSetInputEntropyHook
.index DvxEntropyHookT
+.index DvxSoundE
+.index DvxSoundHookT
+.index dvxSetSoundHook
+.index dvxPlaySound
+.index dvxPlaySoundWait
+.index dvxBeep
.index dvxShutdown
.index dvxRun
.index dvxUpdate
@@ -2317,6 +2350,7 @@ Fields applications and the shell commonly read (all others are internal state):
int32_t mouseButtons Current MOUSE_* button bitmask
int32_t mouseWheel Wheel delta this frame (positive = down)
int32_t keyModifiers Current BIOS shift state (KEY_MOD_*)
+ int32_t keyFocusWinId Window the held-key table was last cleared for (0 = none; internal)
int32_t wheelDirection 1 = normal, -1 = reversed
int32_t wheelStep Lines per wheel notch (1-10, default 3)
PlatformTicksT dblClickTicks Double-click interval in platformClock ticks
@@ -3052,6 +3086,51 @@ Install a sink for input-timing entropy (for example a random-number generator's
fn Entropy sink, or NULL to disable
.endtable
+.h2 Desktop Sounds
+
+The sounds DVX plays for its events. Which file each event plays (or none) is set on the Control Panel's Sounds page and kept in DVX.INI [sounds]; the sound library plays them (see its developer help). Without the sound library, or without sound hardware, nothing is heard and nothing else changes.
+
+.code
+typedef enum {
+ SoundDefaultE, // the default beep (dvxBeep)
+ SoundStartE, // DVX started
+ SoundExitE, // DVX is ending
+ SoundErrorE,
+ SoundWarningE,
+ SoundQuestionE,
+ SoundInfoE,
+ SoundWindowOpenE,
+ SoundWindowCloseE,
+ SoundCountE
+} DvxSoundE;
+.endcode
+
+.list
+.item dvxMessageBox (and the dialogs built on it: dvxErrorBox, dvxInfoBox, dvxPromptSave, ...) plays the event of its icon: MB_ICONERROR SoundErrorE, MB_ICONWARNING SoundWarningE, MB_ICONQUESTION SoundQuestionE, MB_ICONINFO SoundInfoE, no icon SoundDefaultE.
+.item A window created with dvxCreateWindow plays SoundWindowOpenE at the next dvxUpdate, unless it has become modal by then (dialogs are quiet); destroying a window that is not modal plays SoundWindowCloseE.
+.item The shell plays SoundStartE once the desktop program is up, and SoundExitE (waiting for it) when DVX ends.
+.endlist
+
+.h3 dvxPlaySound / dvxPlaySoundWait / dvxBeep
+
+.code
+void dvxPlaySound(DvxSoundE event);
+void dvxPlaySoundWait(DvxSoundE event);
+void dvxBeep(void);
+.endcode
+
+Play the sound of an event. dvxPlaySound returns at once while the sound plays on; dvxPlaySoundWait returns once it has played (at most a few seconds); dvxBeep is dvxPlaySound(SoundDefaultE). An event without a sound is silent.
+
+.h3 dvxSetSoundHook
+
+.code
+typedef void (*DvxSoundHookT)(DvxSoundE event, bool wait);
+
+void dvxSetSoundHook(DvxSoundHookT fn);
+.endcode
+
+The function that plays the sounds, set by the sound library when it loads (NULL: silent). Programs do not call it.
+
.h2 Accelerators
.h3 dvxCreateAccelTable
@@ -3495,6 +3574,76 @@ Compute a djb2-xor hash for cheap dirty detection. Compare at save time with the
Returns: 32-bit hash value.
+.topic api.driver
+.title dvxDrv.h -- Driver Modules
+.toc 1 dvxDrv.h -- Driver Modules
+.index dvxDrv.h
+.index Driver Modules
+.index dvxDriversLoad
+.index dvxDriversFree
+.index dvxDriverKeep
+.index dvxDriverUnload
+.index DvxDriverHeaderT
+.index DvxDriverModT
+.index DvxDriverClassT
+.index DVX_DRIVER_HDR
+
+.h1 dvxDrv.h -- Driver Modules
+
+One model for the hardware drivers of every device class (sound, joysticks, ...). A driver is a module of its own, a file .drv in its class's folder under bin/libs (built like a library, but the loader does not load it). The library of the device class loads its drivers only when a program first needs that kind of device, probes them, keeps the ones it uses and unloads the rest, so drivers for hardware that is not there, or for a device class nobody uses, never stay in memory. Nothing lists drivers: adding one is dropping its file into the folder.
+
+Each driver exports one descriptor under a name its class chooses (audioDriver, joyDriver, ...). The descriptor is the class's own struct and starts with a DvxDriverHeaderT, written with DVX_DRIVER_HDR (the classes wrap it in their own macro, such as AUDIO_DRIVER or JOY_DRIVER).
+
+.h2 Types
+
+.table
+ Type Members
+ ---- -------
+ DvxDriverHeaderT abiVersion (DVX_ABI_VERSION the driver was built for), size (sizeof the whole descriptor), name (short name, lower case, no blanks; DVX.INI settings use it), priority (higher is tried first).
+ DvxDriverModT handle (NULL once kept out of the list or unloaded), desc (the descriptor), kind (index of the descriptor symbol found).
+ DvxDriverClassT symbols (descriptor symbol of each kind, as dlsym wants it: DVX_SYM("name")), minSizes (smallest descriptor accepted per kind), kinds, valid (the class's own check of a descriptor, such as its functions being present; NULL for none).
+.endtable
+
+DVX_DRIVER_HDR(type, name, priority) fills the header: { DVX_ABI_VERSION, sizeof(type), name, priority }.
+
+.h2 Functions
+
+.h3 dvxDriversLoad
+
+.code
+DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls);
+.endcode
+
+Loads every *.drv in dir (relative to the DVX directory), looks each descriptor up and checks it: the ABI version, the size, a name, and the class's valid function. The valid ones come back as an stb_ds array sorted by priority (highest first, by name on a tie); a module that does not load, has no valid descriptor, or repeats a name a higher priority driver already has is logged to DVX.LOG and unloaded. NULL when there are none.
+
+.h3 dvxDriverKeep
+
+.code
+DvxDriverModT dvxDriverKeep(DvxDriverModT *mods, int32_t index);
+.endcode
+
+Takes mods[index] out of the list (so dvxDriversFree leaves it loaded) and returns it; unload it with dvxDriverUnload when done.
+
+.h3 dvxDriversFree
+
+.code
+void dvxDriversFree(DvxDriverModT *mods);
+.endcode
+
+Unloads every module still in the list and frees the array.
+
+.h3 dvxDriverUnload
+
+.code
+void dvxDriverUnload(DvxDriverModT *mod);
+.endcode
+
+Unloads one module (nothing when it is not loaded) and clears its handle and descriptor.
+
+.h2 Writing a Device Class
+
+A library that has drivers puts them in drivers// in its source folder; its Makefile sets OBJDIR and DRVDIR and writes $(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d))) (common.mk), which builds each folder into $(DRVDIR)/.drv. The library defines its descriptor struct (starting with DvxDriverHeaderT), a macro that writes it, and a DvxDriverClassT; it calls dvxDriversLoad when it first needs devices, dvxDriverKeep for the drivers it uses and dvxDriversFree for the rest. Driver code resolves the library's functions when it loads, since the libraries are loaded for everyone to use.
+
.topic api.widget
.title dvxWgt.h -- Widget System
.toc 1 dvxWgt.h -- Widget System
@@ -5007,7 +5156,7 @@ Returns: true on success.
bool prefsSaveAs(PrefsHandleT *h, const char *filename);
.endcode
-Save to a specific file, overriding the handle's stored path. The data is written to a sibling file whose extension is replaced by $ and the first two characters of the original extension (8.3-safe, e.g. DVX.INI -> DVX.$IN) and swapped in only after every write succeeded; the previous file is parked as DVX.~IN during the swap and then removed. A failed save (disk full, write error) returns false and leaves the previous file intact.
+Save to a specific file, overriding the handle's stored path. The data is written to a sibling file whose extension is replaced by $ and the first two characters of the original extension (8.3-safe, e.g. DVX.INI -> DVX.$IN) and swapped in only after every write succeeded; the previous file is parked as DVX.~IN during the swap and then removed. A failed save (disk full, write error) returns false and leaves the previous file intact. When DVX runs from read-only media (platformIsReadOnly), a file that cannot be written is kept in memory instead (the save returns true) and prefsLoad of the same path reads that copy until DVX exits.
.table
Parameter Description
@@ -5744,11 +5893,39 @@ stb_ds dynamic array. Each widget DXE appends its class pointer via wgtRegisterC
.index platformDriveList
.index platformDiskSpace
.index platformBootDrive
+.index platformSetLogMemory
+.index platformLogMemory
+.index platformSetReadOnly
+.index platformIsReadOnly
+.index platformDirIsWritable
+.index platformTempDir
+.index Read-only Media
.index PlatformDriveT
.index Critical Error Handler
.index INT 24h
.index platformSplashNearestColor
.index platformUnlockRegion
+.index platformMicros
+.index platformTimerAdd
+.index platformTimerRemove
+.index platformTimerRemoveOwner
+.index platformTimerAddFast
+.index platformIrqSave
+.index platformIrqRestore
+.index platformShutdownHookAdd
+.index platformShutdownHookRemove
+.index platformTimerStats
+.index PlatformTimerFnT
+.index PlatformTimerStatsT
+.index Timer Service
+.index platformKeyHookInit
+.index platformKeyEventRead
+.index platformKeyIsDown
+.index platformKeyStateGet
+.index platformKeyStateClear
+.index platformInjectScan
+.index platformAppExit
+.index platformSetAppExitHook
.h1 platform/dvxPlat.h -- Platform Abstraction Layer
@@ -5846,6 +6023,63 @@ uint64_t platformEntropyStamp(void);
Fine-grained, free-running timestamp for input-timing entropy. It is not a clock: units and wrap point are backend-specific. On DOS it returns the CPU time-stamp counter (RDTSC) when CPUID reports one, otherwise the BIOS tick count shifted left 16 bits with the live PIT channel 0 counter (about 0.84 us resolution) in the low 16 bits. Used by dvxSetInputEntropyHook.
+.h3 platformMicros
+
+.code
+uint64_t platformMicros(void);
+.endcode
+
+Monotonic microseconds, the high-resolution clock for games and measurements. On DOS it counts from the start of the timer service (dvxInit): the PIT input clocks the timer interrupt has counted plus the part of the running period, read from the latched channel 0 count (about 0.84 us steps), or on a CPU with a time-stamp counter the TSC cycles since the last interrupt at the rate measured over the interrupt period before it, so the clock never drifts from the PIT and a TSC whose rate changes (power saving, an emulator) does no harm. The host backend returns its fake clock. Not for use inside a timer callback.
+
+.h3 Timer Service
+
+.code
+#define PLATFORM_PIT_HZ 1193182
+#define PLATFORM_PIT_BIOS_DIVISOR 65536
+#define PLATFORM_TIMER_MAX_HZ 1000
+#define PLATFORM_TIMER_FAST_MAX_HZ 30000
+#define PLATFORM_TIMER_SLOTS 16
+
+typedef void (*PlatformTimerFnT)(void *userData);
+
+void platformTimerInit(void);
+void platformTimerShutdown(void);
+int32_t platformTimerAdd(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
+int32_t platformTimerAddFast(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
+void platformTimerRemove(int32_t timerId);
+void platformTimerRemoveOwner(int32_t ownerAppId);
+void platformTimerStats(PlatformTimerStatsT *out);
+.endcode
+
+One timer interrupt for everything that must run at a steady rate however busy the programs are (audio refill, joystick polling). dvxInit starts the service; dvxShutdown stops it. On DOS it owns PIT channel 0 (rate generator mode) and INT 8: the interrupt runs as fast as the fastest registered timer needs (at most PLATFORM_TIMER_MAX_HZ; the BIOS 18.2 Hz when there is none) and exactly one interrupt per 65536 PIT clocks is passed on to the BIOS handler, so the BIOS tick count, the DOS time of day, clock() and the floppy motor timeout keep their normal rate. A rate change takes effect at once (the PIT is restarted and the part of the period already run is counted). Shutdown restores the BIOS square-wave mode and the old vector; it also runs at exit() and from the crash handler when a crash is not recovered.
+
+platformTimerAdd calls fn(userData) hz times a second, 1 to PLATFORM_TIMER_MAX_HZ; exact on average, while single intervals vary by up to one interrupt period. It returns a timer id, or -1 (bad arguments, all PLATFORM_TIMER_SLOTS in use, service not running). ownerAppId is recorded for platformTimerRemoveOwner, which the shell calls when a program ends; use 0 for a library that removes its own timers. gameTimerAdd (game.lib) registers one for the calling program. After platformTimerRemove returns, the callback is not running and will not run again.
+
+platformTimerAddFast is the same for a device that must be fed at an audio rate (the PC speaker's digital sound): hz may be up to PLATFORM_TIMER_FAST_MAX_HZ. The whole timer interrupt then runs that often, so the callback must be very short (a few port writes); ask for PLATFORM_PIT_HZ / divisor to be called on every interrupt.
+
+.note warning
+The callback runs INSIDE THE TIMER INTERRUPT, with interrupts disabled, on a small stack. Keep it to a few dozen microseconds; do not call the C library, DVX or DOS; do not use floating point; and pin its code and every byte it touches with platformLockRegion (a page fault there is fatal on DOS). Set flags and counters, fill buffers, write I/O ports.
+.endnote
+
+PlatformTimerStatsT reports rateHz (the interrupt rate, rounded), divisor (the PIT reload value), interrupts (since the service started) and biosTicks (of those, passed on to the BIOS).
+
+.h3 Interrupt-Safe Updates and Shutdown Hooks
+
+.code
+uint32_t platformIrqSave(void);
+void platformIrqRestore(uint32_t state);
+
+#define PLATFORM_SHUTDOWN_HOOKS 8
+typedef void (*PlatformShutdownFnT)(void);
+
+bool platformShutdownHookAdd(PlatformShutdownFnT fn);
+void platformShutdownHookRemove(PlatformShutdownFnT fn);
+.endcode
+
+platformIrqSave disables interrupts and returns the previous state for platformIrqRestore; bracket the few instructions that change data an interrupt handler reads (nothing happens on the host).
+
+A shutdown hook runs when DVX ends without its normal shutdown (exit() or a crash with no recovery point), before the timer service and the keyboard hook are given back, so a module that drives hardware from an interrupt (a sound card playing by DMA) can stop it. Each hook runs once. A module removes its hook before it is unloaded; at most PLATFORM_SHUTDOWN_HOOKS exist at once (Add returns false when full; adding one already present does nothing).
+
.h2 Video
.h3 platformVideoInit / platformVideoShutdown
@@ -5983,15 +6217,21 @@ int32_t platformNormalizeFKeyScan(int32_t scancode);
Map the BIOS scancodes for Shift/Ctrl/Alt+F1..F12 (Shift F1-F10 0x54-0x5D, Ctrl 0x5E-0x67, Alt 0x68-0x71, Shift/Ctrl/Alt F11-F12 0x87-0x8C) onto the plain F-key scancodes (F1-F10 0x3B-0x44, F11 0x85, F12 0x86). Any other scancode is returned unchanged. Applied by every backend's platformKeyboardRead.
-.h3 platformKeyUpInit / platformKeyUpShutdown / platformKeyUpRead
+.h3 Raw Keys and the Held-Key Table
.code
-void platformKeyUpInit(void);
-void platformKeyUpShutdown(void);
-bool platformKeyUpRead(PlatformKeyEventT *evt);
+#define PLATFORM_KEY_EVENT_QUEUE 64
+
+void platformKeyHookInit(void);
+void platformKeyHookShutdown(void);
+bool platformKeyEventRead(RawKeyEventT *evt);
+bool platformKeyIsDown(int32_t rawCode);
+void platformKeyStateGet(uint8_t out[KEY_RAW_COUNT]);
+void platformKeyStateClear(void);
+void platformInjectScan(uint8_t scan);
.endcode
-Key-up event detection. Install/uninstall the INT 9 hook around process lifetime. platformKeyUpRead returns true if a key release was detected since the last call.
+The keyboard hook (on DOS a chain on INT 9 that reads port 60h before the BIOS handler stores the key for platformKeyboardRead) decodes every set-1 byte: 0xE0 prefixes, the fake Shift codes the keyboard wraps around grey keys (dropped), the Pause burst (one press and one release) and keyboard replies such as ACK and overrun (dropped). Each key going down or up updates the held-key table and is queued as a RawKeyEventT (at most PLATFORM_KEY_EVENT_QUEUE; newer events are dropped when the queue is full, the table still changes). Keys are named by raw code, KEY_RAW_CODE(scancode, extended) (see dvxTypes.h): Left and Right Shift, Ctrl and Alt are keys of their own, and the code does not change with modifiers. dvxInit installs the hook and dvxShutdown removes it; the event loop reads the queue (onKeyUp, onRawKey) and clears the table whenever the keyboard focus moves. platformKeyStateGet copies the table (1 = down). platformInjectScan feeds one byte to the decoder as if the keyboard had sent it (the test script's KEYDOWN / KEYUP use it).
.h3 platformAltScanToChar
@@ -6150,6 +6390,21 @@ platformDiskSpace reports the size and free space of the disk holding path (DOS:
platformBootDrive returns the letter of the drive DOS started from (INT 21h AX=3305h, DOS 4.0+), whose root holds AUTOEXEC.BAT, or 0 where there is none (older DOS, and every platform but DOS).
+.h3 Read-only media: platformSetReadOnly / platformIsReadOnly / platformDirIsWritable / platformTempDir / platformSetLogMemory / platformLogMemory
+
+.code
+#define PLATFORM_LOG_MEM_MAX 16384
+
+void platformSetReadOnly(bool readOnly);
+bool platformIsReadOnly(void);
+bool platformDirIsWritable(const char *dir);
+const char *platformTempDir(void);
+void platformSetLogMemory(void);
+const char *platformLogMemory(void);
+.endcode
+
+The loader tests at startup whether the DVX folder can be written to (platformDirIsWritable creates and removes a scratch file in dir) and records the answer with platformSetReadOnly. When DVX runs from a CD-ROM or a write-protected disk (platformIsReadOnly is true): the log goes to DVX.LOG in the TEMP folder (platformTempDir: the TEMP or else TMP variable, NULL when neither is set) or, without one, to memory; the boot-time help rebuild is skipped (its output and counts could not be saved); prefsSave and prefsSaveAs keep a file they cannot write in memory, and prefsLoad reads it back until DVX exits; the shell says so once at startup; and a program that allows several copies runs only once when there is no TEMP folder (a second copy is a temporary copy of the module). platformSetLogMemory sends dvxLog to a buffer holding the latest PLATFORM_LOG_MEM_MAX bytes (the oldest whole lines are dropped), which platformLogMemory returns; platformSetLogPath goes back to a file. SETUP.EXE logs to memory unless /LOG= is given, so it never writes to its own disk.
+
.h3 platformLineEnding / platformStripLineEndings
.code
@@ -6242,7 +6497,20 @@ Pin (or unpin) a code (isCode = true) or data range into physical memory so an i
void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn);
.endcode
-Install signal handlers for SIGSEGV, SIGFPE, SIGILL. On crash, logs platform-specific diagnostics via logFn, stores the signal in *crashSignal, then longjmps to recoveryBuf.
+Install signal handlers for SIGSEGV, SIGFPE, SIGILL. On crash, logs platform-specific diagnostics via logFn, stores the signal in *crashSignal, then longjmps to recoveryBuf. Without a recovery buffer the timer service and the keyboard hook are removed and the signal's default action runs.
+
+.h3 platformAppExit / platformAppAbort / platformSetAppExitHook
+
+.code
+#define PLATFORM_ABORT_STATUS 3
+typedef void (*PlatformAppExitFnT)(int status);
+
+void platformSetAppExitHook(PlatformAppExitFnT fn);
+void platformAppExit(int status);
+void platformAppAbort(void);
+.endcode
+
+What exit() and abort() mean to loaded modules. The DXE export table maps them to platformAppExit and platformAppAbort (the host build links every app with the same substitution, HOST_APP_LDFLAGS in common.mk). Both call the hook first; the shell's hook ends only the program that is current, the way End Task does (its task, or its callback running on the shell task, is abandoned through the crash-recovery path and the program is removed without its appShutdown and without a fault message; the log says "'name' called exit (status n); ending it"). abort() reports status PLATFORM_ABORT_STATUS. With no program current, or no hook, they end the process like the C library functions. Handlers a program registered with atexit are not run.
.h3 platformLogCrashDetail
diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs
index 7e947c36..906ea1cc 100644
--- a/src/libs/kpunch/libdvx/arch.dhs
+++ b/src/libs/kpunch/libdvx/arch.dhs
@@ -828,15 +828,15 @@ Polling model. platformMousePoll() returns position and button bitmask. Wheel su
.h3 Keyboard Input
-platformKeyboardRead() -- non-blocking key read; Shift/Ctrl/Alt+F1..F12 are reported as the plain F-key scancode (platformNormalizeFKeyScan). platformKeyUpRead() -- key release detection (requires INT 9 hook on DOS; its code and data are pinned with platformLockRegion). platformAltScanToChar() -- scancode-to-ASCII lookup for Alt+key combinations.
+platformKeyboardRead() -- non-blocking key read; Shift/Ctrl/Alt+F1..F12 are reported as the plain F-key scancode (platformNormalizeFKeyScan). The keyboard hook (a chain on INT 9 on DOS; its code and data are pinned with platformLockRegion) decodes every key going down and up into a held-key table and a queue of raw events (platformKeyEventRead, platformKeyIsDown), which feed onKeyUp and game input windows. platformAltScanToChar() -- scancode-to-ASCII lookup for Alt+key combinations.
.h3 Timing
-platformClock() / platformClockHz() -- monotonic UI tick counter. platformEntropyStamp() -- fine-grained timestamp (TSC, or BIOS tick + PIT counter) for input-timing entropy.
+platformClock() / platformClockHz() -- monotonic UI tick counter. platformMicros() -- high-resolution monotonic clock (PIT, with the TSC between interrupts when there is one). The timer service owns the timer interrupt: callbacks at up to 1000 Hz (platformTimerAdd) while the BIOS keeps its 18.2 Hz tick. platformEntropyStamp() -- fine-grained timestamp (TSC, or BIOS tick + PIT counter) for input-timing entropy.
.h3 Crash Recovery
-platformInstallCrashHandler() -- signal handlers + longjmp for fault tolerance. platformOutOfMemory() -- stb_ds allocation-failure hook; logs and raises SIGSEGV so it takes the same recovery path.
+platformInstallCrashHandler() -- signal handlers + longjmp for fault tolerance. platformOutOfMemory() -- stb_ds allocation-failure hook; logs and raises SIGSEGV so it takes the same recovery path. platformAppExit() / platformAppAbort() -- exit() and abort() as loaded modules see them: the shell's hook ends only the calling program, by the same recovery path, without a fault message.
.h3 DXE Support
diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
index 882de0fd..6bfde48f 100644
--- a/src/libs/kpunch/libdvx/dvxApp.c
+++ b/src/libs/kpunch/libdvx/dvxApp.c
@@ -235,6 +235,7 @@ static void dirtyIconStrip(AppContextT *ctx);
static bool dispatchAccelKey(AppContextT *ctx, char key);
static void dispatchEvents(AppContextT *ctx);
static void dispatchKey(AppContextT *ctx, int32_t scancode, int32_t ascii, int32_t shiftFlags);
+static void dispatchRawKey(AppContextT *ctx, const RawKeyEventT *evt);
static void drawCursorAt(AppContextT *ctx, int32_t x, int32_t y);
static void drawPopupLevel(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const MenuT *menu, int32_t px, int32_t py, int32_t pw, int32_t ph, int32_t hoverItem, bool hasCheck, const RectT *clipTo);
static bool drawPopupRow(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, int32_t px, int32_t pw, int32_t itemY, bool separator, bool enabled, bool hovered, uint32_t *fg, uint32_t *bg);
@@ -264,6 +265,7 @@ static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win);
static void invalidateAllWindows(AppContextT *ctx);
static bool isFocusCandidate(const WindowT *win);
static bool isModalWindow(const AppContextT *ctx, const WindowT *win);
+static void keyFocusSync(AppContextT *ctx);
static void lowerWindowToBottom(AppContextT *ctx, int32_t idx);
static int32_t menuRowAt(const AppContextT *ctx, int32_t popupY, int32_t itemCount, int32_t my);
static int32_t nextMenuItem(const bool *firstSeparator, size_t stride, int32_t count, int32_t idx, int32_t dir);
@@ -275,6 +277,7 @@ static void openSysMenu(AppContextT *ctx, WindowT *win);
static void packColorScheme(AppContextT *ctx);
static uint32_t packDithered(const DisplayT *d, int32_t r, int32_t g, int32_t b, int32_t row, int32_t col, bool dither);
static inline void pixelToRgb(const ChannelLutT *lut, uint32_t pixel, uint8_t *dst);
+static void playOpenSound(AppContextT *ctx);
static bool pointInPopup(const AppContextT *ctx, int32_t mx, int32_t my);
static void pollKeyboard(AppContextT *ctx);
static void pollMouse(AppContextT *ctx);
@@ -351,6 +354,12 @@ static DvxEntropyHookT sEntropyHook = NULL;
static uint8_t sEntropyBuf[ENTROPY_BATCH_BYTES];
static int32_t sEntropyLen = 0;
+// Desktop sound player (dvxSetSoundHook), and the window created since the
+// last frame whose open sound is due (0: none). The sound waits a frame
+// so a dialog, which is marked modal after it is created, stays quiet.
+static DvxSoundHookT sSoundHook = NULL;
+static int32_t sOpenSoundWinId = 0;
+
// 4x4 Bayer dithering offsets for ordered dithering when converting
// 24-bit wallpaper images to 15/16-bit pixel formats.
static const int32_t sBayerMatrix[BAYER_SIZE][BAYER_SIZE] = {
@@ -1454,6 +1463,15 @@ static void destroyWindowPrep(AppContextT *ctx, WindowT *win) {
// guaranteed same-frame use-after-free without this.
closeWindowTransients(ctx, win);
+ // A window put off for destroying already had its close sound.
+ if (!win->modal && !win->destroyPending) {
+ dvxPlaySound(SoundWindowCloseE);
+ }
+
+ if (sOpenSoundWinId == win->id) {
+ sOpenSoundWinId = 0;
+ }
+
// Clear the modal pointer if it was this window, even when deferring
// -- a hidden pending-destroy modal must not keep eating input.
if (ctx->modalWindow == win) {
@@ -1946,21 +1964,23 @@ static void dispatchEvents(AppContextT *ctx) {
//
// 1. Alt+Tab / Shift+Alt+Tab -- window cycling (always works)
// 2. Alt+F4 -- close focused window
-// 3. Ctrl+F12 / Ctrl+Shift+F12 -- screenshot (full / window)
-// 4. Ctrl+Esc -- system-wide hotkey (task manager)
-// 5. F1 -- system-wide help
-// 6. F10 -- activate/toggle menu bar
-// 7. Keyboard move/resize mode (every key captured)
-// 8. Alt+Space -- system menu toggle
-// 9. System menu keyboard navigation (arrows, enter, esc, accel)
-// 10. System-wide hotkeys (dvxRegisterHotkey; not while a menu or a
+// 3. Ctrl+Esc -- system-wide hotkey (task manager)
+// 4. Game input window focused (and no menu open): its onKey gets
+// the key, nothing below runs
+// 5. Ctrl+F12 / Ctrl+Shift+F12 -- screenshot (full / window)
+// 6. F1 -- system-wide help
+// 7. F10 -- activate/toggle menu bar
+// 8. Keyboard move/resize mode (every key captured)
+// 9. Alt+Space -- system menu toggle
+// 10. System menu keyboard navigation (arrows, enter, esc, accel)
+// 11. System-wide hotkeys (dvxRegisterHotkey; not while a menu or a
// modal dialog is open)
-// 11. Alt+key -- menu bar / widget accelerator dispatch
-// 12. Popup menu keyboard navigation (menus are keyboard-modal)
-// 13. Esc closes an open dropdown list
-// 14. Accelerator table on focused window (Ctrl+S, etc.)
-// 15. Tab/Shift+Tab -- widget focus cycling
-// 16. Fall-through to focused window's onKey callback
+// 12. Alt+key -- menu bar / widget accelerator dispatch
+// 13. Popup menu keyboard navigation (menus are keyboard-modal)
+// 14. Esc closes an open dropdown list
+// 15. Accelerator table on focused window (Ctrl+S, etc.)
+// 16. Tab/Shift+Tab -- widget focus cycling
+// 17. Fall-through to focused window's onKey callback
//
// Key encoding: ASCII keys use their ASCII value; extended keys (arrows,
// function keys) use scancode | KEY_EXT_FLAG to distinguish from ASCII 0.
@@ -1988,6 +2008,26 @@ static void dispatchKey(AppContextT *ctx, int32_t scancode, int32_t ascii, int32
return;
}
+ // Ctrl+Esc -- system-wide hotkey (e.g. task manager)
+ if (scancode == KEY_SCAN_ESC && ascii == KEY_ESCAPE && (shiftFlags & KEY_MOD_CTRL)) {
+ if (ctx->onCtrlEsc) {
+ ctx->onCtrlEsc(ctx->ctrlEscCtx);
+ }
+
+ return;
+ }
+
+ // Game input: the three keys above are the only ones DVX keeps; every
+ // other key goes straight to the game, unless a menu or the keyboard
+ // move/size mode (opened with the mouse) is taking keys.
+ if (win && win->gameInput && !ctx->popup.active && !ctx->sysMenu.active && ctx->kbMoveResize.mode == KbModeNoneE) {
+ if (win->onKey) {
+ WIN_CALLBACK(ctx, win, win->onKey(win, key, shiftFlags));
+ }
+
+ return;
+ }
+
// Ctrl+F12 -- save full screen screenshot
// Ctrl+Shift+F12 -- save focused window screenshot
if (key == KEY_F12 && (shiftFlags & KEY_MOD_CTRL)) {
@@ -2000,15 +2040,6 @@ static void dispatchKey(AppContextT *ctx, int32_t scancode, int32_t ascii, int32
return;
}
- // Ctrl+Esc -- system-wide hotkey (e.g. task manager)
- if (scancode == KEY_SCAN_ESC && ascii == KEY_ESCAPE && (shiftFlags & KEY_MOD_CTRL)) {
- if (ctx->onCtrlEsc) {
- ctx->onCtrlEsc(ctx->ctrlEscCtx);
- }
-
- return;
- }
-
// F1 -- system-wide help
if (key == KEY_F1 && !anyMod) {
if (ctx->onF1) {
@@ -2105,6 +2136,34 @@ static void dispatchKey(AppContextT *ctx, int32_t scancode, int32_t ascii, int32
}
+// One raw key event for the focused window: a release goes to onKeyUp
+// (with the BIOS scancode for F11/F12, as their key-down arrives), and a
+// game input window gets every press and release in onRawKey.
+static void dispatchRawKey(AppContextT *ctx, const RawKeyEventT *evt) {
+ WindowT *win = focusedWindow(ctx);
+
+ if (!win) {
+ return;
+ }
+
+ if (!evt->down && win->onKeyUp) {
+ int32_t scan = evt->scancode;
+
+ if (!evt->extended && scan == KEY_RAW_F11) {
+ scan = KEY_SCAN_F11;
+ } else if (!evt->extended && scan == KEY_RAW_F12) {
+ scan = KEY_SCAN_F12;
+ }
+
+ WIN_CALLBACK(ctx, win, win->onKeyUp(win, scan | KEY_EXT_FLAG, platformKeyboardGetModifiers()));
+ }
+
+ if (win->gameInput && win->onRawKey && !win->destroyPending) {
+ WIN_CALLBACK(ctx, win, win->onRawKey(win, evt));
+ }
+}
+
+
// ============================================================
// drawCursorAt
// ============================================================
@@ -2346,11 +2405,13 @@ void dvxApplyColorScheme(AppContextT *ctx) {
#ifdef DVX_HOST_TEST
void dvxAppTestReset(void) {
- sKeyPressedBtn = NULL;
- sCursorBlinkFn = NULL;
- sTooltipGen = 0;
- sEntropyHook = NULL;
- sEntropyLen = 0;
+ sKeyPressedBtn = NULL;
+ sCursorBlinkFn = NULL;
+ sTooltipGen = 0;
+ sEntropyHook = NULL;
+ sEntropyLen = 0;
+ sSoundHook = NULL;
+ sOpenSoundWinId = 0;
memset(sEntropyBuf, 0, sizeof(sEntropyBuf));
arrfree(sHotkeys);
sHotkeys = NULL;
@@ -2361,6 +2422,11 @@ void dvxAppTestReset(void) {
#endif
+void dvxBeep(void) {
+ dvxPlaySound(SoundDefaultE);
+}
+
+
// ============================================================
// dvxCascadeWindows
// ============================================================
@@ -2673,6 +2739,7 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t
// cleanup on crash/termination.
win->appId = ctx->currentAppId;
ctx->windowsCreated++;
+ sOpenSoundWinId = win->id;
// Raise and focus
focusWindowAt(ctx, ctx->stack.count - 1);
@@ -2917,7 +2984,8 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
memset(ctx, 0, sizeof(*ctx));
platformInit();
- platformKeyUpInit();
+ platformKeyHookInit();
+ platformTimerInit();
// Enumerate available video modes BEFORE setting one. Some VBE
// BIOSes return a stale or truncated mode list once a graphics
@@ -2927,11 +2995,13 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
dvxLog("App: video init failed for %dx%d@%dbpp", requestedW, requestedH, preferredBpp);
- // dvxShutdown never runs on this path; drop the mode list and the
- // key-up hook here (a caller may exit, or try another mode).
+ // dvxShutdown never runs on this path; drop the mode list, the
+ // timer service and the keyboard hook here (a caller may exit, or
+ // try another mode).
arrfree(ctx->videoModes);
ctx->videoModes = NULL;
- platformKeyUpShutdown();
+ platformTimerShutdown();
+ platformKeyHookShutdown();
return -1;
}
@@ -3237,6 +3307,20 @@ void dvxMinimizeWindow(AppContextT *ctx, WindowT *win) {
}
+void dvxPlaySound(DvxSoundE event) {
+ if (sSoundHook) {
+ sSoundHook(event, false);
+ }
+}
+
+
+void dvxPlaySoundWait(DvxSoundE event) {
+ if (sSoundHook) {
+ sSoundHook(event, true);
+ }
+}
+
+
// ============================================================
// dvxQuit
// ============================================================
@@ -3637,6 +3721,11 @@ void dvxSetMouseConfig(AppContextT *ctx, int32_t wheelDir, int32_t dblClickMs, i
}
+void dvxSetSoundHook(DvxSoundHookT fn) {
+ sSoundHook = fn;
+}
+
+
// ============================================================
// dvxSetTitle
// ============================================================
@@ -3806,7 +3895,8 @@ void dvxShowWindow(AppContextT *ctx, WindowT *win) {
void dvxShutdown(AppContextT *ctx) {
- platformKeyUpShutdown();
+ platformTimerShutdown();
+ platformKeyHookShutdown();
for (int32_t i = (int32_t)arrlen(sHotkeys) - 1; i >= 0; i--) {
if (sHotkeys[i].ctx == ctx) {
@@ -4053,6 +4143,7 @@ bool dvxUpdate(AppContextT *ctx) {
}
wgtUpdateTimers();
+ playOpenSound(ctx);
// Flush deferred paints. paintNeeded is set by wgtInvalidatePaint
// (PARTIAL) or wgtInvalidate (FULL). Multiple calls per frame are
@@ -5165,6 +5256,44 @@ static bool isModalWindow(const AppContextT *ctx, const WindowT *win) {
}
+// When keyboard focus moved to another window since the last call, the
+// held-key table starts over for the new one: a game input window that
+// lost focus gets a release for every key still down (it will not see
+// the real releases, which go to the new window; these report no
+// modifiers), then the table is cleared.
+static void keyFocusSync(AppContextT *ctx) {
+ WindowT *win = focusedWindow(ctx);
+ int32_t winId = win ? win->id : 0;
+
+ if (winId == ctx->keyFocusWinId) {
+ return;
+ }
+
+ WindowT *old = findWindowById(ctx, ctx->keyFocusWinId);
+
+ ctx->keyFocusWinId = winId;
+
+ if (old && old->gameInput && old->onRawKey && !old->destroyPending) {
+ uint8_t held[KEY_RAW_COUNT];
+
+ platformKeyStateGet(held);
+
+ for (int32_t code = 0; code < KEY_RAW_COUNT; code++) {
+ if (held[code]) {
+ RawKeyEventT up;
+
+ memset(&up, 0, sizeof(up));
+ up.scancode = (uint8_t)(code & KEY_RAW_SCAN_MASK);
+ up.extended = (code & KEY_RAW_EXT) != 0;
+ WIN_CALLBACK(ctx, old, old->onRawKey(old, &up));
+ }
+ }
+ }
+
+ platformKeyStateClear();
+}
+
+
// Moves stack slot idx to the bottom (slot 0), shifting the windows below
// it up one slot, and remaps every stack-index reference (focus, drag,
// resize, scroll) the same way. The inverse of wmRaiseWindow; used by
@@ -5526,6 +5655,23 @@ static inline void pixelToRgb(const ChannelLutT *lut, uint32_t pixel, uint8_t *d
// True when (mx, my) lies inside the current (deepest) popup level.
+// The open sound of the window created last, unless it has become a
+// dialog (modal) or is gone.
+static void playOpenSound(AppContextT *ctx) {
+ if (!sOpenSoundWinId) {
+ return;
+ }
+
+ const WindowT *win = findWindowById(ctx, sOpenSoundWinId);
+
+ sOpenSoundWinId = 0;
+
+ if (win && !win->modal) {
+ dvxPlaySound(SoundWindowOpenE);
+ }
+}
+
+
static bool pointInPopup(const AppContextT *ctx, int32_t mx, int32_t my) {
return mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW && my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH;
}
@@ -5553,11 +5699,13 @@ static void pollKeyboard(AppContextT *ctx) {
// discard
}
- // The INT 9 ISR keeps queueing break codes regardless of busy, so
- // the key-up ring must be drained too -- otherwise the releases of
+ // The keyboard hook keeps queueing raw events regardless of busy,
+ // so that queue must be drained too -- otherwise the releases of
// keys typed during busy replay as phantom onKeyUp events the
// instant busy clears.
- while (platformKeyUpRead(&drain)) {
+ RawKeyEventT rawDrain;
+
+ while (platformKeyEventRead(&rawDrain)) {
// discard
}
@@ -5565,6 +5713,10 @@ static void pollKeyboard(AppContextT *ctx) {
}
PlatformKeyEventT evt;
+ RawKeyEventT raw;
+
+ // A focus change since the last poll (a click, a new window) first.
+ keyFocusSync(ctx);
// evt.modifiers is per key: a modifier-specific BIOS code (Alt+F4,
// Shift+F5, Alt+Left) keeps its modifier even if it was released
@@ -5576,17 +5728,14 @@ static void pollKeyboard(AppContextT *ctx) {
dispatchKey(ctx, evt.scancode, evt.ascii, evt.modifiers);
}
- // Key-up events: dispatch to the focused window's onKeyUp callback
- PlatformKeyEventT upEvt;
-
- while (platformKeyUpRead(&upEvt)) {
- WindowT *win = focusedWindow(ctx);
-
- if (win && win->onKeyUp) {
- int32_t mod = platformKeyboardGetModifiers();
- WIN_CALLBACK(ctx, win, win->onKeyUp(win, upEvt.scancode | KEY_EXT_FLAG, mod));
- }
+ // Presses and releases after the keystrokes, so a key tapped within one
+ // frame reaches onKey before its onKeyUp.
+ while (platformKeyEventRead(&raw)) {
+ dispatchRawKey(ctx, &raw);
}
+
+ // A keystroke may have moved the focus (Alt+Tab).
+ keyFocusSync(ctx);
}
diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h
index 15413e5d..d186e653 100644
--- a/src/libs/kpunch/libdvx/dvxApp.h
+++ b/src/libs/kpunch/libdvx/dvxApp.h
@@ -76,6 +76,7 @@ typedef struct AppContextT {
int32_t mouseButtons;
int32_t mouseWheel; // wheel delta this frame (positive=down, negative=up)
int32_t keyModifiers; // current BIOS shift state (KEY_MOD_xxx)
+ int32_t keyFocusWinId; // window the held-key table was last cleared for (0 = none)
// Previous-frame mouse state for edge detection (button press/release
// transitions and delta-based cursor movement).
int32_t prevMouseX;
@@ -183,6 +184,45 @@ typedef void (*DvxEntropyHookT)(const uint8_t *data, int32_t len);
void dvxSetInputEntropyHook(DvxEntropyHookT fn);
+// ============================================================
+// Desktop sounds
+// ============================================================
+//
+// Events DVX plays a sound for. The sound of each is chosen on the
+// Control Panel's Sounds page (DVX.INI [sounds]); an event may have none.
+// Message boxes play the event of their icon (SoundDefaultE without one),
+// windows play SoundWindowOpenE / SoundWindowCloseE (not dialogs), and the
+// shell plays SoundStartE and SoundExitE. The sound library plays them
+// through the hook it sets when it loads; without it nothing is heard.
+
+typedef enum {
+ SoundDefaultE, // the default beep (dvxBeep)
+ SoundStartE, // DVX started
+ SoundExitE, // DVX is ending
+ SoundErrorE,
+ SoundWarningE,
+ SoundQuestionE,
+ SoundInfoE,
+ SoundWindowOpenE,
+ SoundWindowCloseE,
+ SoundCountE
+} DvxSoundE;
+
+// Plays the sound of an event; with wait it returns once the sound has
+// played (the exit sound, before the sound device stops).
+typedef void (*DvxSoundHookT)(DvxSoundE event, bool wait);
+
+// Set by the sound library (NULL: silent).
+void dvxSetSoundHook(DvxSoundHookT fn);
+
+// Play the sound of an event now (it plays on while the caller goes on),
+// or wait until it has played. Silent when the event has no sound.
+void dvxPlaySound(DvxSoundE event);
+void dvxPlaySoundWait(DvxSoundE event);
+
+// The default beep: dvxPlaySound(SoundDefaultE).
+void dvxBeep(void);
+
// ============================================================
// Color scheme
// ============================================================
diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c
index 652756d4..a55ed572 100644
--- a/src/libs/kpunch/libdvx/dvxDialog.c
+++ b/src/libs/kpunch/libdvx/dvxDialog.c
@@ -294,6 +294,7 @@ static bool fdValidateFilename(FileDialogStateT *fd, const char *name);
static void ibOnOk(WidgetT *w);
static void iibOnOk(WidgetT *w);
static int32_t msgBoxRun(AppContextT *ctx, const char *title, const char *message, int32_t iconFlags, const char * const *btnLabels, const int32_t *btnIds, int32_t btnCount, int32_t closeResult);
+static DvxSoundE msgBoxSound(int32_t iconFlags);
static void onButtonClick(WidgetT *w);
static void onMsgBoxPaint(WindowT *win, RectT *dirtyArea);
static void restoreModal(AppContextT *ctx, WindowT *prevModal, int32_t prevModalId);
@@ -1669,6 +1670,7 @@ static int32_t msgBoxRun(AppContextT *ctx, const char *title, const char *messag
// Initial paint (window is already correctly sized, don't call dvxFitWindow)
RectT fullRect = { 0, 0, win->contentW, win->contentH };
win->onPaint(win, &fullRect);
+ dvxPlaySound(msgBoxSound(iconFlags));
// Nested event loop -- blocks here until user clicks a button or
// closes. A message box opened from within a file dialog (e.g. an
@@ -1680,6 +1682,27 @@ static int32_t msgBoxRun(AppContextT *ctx, const char *title, const char *messag
}
+// The desktop sound of a message box with this icon.
+static DvxSoundE msgBoxSound(int32_t iconFlags) {
+ switch (iconFlags) {
+ case MB_ICONINFO:
+ return SoundInfoE;
+
+ case MB_ICONWARNING:
+ return SoundWarningE;
+
+ case MB_ICONERROR:
+ return SoundErrorE;
+
+ case MB_ICONQUESTION:
+ return SoundQuestionE;
+
+ default:
+ return SoundDefaultE;
+ }
+}
+
+
// Message box button: record its ID and end the dialog.
static void onButtonClick(WidgetT *w) {
MsgBoxStateT *mb = (MsgBoxStateT *)w->window->userData;
diff --git a/src/libs/kpunch/libdvx/dvxDriver.c b/src/libs/kpunch/libdvx/dvxDriver.c
new file mode 100644
index 00000000..4665f65f
--- /dev/null
+++ b/src/libs/kpunch/libdvx/dvxDriver.c
@@ -0,0 +1,174 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// dvxDriver.c -- driver modules (see dvxDrv.h)
+
+#include "dvxDrv.h"
+#include "dvxPlat.h"
+#include "thirdparty/stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+DvxDriverModT dvxDriverKeep(DvxDriverModT *mods, int32_t index);
+DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls);
+void dvxDriversFree(DvxDriverModT *mods);
+void dvxDriverUnload(DvxDriverModT *mod);
+static bool findDescriptor(void *handle, const DvxDriverClassT *cls, DvxDriverModT *out);
+static void insertSorted(DvxDriverModT **mods, const DvxDriverModT *mod);
+static bool modBefore(const DvxDriverModT *a, const DvxDriverModT *b);
+static bool nameTaken(const DvxDriverModT *mods, const char *name);
+
+
+DvxDriverModT dvxDriverKeep(DvxDriverModT *mods, int32_t index) {
+ DvxDriverModT mod = mods[index];
+
+ mods[index].handle = NULL;
+ return mod;
+}
+
+
+// Every valid module, highest priority first; a name is kept by its
+// highest priority module (the list is sorted before names are checked,
+// so the result never depends on the directory order).
+DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls) {
+ char **names = dvxReadDir(dir);
+ DvxDriverModT *all = NULL;
+ DvxDriverModT *mods = NULL;
+
+ if (!names) {
+ dvxLog("Drivers: none in %s", dir);
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
+ char path[DVX_MAX_PATH];
+ DvxDriverModT mod;
+
+ if (!dvxHasExt(names[i], DVX_DRIVER_EXT)) {
+ continue;
+ }
+
+ snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dir, names[i]);
+ mod.handle = dlopen(path, RTLD_NOW);
+
+ if (!mod.handle) {
+ dvxLog("Drivers: %s did not load: %s", path, dlerror());
+ continue;
+ }
+
+ if (!findDescriptor(mod.handle, cls, &mod)) {
+ dvxLog("Drivers: %s is not a driver for this version of DVX", path);
+ dlclose(mod.handle);
+ continue;
+ }
+
+ insertSorted(&all, &mod);
+ }
+
+ dvxReadDirFree(names);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(all); i++) {
+ if (nameTaken(mods, all[i].desc->name)) {
+ dvxLog("Drivers: another driver named '%s' in %s is not used", all[i].desc->name, dir);
+ dlclose(all[i].handle);
+ continue;
+ }
+
+ arrput(mods, all[i]);
+ }
+
+ arrfree(all);
+ return mods;
+}
+
+
+void dvxDriversFree(DvxDriverModT *mods) {
+ for (int32_t i = 0; i < (int32_t)arrlen(mods); i++) {
+ dvxDriverUnload(&mods[i]);
+ }
+
+ arrfree(mods);
+}
+
+
+void dvxDriverUnload(DvxDriverModT *mod) {
+ if (mod->handle) {
+ dlclose(mod->handle);
+ }
+
+ mod->handle = NULL;
+ mod->desc = NULL;
+}
+
+
+// The first kind whose descriptor the module exports and passes the
+// checks.
+static bool findDescriptor(void *handle, const DvxDriverClassT *cls, DvxDriverModT *out) {
+ for (int32_t k = 0; k < cls->kinds; k++) {
+ const DvxDriverHeaderT *desc = (const DvxDriverHeaderT *)dlsym(handle, cls->symbols[k]);
+
+ if (desc && desc->abiVersion == DVX_ABI_VERSION && desc->size >= cls->minSizes[k] && desc->name && (!cls->valid || cls->valid(desc, k))) {
+ out->desc = desc;
+ out->kind = k;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+static void insertSorted(DvxDriverModT **mods, const DvxDriverModT *mod) {
+ int32_t at = (int32_t)arrlen(*mods);
+
+ arrput(*mods, *mod);
+
+ while (at > 0 && modBefore(mod, &(*mods)[at - 1])) {
+ (*mods)[at] = (*mods)[at - 1];
+ at--;
+ }
+
+ (*mods)[at] = *mod;
+}
+
+
+static bool modBefore(const DvxDriverModT *a, const DvxDriverModT *b) {
+ return a->desc->priority > b->desc->priority || (a->desc->priority == b->desc->priority && strcasecmp(a->desc->name, b->desc->name) < 0);
+}
+
+
+static bool nameTaken(const DvxDriverModT *mods, const char *name) {
+ for (int32_t i = 0; i < (int32_t)arrlen(mods); i++) {
+ if (strcasecmp(mods[i].desc->name, name) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/libs/kpunch/libdvx/dvxDrv.h b/src/libs/kpunch/libdvx/dvxDrv.h
new file mode 100644
index 00000000..f0972a39
--- /dev/null
+++ b/src/libs/kpunch/libdvx/dvxDrv.h
@@ -0,0 +1,97 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// dvxDrv.h -- driver modules: one model for every device class
+//
+// A driver is a module of its own, a file .drv in its device
+// class's folder (built like a .lib, but not loaded by the loader). It
+// exports one descriptor under a name the class chooses; the descriptor
+// starts with a DvxDriverHeaderT (write it with DVX_DRIVER_HDR), which
+// carries the DVX_ABI_VERSION the driver was built for, the descriptor's
+// size, a short name and a priority.
+//
+// The library of a device class calls dvxDriversLoad when it first needs
+// its devices: every *.drv in the folder is loaded, its descriptor looked
+// up and checked (ABI, size, name, the class's own check), and the valid
+// ones come back sorted by priority (highest first, by name on a tie).
+// Nothing lists drivers. The library then probes them and keeps only
+// the ones it uses: it takes a module out of the list (dvxDriverKeep)
+// and unloads it with dvxDriverUnload when done; dvxDriversFree unloads
+// every module still in the list. So a driver for hardware that is not
+// there, or for a device class nobody uses, never stays in memory.
+#ifndef DVX_DRV_H
+#define DVX_DRV_H
+
+#include "dvxTypes.h"
+
+#include
+#include
+
+// File extension of driver modules
+#define DVX_DRIVER_EXT ".drv"
+
+// The start of every driver descriptor.
+typedef struct {
+ uint32_t abiVersion; // DVX_ABI_VERSION it was built for
+ uint32_t size; // sizeof the whole descriptor
+ const char *name; // short name, lower case, no blanks (DVX.INI settings use it)
+ int32_t priority; // tried from the highest down
+} DvxDriverHeaderT;
+
+// A descriptor's header: DVX_DRIVER_HDR(AudioDriverT, "sb", 100).
+#define DVX_DRIVER_HDR(type, drvName, drvPriority) { DVX_ABI_VERSION, sizeof(type), drvName, drvPriority }
+
+// A loaded driver module. kind is the index of the descriptor symbol
+// it exports (a class may have several kinds of driver).
+typedef struct {
+ void *handle; // NULL once kept or unloaded
+ const DvxDriverHeaderT *desc;
+ int32_t kind;
+} DvxDriverModT;
+
+// What a class looks for: the descriptor symbol of each kind (as dlsym
+// wants it: DVX_SYM("name")), the smallest descriptor size it accepts,
+// and its own check of a descriptor (functions present; NULL: none).
+typedef struct {
+ const char *const *symbols;
+ const uint32_t *minSizes;
+ int32_t kinds;
+ bool (*valid)(const DvxDriverHeaderT *desc, int32_t kind);
+} DvxDriverClassT;
+
+// Load every valid driver in dir (relative to the DVX directory). A
+// module that does not load, exports no valid descriptor or repeats a
+// name already taken (by a higher priority) is logged and unloaded.
+// Returns an stb_ds array (NULL: none) for dvxDriversFree.
+DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls);
+
+// Unload every module still in mods (not kept) and free the array.
+void dvxDriversFree(DvxDriverModT *mods);
+
+// Take mods[index] out of the list so dvxDriversFree leaves it loaded;
+// returns it for dvxDriverUnload.
+DvxDriverModT dvxDriverKeep(DvxDriverModT *mods, int32_t index);
+
+// Unload a kept module (nothing when it is not loaded).
+void dvxDriverUnload(DvxDriverModT *mod);
+
+#endif // DVX_DRV_H
diff --git a/src/libs/kpunch/libdvx/dvxPrefs.c b/src/libs/kpunch/libdvx/dvxPrefs.c
index 9637acce..6bfa644d 100644
--- a/src/libs/kpunch/libdvx/dvxPrefs.c
+++ b/src/libs/kpunch/libdvx/dvxPrefs.c
@@ -72,6 +72,15 @@ struct PrefsHandleT {
bool loadFailed; // file exists but could not be read: prefsSave refuses
};
+// A settings file saved while DVX's folder cannot be written to
+// (platformIsReadOnly): kept here until DVX exits, and read back by
+// prefsLoad in place of the file on disk.
+typedef struct {
+ char *path;
+ char *text;
+ int32_t len;
+} PrefsMemFileT;
+
// Decimal text of an int32_t including sign and NUL
#define PREFS_INT_TEXT_MAX 16
@@ -86,6 +95,9 @@ struct PrefsHandleT {
// Words prefsGetBool accepts (case-insensitive, whole value)
static const char *const sTrueWords[] = { "true", "yes", "1" };
+
+// Settings files held in memory (stb_ds array)
+static PrefsMemFileT *sMemFiles = NULL;
static const char *const sFalseWords[] = { "false", "no", "0" };
@@ -117,13 +129,18 @@ const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT] = {
// ============================================================
static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key, const char *value, const char *raw);
+static void appendText(char **buf, const char *a, const char *b, const char *c);
static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key);
static int32_t findSection(PrefsHandleT *h, const char *section);
+static char *formatText(const PrefsHandleT *h);
static void freeEntry(PrefsEntryT *e);
static bool matchWord(const char *val, const char *const *words, int32_t count);
+static char *memFileCopy(const char *path, int32_t *len);
+static bool memFileStore(const char *path, const char *text, int32_t len);
static void parseLine(PrefsHandleT *h, char *line, char **currentSection);
static char *strdupOrNull(const char *s);
static char *trimInPlace(char *buf);
+static bool writeText(const char *filename, const char *text, int32_t len);
// Appends one entry; every string is copied (NULL stays NULL). Out of
@@ -147,6 +164,18 @@ static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, cons
}
+// Add a, b and c (each may be NULL) to the stb_ds char array *buf.
+static void appendText(char **buf, const char *a, const char *b, const char *c) {
+ const char *parts[] = { a, b, c };
+
+ for (int32_t i = 0; i < (int32_t)(sizeof(parts) / sizeof(parts[0])); i++) {
+ for (const char *p = parts[i]; p && *p; p++) {
+ arrput(*buf, *p);
+ }
+ }
+}
+
+
static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) {
for (int32_t i = 0; i < arrlen(h->entries); i++) {
PrefsEntryT *e = &h->entries[i];
@@ -173,6 +202,28 @@ static int32_t findSection(PrefsHandleT *h, const char *section) {
}
+// The file's text: every entry, CR LF after each (stb_ds char array, no
+// NUL; NULL when there are no entries).
+static char *formatText(const PrefsHandleT *h) {
+ char *buf = NULL;
+
+ for (int32_t i = 0; i < arrlen(h->entries); i++) {
+ const PrefsEntryT *e = &h->entries[i];
+
+ if (e->raw) {
+ appendText(&buf, e->raw, PREFS_EOL, NULL);
+ } else if (e->kind == PrefsLineSectionE) {
+ appendText(&buf, "[", e->section, "]" PREFS_EOL);
+ } else if (e->kind == PrefsLineKeyE) {
+ appendText(&buf, e->key, " = ", e->value);
+ appendText(&buf, PREFS_EOL, NULL, NULL);
+ }
+ }
+
+ return buf;
+}
+
+
static void freeEntry(PrefsEntryT *e) {
free(e->section);
free(e->key);
@@ -197,6 +248,59 @@ static bool matchWord(const char *val, const char *const *words, int32_t count)
}
+// A copy of the text held for path (NUL-terminated, for free); NULL when
+// none is held.
+static char *memFileCopy(const char *path, int32_t *len) {
+ for (int32_t i = 0; i < arrlen(sMemFiles); i++) {
+ if (strcasecmp(sMemFiles[i].path, path) == 0) {
+ char *copy = (char *)malloc((size_t)sMemFiles[i].len + 1);
+
+ if (copy) {
+ memcpy(copy, sMemFiles[i].text, (size_t)sMemFiles[i].len);
+ copy[sMemFiles[i].len] = '\0';
+ *len = sMemFiles[i].len;
+ }
+
+ return copy;
+ }
+ }
+
+ return NULL;
+}
+
+
+// Hold text as the content of path (replacing what was held).
+static bool memFileStore(const char *path, const char *text, int32_t len) {
+ char *copy = (char *)malloc((size_t)len + 1);
+
+ if (!copy) {
+ return false;
+ }
+
+ memcpy(copy, text, (size_t)len);
+ copy[len] = '\0';
+
+ for (int32_t i = 0; i < arrlen(sMemFiles); i++) {
+ if (strcasecmp(sMemFiles[i].path, path) == 0) {
+ free(sMemFiles[i].text);
+ sMemFiles[i].text = copy;
+ sMemFiles[i].len = len;
+ return true;
+ }
+ }
+
+ PrefsMemFileT m = { strdup(path), copy, len };
+
+ if (!m.path) {
+ free(copy);
+ return false;
+ }
+
+ arrput(sMemFiles, m);
+ return true;
+}
+
+
// Classifies one physical line (modified in place) and appends it. A
// "[name]" line starts a section; a line with '=' that is not a comment
// is a key (key and value trimmed); everything else is kept verbatim.
@@ -395,10 +499,14 @@ PrefsHandleT *prefsLoad(const char *filename) {
h->filePath = strdup(filename);
int32_t len = 0;
- char *text = platformReadFile(filename, &len);
+ char *text = memFileCopy(filename, &len);
struct stat st;
char bakPath[DVX_MAX_PATH];
+ if (!text) {
+ text = platformReadFile(filename, &len);
+ }
+
// Missing: a crash inside platformReplaceFile may have left the only
// copy under the backup name. A save then writes it back to filename.
if (!text && stat(filename, &st) != 0 && platformPathSibling(bakPath, sizeof(bakPath), filename, PLATFORM_BAK_EXT)) {
@@ -459,47 +567,24 @@ bool prefsSave(PrefsHandleT *h) {
}
-// Writes the entries to the .$ sibling (PLATFORM_TMP_EXT; the
-// extension is replaced, so 8.3 names stay valid) and swaps it in only
-// when every write succeeded, so a full disk or I/O error leaves the previous file
-// intact and reports failure.
+// Writes the file (see writeText). When DVX's folder cannot be written
+// to (read-only media) and the write fails, the text is kept in memory
+// instead and prefsLoad reads it back until DVX exits.
bool prefsSaveAs(PrefsHandleT *h, const char *filename) {
- char tmpPath[DVX_MAX_PATH];
- char bakPath[DVX_MAX_PATH];
-
- if (!h || !filename || !platformPathSibling(tmpPath, sizeof(tmpPath), filename, PLATFORM_TMP_EXT) || !platformPathSibling(bakPath, sizeof(bakPath), filename, PLATFORM_BAK_EXT)) {
+ if (!h || !filename) {
return false;
}
- FILE *fp = fopen(tmpPath, "wb");
+ char *text = formatText(h);
+ int32_t len = (int32_t)arrlen(text);
+ bool ok = writeText(filename, text ? text : "", len);
- if (!fp) {
- return false;
+ if (!ok && platformIsReadOnly()) {
+ ok = memFileStore(filename, text ? text : "", len);
}
- for (int32_t i = 0; i < arrlen(h->entries); i++) {
- const PrefsEntryT *e = &h->entries[i];
-
- if (e->raw) {
- fprintf(fp, "%s" PREFS_EOL, e->raw);
- } else if (e->kind == PrefsLineSectionE) {
- fprintf(fp, "[%s]" PREFS_EOL, e->section);
- } else if (e->kind == PrefsLineKeyE) {
- fprintf(fp, "%s = %s" PREFS_EOL, e->key, e->value);
- }
- }
-
- bool ok = !ferror(fp);
-
- // fclose flushes: a late write error surfaces here
- ok = (fclose(fp) == 0) && ok;
-
- if (!ok) {
- remove(tmpPath);
- return false;
- }
-
- return platformReplaceFile(tmpPath, filename, bakPath);
+ arrfree(text);
+ return ok;
}
@@ -614,3 +699,35 @@ static char *trimInPlace(char *buf) {
dvxTrimRight(p);
return p;
}
+
+
+// Writes text to the .$ sibling (PLATFORM_TMP_EXT; the
+// extension is replaced, so 8.3 names stay valid) and swaps it in only
+// when every write succeeded, so a full disk or I/O error leaves the
+// previous file intact and reports failure.
+static bool writeText(const char *filename, const char *text, int32_t len) {
+ char tmpPath[DVX_MAX_PATH];
+ char bakPath[DVX_MAX_PATH];
+
+ if (!platformPathSibling(tmpPath, sizeof(tmpPath), filename, PLATFORM_TMP_EXT) || !platformPathSibling(bakPath, sizeof(bakPath), filename, PLATFORM_BAK_EXT)) {
+ return false;
+ }
+
+ FILE *fp = fopen(tmpPath, "wb");
+
+ if (!fp) {
+ return false;
+ }
+
+ bool ok = (int32_t)fwrite(text, 1, (size_t)len, fp) == len;
+
+ // fclose flushes: a late write error surfaces here
+ ok = (fclose(fp) == 0) && ok;
+
+ if (!ok) {
+ remove(tmpPath);
+ return false;
+ }
+
+ return platformReplaceFile(tmpPath, filename, bakPath);
+}
diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h
index 40ff3303..f0d49a34 100644
--- a/src/libs/kpunch/libdvx/dvxTypes.h
+++ b/src/libs/kpunch/libdvx/dvxTypes.h
@@ -65,7 +65,7 @@
// src/test/abi/abiSizes.txt (make -C src/test/abi update) in the same
// change; the abi test fails when a public struct changes size without a
// bump, and when the table was not regenerated after one.
-#define DVX_ABI_VERSION 1
+#define DVX_ABI_VERSION 3
// A module's stamp is named , e.g. the
// stamp of LIBS/KPUNCH/LIBDVX/LIBDVX.LIB is libdvxAbiVersion.
@@ -584,6 +584,54 @@ typedef struct {
#define KEY_CTRL_X 0x18
#define KEY_CTRL_Z 0x1A
+// ============================================================
+// Raw keys (held-key table and game input)
+// ============================================================
+//
+// A raw key code names one physical key: its PC set-1 make code (1-127)
+// with KEY_RAW_EXT added for the keys the keyboard sends behind an 0xE0
+// prefix (grey arrows and navigation keys, right Ctrl/Alt, keypad Enter
+// and /). Unlike the BIOS scancodes above, the code is the same whatever
+// modifier is held, and Shift, Ctrl and Alt are keys of their own.
+// Letters, digits and punctuation use their plain BIOS scancode
+// (KEY_SCAN_*), which is the set-1 make code.
+#define KEY_RAW_EXT 0x80
+#define KEY_RAW_SCAN_MASK 0x7F
+#define KEY_RAW_COUNT 256 // every raw code, for the held-key snapshot
+#define KEY_RAW_LSHIFT 0x2A
+#define KEY_RAW_RSHIFT 0x36
+#define KEY_RAW_LCTRL 0x1D
+#define KEY_RAW_RCTRL (0x1D | KEY_RAW_EXT)
+#define KEY_RAW_LALT 0x38
+#define KEY_RAW_RALT (0x38 | KEY_RAW_EXT)
+#define KEY_RAW_F11 0x57
+#define KEY_RAW_F12 0x58
+#define KEY_RAW_UP (KEY_SCAN_UP | KEY_RAW_EXT)
+#define KEY_RAW_DOWN (KEY_SCAN_DOWN | KEY_RAW_EXT)
+#define KEY_RAW_LEFT (KEY_SCAN_LEFT | KEY_RAW_EXT)
+#define KEY_RAW_RIGHT (KEY_SCAN_RIGHT | KEY_RAW_EXT)
+#define KEY_RAW_KP_ENTER (KEY_SCAN_ENTER | KEY_RAW_EXT)
+// Pause has no break code: it is reported as a press and a release at once.
+#define KEY_RAW_PAUSE (0x45 | KEY_RAW_EXT)
+
+// Raw code of a key from its make code and 0xE0 flag.
+#define KEY_RAW_CODE(scan, extended) ((int32_t)(((scan) & KEY_RAW_SCAN_MASK) | ((extended) ? KEY_RAW_EXT : 0)))
+
+// Left and right Shift in the BIOS shift-state byte (KEY_MOD_SHIFT is both)
+#define KEY_MOD_RSHIFT 0x01
+#define KEY_MOD_LSHIFT 0x02
+
+// One key going down or up, as the keyboard reported it (WindowT.onRawKey,
+// platformKeyEventRead). modifiers are the KEY_MOD_* bits of the held-key
+// table just after this event (left and right Shift apart).
+typedef struct {
+ uint8_t scancode; // set-1 make code (1-127)
+ bool extended; // sent behind an 0xE0 prefix
+ bool down; // true = pressed, false = released
+ bool repeat; // a typematic repeat of a key already down
+ int32_t modifiers; // KEY_MOD_* from the held-key table
+} RawKeyEventT;
+
typedef struct {
int32_t cmdId; // command ID passed to onMenu
int32_t normKey; // normalized key (see dvxAddAccel)
@@ -679,6 +727,11 @@ typedef struct WindowT {
bool modal;
bool destroyPending; // hidden and queued for deferred destruction
bool iconNeedsRefresh; // true when contentBuf has changed since last icon refresh
+ // Game input: while this window has focus, every key except Alt+Tab,
+ // Ctrl+Esc and Alt+F4 goes to onKey (no F1 help, F10/Alt menus,
+ // accelerators or hotkeys), and onRawKey gets each key going down
+ // and up.
+ bool gameInput;
// Paint state: PAINT_NONE = idle, PAINT_PARTIAL = only dirty widgets,
// PAINT_FULL = clear background + relayout + repaint all.
// wgtInvalidatePaint sets PARTIAL, wgtInvalidate sets FULL.
@@ -741,6 +794,7 @@ typedef struct WindowT {
int32_t (*onCursorQuery)(struct WindowT *win, int32_t x, int32_t y); // return CURSOR_* or 0 for default
void (*onFocus)(struct WindowT *win); // window gained focus
void (*onBlur)(struct WindowT *win); // window lost focus
+ void (*onRawKey)(struct WindowT *win, const RawKeyEventT *evt); // gameInput windows only
} WindowT;
// ============================================================
diff --git a/src/libs/kpunch/libdvx/games.dhs b/src/libs/kpunch/libdvx/games.dhs
new file mode 100644
index 00000000..7256a658
--- /dev/null
+++ b/src/libs/kpunch/libdvx/games.dhs
@@ -0,0 +1,396 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# games.dhs -- developer guide: writing games for DVX, and porting a game
+# library to DVX.
+
+.section Technical Reference (for Developers)|Writing Games for DVX
+.topic games.intro
+.title Writing Games for DVX
+.toc 0 Writing Games for DVX
+.index Games
+.index Game template
+.index basicgame template
+.index Game loop
+
+.h1 Writing Games for DVX
+
+A game in DVX is a program with its own main loop (hasMainLoop = true in its SHELL_APP_DESCRIPTOR). DVX gives it its own task: appMain runs the game loop itself, draws into the game's window, and waits for each frame on the high-resolution clock while the other programs keep running. Nothing in DVX has to stop for a game: the desktop, other programs, the Task Manager (Ctrl+Esc) and help all stay available.
+
+.h2 Starting From a Template
+
+.code
+ bin/sdk/newProject.sh game myGame a C game
+ bin/sdk/newProject.sh basicgame myGame a DVX BASIC game
+.endcode
+
+The C template is a small but complete game, and every part of this guide is shown in it: a game input window, a 320x200 screen of 8-bit colour numbers drawn scaled into the window on any desktop colour depth, frame pacing, the arrow keys and a joystick, a sound effect and music carried inside the .app as resources, tones for the end of a round, the best score saved in the program's settings folder, its own help file (F1) and icon. The BASIC template does the same in a few dozen lines with FRAMEWAIT, GameKeyDown, STICK, STRIG and PLAY.
+
+.h2 The Shape of a Game
+
+.code
+int32_t appMain(DxeAppContextT *ctx) {
+ GameFramePacerT pacer;
+
+ createGameWindow(ctx); // gameInput = true, onClose sets quit
+ gameFrameInit(&pacer, 60);
+
+ while (!quit) {
+ uint32_t missed = gameFrameWait(&pacer); // yields until the frame is due
+
+ audioPump(); // sound end calls and streams
+ musicPump();
+ update(missed + 1); // move by the frames that passed
+ draw(); // into the game's own framebuffer
+ present(); // changed parts -> window, dvxInvalidateRect
+ }
+
+ cleanUp(); // or just return: DVX releases the rest
+ dvxDestroyWindow(ctx->shellCtx, win);
+ return 0;
+}
+.endcode
+
+The sections that follow cover each part.
+
+.link games.input Keyboard, Mouse and Joysticks
+.link games.timing Timing and Frame Pacing
+.link games.screen The Screen: Framebuffers and Palettes
+.link games.sound Sound, Music and MIDI
+.link games.files Data Files and Saved Games
+.link games.packaging Packaging and Ending
+.link games.basic Games in DVX BASIC
+.link games.porting Porting a Game Library to DVX
+
+.topic games.input
+.title Keyboard, Mouse and Joysticks
+.toc 1 Keyboard, Mouse and Joysticks
+.index Game Input
+.index gameInput
+.index onRawKey
+.index RawKeyEventT
+.index Held keys
+.index Reserved keys
+
+.h1 Keyboard, Mouse and Joysticks
+
+.h2 Game Input Windows
+
+Set gameInput on the game's window and give it an onRawKey callback if it wants every key going down and up:
+
+.code
+win->gameInput = true;
+win->onKey = gameKey; // typed keys: every key but three
+win->onRawKey = gameRawKey; // each key going down and up
+.endcode
+
+While the window has the focus, DVX keeps only three keys, so the user can always switch away, open the Task Manager or close the game:
+
+.table
+ Key Always does
+ --- -----------
+ Alt+Tab Switch to the next window
+ Ctrl+Esc Open the Task Manager
+ Alt+F4 Close the window (its onClose)
+.endtable
+
+Every other key, F1, F10, Alt+letter and the accelerators included, goes to onKey instead of opening help or menus. The game decides what F1 does (the template opens its help with shellOpenHelp); its menus still work with the mouse. onRawKey gets a RawKeyEventT for every key going down or up: the set-1 scancode with an extended flag for the grey keys, down or up, whether it is a typematic repeat, and the modifiers. Shift, Ctrl and Alt are keys of their own there, left and right apart.
+
+.h2 Held Keys
+
+For "is it held right now" use gameKeyIsDown(win, rawCode) with a raw key code (KEY_RAW_UP, KEY_RAW_LCTRL, or KEY_RAW_CODE(scan, extended) for any key); it is true only while win has the focus. platformKeyIsDown and platformKeyStateGet read the table itself. When the focus moves away, the game gets a release for every key still held and the table starts over, so no key stays stuck down. A key already held when the window gets the focus is seen once it repeats.
+
+.h2 Typed Characters and the Mouse
+
+onKey gets typed characters (with Shift and the keyboard layout applied) as it does in any window; keep them in the game's own queue if the game reads text. onMouse gets the pointer in content-area pixels: divide by the scale the game draws at, and clamp.
+
+.h2 Joysticks
+
+joyCount says how many joysticks there are; joyOpen(ctx, device) gives the program a handle (closed for it when it ends) and joyRead fills a JoyStateT: axes calibrated to -32768..32767 with a dead zone, buttons (a press between two reads is not lost), hats. Read it once a frame. Programs that wait for input can take events instead (joySetEventFn). With no joystick connected the keyboard joystick stands in (the arrow keys and four buttons, chosen in the Control Panel), answering only while the program's window has the focus; so a game that reads both the keys and the joystick should let the keys win rather than add the two up. Polling a game port joystick costs about 4 to 7 percent of the processor while a program has one open. See the Joystick Library.
+
+.topic games.timing
+.title Timing and Frame Pacing
+.toc 1 Timing and Frame Pacing
+.index Frame Pacing
+.index gameFrameWait
+.index platformMicros
+.index Timer Service
+
+.h1 Timing and Frame Pacing
+
+platformMicros() is a monotonic microsecond clock from the PIT, the timer chip every PC has (with the CPU's time-stamp counter between timer interrupts when there is one). Use it for any game time: elapsed milliseconds are platformMicros() / 1000.
+
+.h2 Frame Pacing
+
+gameFrameInit(&pacer, hz) starts a frame clock (1 to GAME_FRAME_MAX_HZ frames a second); gameFrameWait(&pacer) once a frame yields to the other programs until the next frame is due and returns how many frames were missed. Frame n starts at start + n / hz, so the rate never drifts; frames that already ended while the game was busy are skipped, not caught up (pacer.missed counts them). Move by missed + 1 frames' worth (with a limit, so a long stall does not teleport the player) and the game runs at the same speed on a slow machine. pacer.frame is a frame counter.
+
+There is no vertical retrace wait: a game window is composited with the rest of the desktop, and the frame rate is a virtual one. 60 or 70 frames a second both work; pick what the game was designed for.
+
+.h2 The Timer Service
+
+For work that must happen at a steady rate however busy the programs are (a music player, a sample-accurate sequencer), the timer service calls a function up to 1000 times a second from the timer interrupt: gameTimerAdd(ctx, hz, fn, userData), removed when the program ends (or platformTimerAdd). DVX speeds the timer interrupt up only as far as the fastest timer needs and keeps the DOS clock at its normal rate. The callback runs at interrupt time: no C library, no DVX calls, no floating point, and its code and data pinned with platformLockRegion. See Timer Service in the platform reference.
+
+.topic games.screen
+.title The Screen: Framebuffers and Palettes
+.toc 1 The Screen: Framebuffers and Palettes
+.index Framebuffer
+.index Palette
+.index 8-bit desktop
+.index gamePalNearest
+.index GamePalCacheT
+.index contentBuf
+
+.h1 The Screen: Framebuffers and Palettes
+
+A window's content buffer (win->contentBuf, win->contentPitch bytes per row, contentW x contentH pixels) is in the desktop's own pixel format: 8, 15, 16 or 32 bits per pixel, as dvxGetDisplay(ctx)->format says. A game that must look the same everywhere draws into a framebuffer of its own (for example 320x200 bytes, each a colour number into a 256-colour game palette) and converts only what changed into the content buffer each frame.
+
+.h2 The Colour Lookup Table
+
+Keep one desktop pixel value per game colour, and rebuild it when the game palette changes or the desktop's pixel format or palette does (the user can change the video mode while the game runs):
+
+.code
+const DisplayT *d = dvxGetDisplay(ctx);
+
+for (int32_t i = 0; i < 256; i++) {
+ const uint8_t *c = &gamePal[i * 3];
+
+ lut[i] = d->palette ? gamePalNearest(d->palette, c[0], c[1], c[2]) // 8-bit desktop
+ : packColor(d, c[0], c[1], c[2]); // 15, 16, 32 bits
+}
+.endcode
+
+.table
+ Desktop Pixel size Game colours
+ ------- ---------- ------------
+ 32-bit 4 bytes Exact.
+ 16-bit 2 bytes 5-6-5 bits of red, green, blue: very close.
+ 15-bit 2 bytes 5 bits each: very close.
+ 8-bit 1 byte The nearest of the desktop's 256 colours.
+.endtable
+
+On an 8-bit desktop all windows share the one desktop palette: a 6x6x6 colour cube, a 16-step grey ramp and the chrome colours (see dvxPal.h). A game cannot change it; gamePalNearest finds the closest entry, and a GamePalCacheT remembers the answer per 15-bit colour when there are many colours to map (gamePalCacheSync starts over if the desktop palette changed). Gradients come out banded; design for it, or ask players to use a 15-bit or better desktop for the best look.
+
+Palette animation (colour cycling, fades) works at every depth: change the game palette entries, look up just those entries again, and redraw the pixels that use them. Games whose hardware picks a palette per scan line keep one small table per palette and pick the table per row.
+
+.h2 Presenting a Frame
+
+.list
+.item Keep a list of the framebuffer rectangles (or rows) that changed this frame.
+.item For each, expand the colour numbers through the table into contentBuf, repeating each pixel scale times across and each row scale times down, then call dvxInvalidateRect(ctx, win, x * scale, y * scale, w * scale, h * scale) so DVX puts it on the screen.
+.item Give the window an onPaint that converts the whole framebuffer (without invalidating): DVX calls it when the window is first shown and after a video mode change, when contentBuf has been made again.
+.item Clip to contentW and contentH, and do nothing while contentBuf is NULL.
+.endlist
+
+Size the window so its content area is exactly the framebuffer times a whole scale that fits the screen (the template computes the scale from the display size and the chrome constants, then corrects with dvxResizeWindow after adding the menu bar). Copying only what changed matters on slower machines: a full 320x200 frame at 2x on a 16-bit desktop is 256 KB of pixels to convert and composite.
+
+.topic games.sound
+.title Sound, Music and MIDI
+.toc 1 Sound, Music and MIDI
+.index Sound
+.index Music
+.index MIDI
+.index audioPump
+
+.h1 Sound, Music and MIDI
+
+Games play sound on whatever the computer has: a Sound Blaster family card, an FM synthesizer chip, a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.
+
+.h2 Sound Effects and Tones (audio.h)
+
+.list
+.item audioOpen() gives the program an audio context (the first one opened starts the sound device).
+.item audioSampleLoad / audioSampleLoadMem load a WAV or VOC file (from memory: a resource, say); audioSampleCreate makes a sample from raw PCM (8-bit unsigned or 16-bit signed, mono or stereo, any rate from 4000 to 48000 Hz).
+.item audioPlay(sample, volume, pan, loop) plays it on a voice; the voice id sets volume, pan and pitch while it plays (audioVoiceSet...), audioVoiceStop stops it, and audioSetEndFn tells when it ends.
+.item audioTone plays a square, triangle or noise tone of a frequency and length; audioNotesOpen gives a queue of notes and rests played back to back, each to the exact sample (jingles, chip-style tunes).
+.item audioStreamOpen takes the game's own sound at any rate: written with audioStreamWrite, or asked for by a fill function.
+.item audioSetVolume sets the program's volume; the master volume belongs to the user (Control Panel).
+.endlist
+
+.h2 Music (music.h)
+
+musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
+
+.h2 The FM Chip and the MIDI Port
+
+For music drivers of its own, a game can allocate two-operator FM voices shared with the other programs (audioFmAlloc, audioFmWrite with the AUDIO_FM_* register numbers), take the whole FM chip for itself (audioFmRawAcquire; refused with AUDIO_ERR_BUSY while another program has it), or send bytes to the MIDI port (audioMidiAcquire, audioMidiPut). Give them back as soon as the game does not need them.
+
+.h2 Keeping Sound Fed
+
+Sampled sound is mixed from the sound card's own interrupt, so it keeps playing however busy the game is. End notifications, pull streams, modules and the software synthesizer are made in the desktop's idle time; a game that redraws every frame should call audioPump() and musicPump() once a frame so they keep up. Modules use floating point: on a computer without a maths coprocessor they are slow.
+
+See the Sound Library, the Music Library and the Joystick Library for every function.
+
+.topic games.files
+.title Data Files and Saved Games
+.toc 1 Data Files and Saved Games
+.index Data files
+.index Saved games
+.index configDir
+.index appDir
+.index Resources
+
+.h1 Data Files and Saved Games
+
+DOS has one current directory for everything, so a program never relies on it. appMain gets a DxeAppContextT with the paths it needs:
+
+.table
+ Field What it is Use it for
+ ----- ---------- ----------
+ appPath the program's .app file dvxResOpen: resources inside the .app
+ appDir the folder the .app is in read-only game data installed beside it
+ configDir the program's own settings folder saved games, high scores, options
+ helpFile the program's .hlp file shellOpenHelp (F1)
+.endtable
+
+configDir is CONFIG plus the folder the .app is in: a game in APPS\ACME\MYGAME keeps its files in CONFIG\APPS\ACME\MYGAME.
+
+.list
+.item Small data (sounds, music, a title picture) can travel inside the .app as resources: list them in NAME.res as binary (or icon, text) and read them with dvxResOpen(ctx->appPath) and dvxResRead. The template carries its sound effect and music this way.
+.item Bigger data goes in files beside the .app (the DATA variable in the Makefile packages them there): build paths from appDir, never from the current directory.
+.item Saved games go in configDir, which DVX keeps apart from the program files so a reinstall never overwrites them. Create it with shellEnsureConfigDir(ctx) before the first write and build names with shellConfigPath(ctx, "SAVE1.DAT", path, size). For settings and scores, prefsLoad and prefsSaveAs read and write INI files safely (a failed save leaves the old file intact).
+.item Use 8.3 file names (8 characters, a dot, 3 characters).
+.item platformDiskSpace tells how much room is free before a big save.
+.item dvxLog writes a line to DVX.LOG, the place to look when something goes wrong.
+.endlist
+
+.topic games.packaging
+.title Packaging and Ending
+.toc 1 Packaging and Ending
+.index exit
+.index abort
+.index Game packaging
+
+.h1 Packaging and Ending
+
+.h2 Packaging
+
+make package lays out pkg/APPS/// with NAME.app, NAME.hlp and the files listed in DATA; zip the contents of pkg/ and the player unpacks it into the DVX directory (make install DVX_DIR=... does it for you). The .app carries its icon, name, author and description for Program Manager (NAME.res). Every game should have its own help (NAME.dhs, compiled to NAME.hlp), since F1 in a game input window goes to the game: open the help from the game's F1 and from Help > Contents. A game built on a static library of its own links it in with the LIBS variable (LIBS = port/libgame.a).
+
+.h2 Ending
+
+Close the game's window and return from appMain. Every sound, music, joystick handle and timer the program opened is released when it ends, however it ends, so a crash or End Task leaves nothing playing. A game (or any program) that calls exit() or abort() ends only itself, like End Task: its windows close, its timers are removed and DVX keeps running. Handlers registered with atexit are not run; clean up before calling exit(), or return from appMain. malloc and free work as usual; DVX counts each program's memory (the Task Manager shows it).
+
+.topic games.basic
+.title Games in DVX BASIC
+.toc 1 Games in DVX BASIC
+.index FRAMEWAIT
+.index STICK
+.index STRIG
+.index GameKeyDown
+
+.h1 Games in DVX BASIC
+
+newProject.sh basicgame NAME starts a BASIC game. A Timer fires once and runs the game loop; FRAMEWAIT rate waits for the next frame while the desktop and the program's forms keep running, and the loop ends when the form closes:
+
+.code
+Sub tmrStart_Timer ()
+ tmrStart.Enabled = False
+ Do
+ MovePlayer ' GameKeyDown, STICK(0), STICK(1), STRIG(1)
+ DrawScreen ' a PictureBox: Clear, FillRect, FillCircle
+ FRAMEWAIT 30
+ Loop Until quitting ' set in the form's Unload event
+End Sub
+.endcode
+
+GameKeyDown (game.bas) tells whether a key is held; QBasic's STICK and STRIG read joysticks (the keyboard joystick when there is none); BEEP, SOUND and PLAY make sound, PLAY "MB ..." in the background so the game does not wait. sound.bas plays WAV files and tones, music.bas plays modules and MIDI files, and joystick.bas reads joysticks in full. See Sound, Joysticks and Frames and Include Libraries in the BASIC reference.
+
+.topic games.porting
+.title Porting a Game Library to DVX
+.toc 1 Porting a Game Library to DVX
+.index Porting
+.index Game library
+.index HAL
+
+.h1 Porting a Game Library to DVX
+
+Portable game libraries for DOS-era machines usually keep everything machine-specific behind a small hardware layer: start and stop, show a frame, set the palette, read input, wait for the next frame, play sound, and find files. A DVX port replaces that layer. The rest of the library, including drawing code, sprite compilers that write x86 code into memory, and asset formats built for DOS, works unchanged, because a DVX program is 32-bit protected-mode x86 code in the same flat memory as a DOS program built with the same compiler.
+
+.h2 The Model: a Window, a Task, a Virtual Frame Rate
+
+The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. The game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame.
+
+.h2 Mapping a Hardware Layer to DVX
+
+.table
+ Hardware layer DVX
+ -------------- ---
+ Start-up SHELL_APP_DESCRIPTOR (hasMainLoop), dvxCreateWindowCentered, gameInput
+ Shut-down dvxDestroyWindow, return from appMain
+ Error text, debug log dvxLog (DVX.LOG)
+ Present the back buffer colour table -> win->contentBuf, dvxInvalidateRect; onPaint
+ Palette packColor, gamePalNearest, GamePalCacheT
+ Palette per scan line one colour table per palette, chosen per row
+ Wait for vertical blank gameFrameInit, gameFrameWait
+ Frame counter, frame rate GameFramePacerT.frame, the rate given to gameFrameInit
+ Milliseconds platformMicros() / 1000
+ Held keys onRawKey, gameKeyIsDown, platformKeyStateGet
+ Typed characters onKey
+ Mouse onMouse
+ Joystick joyCount, joyOpen, joyRead
+ Periodic interrupt callback gameTimerAdd, platformLockRegion
+ Critical section platformIrqSave, platformIrqRestore
+ Audio start and stop audioOpen, audioClose
+ Sound effect slots audioSampleCreate, audioSampleLoadMem, audioPlay, audioVoiceSetPan, audioVoiceStop
+ Streamed sound audioStreamOpen, audioStreamWrite
+ Tracker modules musicLoadMem, musicPlay, musicStop, musicIsPlaying
+ Tone and noise voices audioTone, audioVoiceSetFrequency, audioVoiceSetVolume, audioNotesOpen, audioFmAlloc
+ Per-frame audio work audioPump, musicPump
+ Read-only data folder DxeAppContextT.appDir, dvxResOpen, dvxResRead
+ Save folder DxeAppContextT.configDir, shellEnsureConfigDir, shellConfigPath
+ Disk free platformDiskSpace
+ Memory malloc, free
+ Serial port the serial library (rs232Open)
+ exit() ends only the game
+.endtable
+
+.h2 Notes on the Mapping
+
+.list
+.item Start-up: make the window not resizable, with its content area the game's screen at a whole scale that fits the display; give it onClose and onPaint. audioOpen returning NULL (no sound device) is not an error.
+.item Shut-down: close what the port opened and return. Anything left (sounds, music, joystick handles, timers) is released when the program ends.
+.item Present: expand the rows or word bands that changed through the colour table into the content buffer, scaled, and invalidate them; onPaint does the whole screen. See The Screen: Framebuffers and Palettes.
+.item Palette: scale 12-bit or 18-bit colour to 0..255 per channel before looking it up. Never touch the VGA palette registers: other programs share the screen.
+.item Vertical blank: there is none in a window. The frame rate is a virtual one: use the rate the library reports (60 or 70 frames a second); truncate the frame counter to the library's width.
+.item Keyboard: raw codes are set-1 scancodes with KEY_RAW_EXT for the keys sent behind 0xE0, so a DOS port's scancode table maps directly. Never hook the keyboard interrupt.
+.item Mouse: divide content-area pixels by the scale and clamp.
+.item Joystick: axes are -32768..32767 after calibration and dead zone; divide down to the library's range, or compare with a threshold for a digital stick. Buttons are bits. Never read the game port directly; calibration lives in the Control Panel.
+.item Timer callbacks run inside the timer interrupt: no C library, no DVX calls, no floating point, code and data pinned. Never reprogram the PIT or hook the timer interrupt.
+.item Sound effects: DVX's 8-bit samples are unsigned; flip the top bit of signed 8-bit data or convert it to 16-bit. Stream fill functions run in program time, not in an interrupt.
+.item Files: the library's fixed "DATA" and save folders become run-time paths under appDir and configDir; use remove() to delete a save.
+.item Serial: one owner per COM port, so the port uses DVX's serial library rather than programming the UART.
+.item exit() ends only the game, like End Task; atexit handlers do not run.
+.endlist
+
+.h2 Things to Decide
+
+.list
+.item Shared library or static: built as a DVX library (.lib, loaded when DVX starts) the port is shared by every game, but its global state (the stage, the key table) means one game at a time. Linked into each game's .app (the LIBS variable) every game has its own copy, several can run at once, and nothing is loaded while no game runs.
+.item Paths: libraries that open "DATA/..." or write saves to a fixed folder need a run-time root (appDir and configDir) instead of compile-time paths.
+.item Speed: every frame costs a conversion and a composite. Present only what changed; a 1x scale on a 486 and larger scales on faster machines.
+.item Tearing: there is no vertical retrace in a window. Games that depend on raster timing need rework.
+.item The 256-colour desktop: nearest colours only; say so in the game's help if it needs 15-bit colour or better.
+.endlist
+
+.h2 Checking a Port
+
+The library's own drawing tests (hashes of the back buffer after each drawing call) should give the same answers as on DOS, since the drawing code is the same. Test in a 256-colour and a 16-bit desktop, with and without a sound card and a joystick, and with other programs running; End Task and exit() must leave DVX, the sound and the timer as they were.
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h
index 2d0154ab..f14f9f9e 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlat.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h
@@ -28,13 +28,17 @@
//
// Two implementations exist:
// dvxPlatformDos.c -- DJGPP/DPMI: real VESA VBE, INT 33h mouse,
-// INT 16h keyboard, rep movsd/stosl asm spans
+// INT 16h keyboard, INT 9 keyboard hook, PIT / INT 8
+// timer service, rep movsd/stosl asm spans
// dvxPlatformHost.c -- native Linux test backend: offscreen
// framebuffer, queue-driven input, fake clock
+// with simulated timer interrupts
//
// Code shared by every backend lives in dvxPlatformUtil.c (logging,
-// path/file helpers, glob matching, Alt scancode table) and
-// dvxMemTrack.c (per-app allocation tracking).
+// path/file helpers, glob matching, Alt scancode table, exit routing),
+// dvxPlatformIrq.c (held-key table, timer service; interrupt-time steps
+// in the private dvxPlatIsr.h) and dvxMemTrack.c (per-app allocation
+// tracking).
//
// The abstraction covers video mode setup, framebuffer flushing,
// optimized memory spans, mouse and keyboard input, system information,
@@ -92,6 +96,31 @@ void dvxLog(const char *fmt, ...);
// directory until this is called (and for a NULL path).
void platformSetLogPath(const char *path);
+// Keep the log in memory instead of a file (DVX on read-only media with
+// no TEMP folder, or SETUP.EXE, which never writes to its disk): the
+// latest PLATFORM_LOG_MEM_MAX bytes. platformSetLogPath goes back to a
+// file.
+#define PLATFORM_LOG_MEM_MAX 16384
+void platformSetLogMemory(void);
+
+// The log kept in memory ("" when there is none).
+const char *platformLogMemory(void);
+
+// Whether DVX's own folder can be written to. The loader tests it at
+// startup (platformDirIsWritable); on read-only media (a CD-ROM, a
+// write-protected disk) settings are kept in memory until DVX exits, the
+// help files are used as they are, and the shell says so once.
+void platformSetReadOnly(bool readOnly);
+bool platformIsReadOnly(void);
+
+// True when a file can be created in dir (a scratch file is made and
+// removed again).
+bool platformDirIsWritable(const char *dir);
+
+// The folder for scratch files: the TEMP (or else TMP) environment
+// variable; NULL when neither is set.
+const char *platformTempDir(void);
+
// ============================================================
// Timing
// ============================================================
@@ -118,6 +147,105 @@ uint64_t platformEntropyStamp(void);
// Convert a millisecond interval to ticks at the platform rate.
#define PLATFORM_MS_TO_TICKS(ms) ((PlatformTicksT)(((uint64_t)(ms) * platformClockHz()) / 1000))
+// ============================================================
+// Timing -- high-resolution clock and the timer service
+// ============================================================
+//
+// One timer interrupt serves everything. On DOS the service owns PIT
+// channel 0 (the 8254 timer, rate generator mode) and the INT 8 vector:
+// it runs the interrupt as fast as the fastest timer needs (at most
+// PLATFORM_TIMER_MAX_HZ, at the BIOS 18.2 Hz while no timer is
+// registered) and passes on exactly one interrupt per 65536 PIT input
+// clocks to the BIOS handler, so the BIOS tick count, the DOS time of day
+// and clock() keep their normal rate. The host backend simulates the same
+// interrupts, driven by hostClockAdvance.
+
+#define PLATFORM_PIT_HZ 1193182 // PIT input clock
+#define PLATFORM_PIT_BIOS_DIVISOR 65536 // PIT clocks per BIOS tick (18.2 Hz)
+#define PLATFORM_TIMER_MAX_HZ 1000 // highest rate a timer may ask for
+#define PLATFORM_TIMER_FAST_MAX_HZ 30000 // highest rate platformTimerAddFast accepts
+#define PLATFORM_TIMER_SLOTS 16 // timers that can exist at once
+#define PLATFORM_US_PER_SEC 1000000
+
+// Microseconds since the timer service started (0 before), monotonic. On
+// DOS the PIT clocks counted by the interrupt plus the live channel 0
+// count (about 0.84 us steps), or, on a CPU with a time-stamp counter,
+// the TSC since the last interrupt scaled by the TSC rate measured over
+// the interrupt period before it, so it never drifts from the PIT. Not
+// for use inside a timer callback.
+uint64_t platformMicros(void);
+
+// A timer callback. It runs INSIDE THE TIMER INTERRUPT, with interrupts
+// disabled, on a small interrupt stack: keep it to a few dozen
+// microseconds, do not call the C library, DVX or DOS, do not use floating
+// point, and only touch code and data pinned with platformLockRegion
+// (on DOS a page fault here is fatal). Setting flags or counters, filling
+// a buffer or writing I/O ports is what it is for.
+typedef void (*PlatformTimerFnT)(void *userData);
+
+// Diagnostic counters of the timer service.
+typedef struct {
+ uint32_t rateHz; // current interrupt rate, rounded
+ uint32_t divisor; // PIT channel 0 reload value
+ uint32_t interrupts; // timer interrupts since the service started
+ uint32_t biosTicks; // of those, passed on to the BIOS
+} PlatformTimerStatsT;
+
+// Start / stop the timer service. Init programs the PIT for the BIOS
+// rate and takes the timer interrupt; Shutdown gives the PIT and the
+// vector back as it found them (BIOS square-wave mode, 18.2 Hz) and drops
+// every timer. Init also arranges for Shutdown to run at exit() and on
+// a crash that is not recovered. A second Init does nothing.
+void platformTimerInit(void);
+void platformTimerShutdown(void);
+
+// Call fn(userData) hz times a second (1..PLATFORM_TIMER_MAX_HZ, exact on
+// average: the calls fall on timer interrupts, so single intervals jitter
+// by up to one interrupt period; the interrupt rate changes at once, the
+// PIT restarting with the part of the period already run counted, and
+// the first call comes one interval on). ownerAppId is recorded for
+// platformTimerRemoveOwner (0 for a library that removes its own timers).
+// Returns a timer id, or -1 when hz is out of range, fn is NULL, all
+// PLATFORM_TIMER_SLOTS are in use or the service is not running.
+int32_t platformTimerAdd(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
+
+// platformTimerAdd for a device that must be fed at an audio rate (the PC
+// speaker's digital playback): hz may be up to PLATFORM_TIMER_FAST_MAX_HZ.
+// The whole timer interrupt then runs that often, so fn must be very
+// short (a few port writes); every other rule of platformTimerAdd holds.
+// For a call on every interrupt, ask for PLATFORM_PIT_HZ / divisor.
+int32_t platformTimerAddFast(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
+
+// Stop one timer; the interrupt rate drops when it was the fastest. When
+// this returns the callback is not running and will not run again.
+void platformTimerRemove(int32_t timerId);
+
+// Stop every timer registered for ownerAppId (the shell calls this when an
+// app ends).
+void platformTimerRemoveOwner(int32_t ownerAppId);
+
+// Current counters of the timer service.
+void platformTimerStats(PlatformTimerStatsT *out);
+
+// Disable interrupts and return the previous state, for
+// platformIrqRestore: brackets the few instructions that change data an
+// interrupt handler reads. Nothing to do on the host.
+void platformIrqRestore(uint32_t state);
+uint32_t platformIrqSave(void);
+
+// Shutdown hooks: fn runs when DVX ends without its normal shutdown
+// (exit() or a crash with no recovery point), before the timer service and
+// the keyboard hook are given back, so a module that drives hardware from
+// an interrupt (a sound card playing by DMA) can stop it. A module removes
+// its hook before it is unloaded. At most PLATFORM_SHUTDOWN_HOOKS at once;
+// Add returns false when full (adding one already present is a no-op).
+#define PLATFORM_SHUTDOWN_HOOKS 8
+
+typedef void (*PlatformShutdownFnT)(void);
+
+bool platformShutdownHookAdd(PlatformShutdownFnT fn);
+void platformShutdownHookRemove(PlatformShutdownFnT fn);
+
// ============================================================
// System lifecycle
// ============================================================
@@ -282,16 +410,42 @@ int32_t platformNormalizeFKeyScan(int32_t scancode);
// the scancode is kept and evt->modifiers = liveMods.
void platformNormalizeKeyEvent(PlatformKeyEventT *evt, int32_t liveMods);
-// Non-blocking read of the next key-up event. Returns true if a
-// key release was detected. On DOS this requires an INT 9 hook to
-// detect break codes (scan code with bit 7 set).
-bool platformKeyUpRead(PlatformKeyEventT *evt);
+// ============================================================
+// Input -- Raw keys and the held-key table
+// ============================================================
+//
+// The keyboard hook (on DOS a chain on the INT 9 hardware interrupt that
+// reads port 60h; the BIOS still stores typed keys for
+// platformKeyboardRead) decodes every set-1 byte the keyboard sends:
+// 0xE0 prefixes, the fake Shift codes the keyboard wraps around grey keys
+// (dropped), the Pause burst (one press and one release) and keyboard
+// replies such as ACK and overrun (dropped). Every key going down or up
+// updates the held-key table and is queued as a RawKeyEventT. Raw codes
+// are KEY_RAW_CODE(scancode, extended) (dvxTypes.h).
-// Install/remove the INT 9 hook for key-up detection. On DOS this
-// chains the hardware keyboard interrupt. Call Init before using
-// platformKeyUpRead, and Shutdown before exit.
-void platformKeyUpInit(void);
-void platformKeyUpShutdown(void);
+// Size of the raw key event queue; when it is full new events are dropped
+// (the held-key table is still updated).
+#define PLATFORM_KEY_EVENT_QUEUE 64
+
+// Install / remove the keyboard hook. Init before the other functions of
+// this section, Shutdown before exit. A second Init does nothing. On DOS
+// the hook's code and data are pinned with platformLockRegion; if that
+// fails there is no hook (no events, every key reads as up).
+void platformKeyHookInit(void);
+void platformKeyHookShutdown(void);
+
+// Next queued raw key event, oldest first; false when none is waiting.
+bool platformKeyEventRead(RawKeyEventT *evt);
+
+// True while the key with this raw code is held down.
+bool platformKeyIsDown(int32_t rawCode);
+
+// Copy the held-key table: out[rawCode] is 1 while that key is down.
+void platformKeyStateGet(uint8_t out[KEY_RAW_COUNT]);
+
+// Mark every key up (queued events are kept). A key that is still held
+// reads as down again at its next typematic repeat.
+void platformKeyStateClear(void);
// ============================================================
// Input -- Test injection
@@ -313,6 +467,12 @@ bool platformInjectKey(int32_t ascii, int32_t scancode, int32_t modifiers);
// Set the reported modifier state without queueing a key.
void platformInjectModifiers(int32_t modifiers);
+// Feed one set-1 byte (an 0xE0 prefix, a make code or a break code) to
+// the keyboard hook's decoder as if the keyboard had sent it: the
+// held-key table and the raw event queue change, the BIOS keyboard
+// buffer does not.
+void platformInjectScan(uint8_t scan);
+
// Override the mouse position and buttons platformMousePoll reports
// until the next call. The driver is still polled, so its state machine
// keeps running; only the reported values are replaced.
@@ -587,6 +747,30 @@ void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal
// also available for manual invocation if needed.
void platformLogCrashDetail(int sig, PlatformLogFnT logFn);
+// ============================================================
+// Program exit
+// ============================================================
+//
+// Loaded modules do not get the C library's exit() and abort(): on DOS the
+// DXE export table maps them to platformAppExit and platformAppAbort, and
+// the host build links every app with the same substitution (see
+// HOST_APP_LDFLAGS in common.mk). They call the hook the shell installs,
+// which ends just the program that called them, the way End Task does,
+// when a program is running; otherwise (or with no hook) they end the
+// process as the C library functions would.
+
+// Exit status reported for abort()
+#define PLATFORM_ABORT_STATUS 3
+
+// Called with the exit status; returns only when no program is running.
+typedef void (*PlatformAppExitFnT)(int status);
+
+void platformSetAppExitHook(PlatformAppExitFnT fn);
+
+// exit() / abort() as seen by loaded modules.
+void platformAppAbort(void) __attribute__((noreturn));
+void platformAppExit(int status) __attribute__((noreturn));
+
// Out-of-memory hook for stb_ds (STBDS_OUT_OF_MEMORY, set in
// stb_ds_wrap.h). arrput/hmput/shput have no error channel, so a failed
// growth logs "out of memory (N bytes) in dynamic array" via dvxLog and
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatHost.h b/src/libs/kpunch/libdvx/platform/dvxPlatHost.h
index fe0d753f..5790a75e 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatHost.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatHost.h
@@ -60,7 +60,9 @@ void hostWheelPush(int32_t delta);
// platformKeyboardGetModifiers will report from this point on.
void hostKeyPush(int32_t ascii, int32_t scancode, int32_t modifiers);
-// Queue a key release for platformKeyUpRead.
+// Queue a bare key release event (this scancode, no 0xE0, the held-key
+// table untouched): what the focused window's onKeyUp then receives. For
+// real key presses and releases through the decoder use platformInjectScan.
void hostKeyUpPush(int32_t scancode);
// Set the modifier byte without queueing a key (e.g. to hold Shift down
@@ -72,9 +74,14 @@ void hostSetModifiers(int32_t modifiers);
// ============================================================
// Advance the fake clock by ms milliseconds. Fake-clock only: ignored
-// while hostClockSetRealTime(true) is in effect.
+// while hostClockSetRealTime(true) is in effect. platformMicros moves on
+// by the same time and the timer interrupts that time holds are run
+// (timer callbacks included) before this returns.
void hostClockAdvance(int32_t ms);
+// Advance the fake clock by us microseconds (as hostClockAdvance).
+void hostClockAdvanceUs(uint64_t us);
+
// Set the fake clock to an absolute millisecond value. Fake-clock only:
// ignored while hostClockSetRealTime(true) is in effect.
void hostClockSet(uint32_t ms);
@@ -84,6 +91,20 @@ void hostClockSet(uint32_t ms);
// current fake value; leaving it freezes at the current wall value.
void hostClockSetRealTime(bool realTime);
+// Make every platformYield advance the fake clock by us microseconds (0,
+// the default, leaves the clock alone), so code that yields until a time
+// arrives finishes under test.
+void hostClockSetYieldStep(uint32_t us);
+
+// ============================================================
+// Memory locking
+// ============================================================
+
+// Bytes platformLockRegion pinned minus the bytes platformUnlockRegion
+// released since the process started (a leak check for modules that pin
+// interrupt code and are unloaded again).
+int64_t hostLockedBytes(void);
+
// ============================================================
// Video
// ============================================================
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatIsr.h b/src/libs/kpunch/libdvx/platform/dvxPlatIsr.h
new file mode 100644
index 00000000..63750e39
--- /dev/null
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatIsr.h
@@ -0,0 +1,315 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// dvxPlatIsr.h -- interrupt-time services shared by the platform backends
+//
+// Private to the platform layer (not in the SDK). Holds the state and the
+// interrupt-time steps of the two services every backend runs the same
+// way: the keyboard decoder behind the held-key table (dvxPlat.h, "Raw
+// keys") and the timer service ("Timing"). The steps are forced inline so
+// on DOS they become part of the interrupt handlers, whose code is pinned
+// as one range; the host backend runs the very same steps from its
+// injection and simulated-clock calls. The pure arithmetic of the
+// high-resolution clock is here too, so host tests cover what the DOS
+// clock computes.
+//
+// dvxPlatformIrq.c owns the state and the non-interrupt API; each backend
+// supplies the three primitives declared at the end.
+#ifndef DVX_PLAT_ISR_H
+#define DVX_PLAT_ISR_H
+
+#include "dvxPlat.h"
+
+// Interrupt-time code: always inlined into its caller (see above).
+#define ISR_INLINE static inline __attribute__((always_inline))
+
+// Set-1 bytes with a meaning of their own. 0xE0 precedes the make or
+// break code of an extended key; 0xE1 starts the Pause burst
+// (E1 1D 45 E1 9D C5), which has no break code. The keyboard wraps grey
+// keys in fake Left/Right Shift codes behind an 0xE0 (E0 2A, E0 AA, E0 36,
+// E0 B6), which are not key presses. The rest are keyboard replies
+// (overrun, echo, ACK, resend), not keys.
+#define PS2_PREFIX_E0 0xE0
+#define PS2_PREFIX_E1 0xE1
+#define PS2_BREAK_BIT 0x80
+#define PS2_PAUSE_TAIL_BYTES 5 // bytes of the Pause burst after its first 0xE1
+#define PS2_REPLY_OVERRUN_LOW 0x00
+#define PS2_REPLY_ECHO 0xEE
+#define PS2_REPLY_ACK 0xFA
+#define PS2_REPLY_RESEND 0xFE
+#define PS2_REPLY_OVERRUN_HIGH 0xFF
+
+// Key state flags in KeyScanStateT.down
+#define KEY_STATE_UP 0
+#define KEY_STATE_DOWN 1
+
+// Decoder state, the held-key table and the raw event queue. One struct
+// so a single lock pins everything the keyboard interrupt touches.
+typedef struct {
+ RawKeyEventT ring[PLATFORM_KEY_EVENT_QUEUE];
+ volatile uint32_t head; // next slot the decoder fills
+ volatile uint32_t tail; // next slot the reader takes
+ volatile uint8_t down[KEY_RAW_COUNT]; // KEY_STATE_* per raw code
+ volatile uint8_t prefixE0; // an 0xE0 came last
+ volatile uint8_t pauseLeft; // Pause burst bytes still to swallow
+} KeyScanStateT;
+
+// One registered timer (fn == NULL: free slot).
+typedef struct {
+ PlatformTimerFnT fn;
+ void *userData;
+ uint32_t hz;
+ uint32_t acc; // PIT clocks * hz toward the next call
+ int32_t owner; // ownerAppId given to platformTimerAdd
+} TimerSlotT;
+
+// Timer service state, everything the timer interrupt touches.
+typedef struct {
+ TimerSlotT slots[PLATFORM_TIMER_SLOTS];
+ volatile uint64_t pitCount; // PIT clocks up to the last interrupt
+ volatile uint64_t lastRead; // largest clock value handed out (monotonic)
+ volatile uint64_t tscAtIrq; // TSC at the last interrupt (DOS, TSC CPUs)
+ volatile uint64_t tscPeriod; // TSC cycles of the period before it (0 = unknown)
+ volatile uint32_t tscPeriodDivisor; // PIT clocks of that period
+ volatile uint32_t divisor; // PIT clocks per period (the reload value)
+ volatile uint32_t biosAcc; // PIT clocks since the last BIOS tick
+ volatile uint32_t biosTicks; // interrupts passed on to the BIOS
+ volatile uint32_t interrupts; // timer interrupts so far
+ volatile bool periodAccounted; // the pending interrupt's period is already counted
+ bool tscPresent; // the CPU has a time-stamp counter
+ bool running; // platformTimerInit has run
+} TimerServiceT;
+
+// The two services (dvxPlatformIrq.c).
+extern KeyScanStateT platformKeyScan;
+extern TimerServiceT platformTimer;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+ISR_INLINE void keyScanFeed(KeyScanStateT *s, uint8_t byte);
+ISR_INLINE int32_t keyScanModifiers(const KeyScanStateT *s);
+ISR_INLINE void keyScanQueue(KeyScanStateT *s, uint8_t scan, bool extended, bool down, bool repeat);
+static inline uint32_t timerPitElapsed(uint32_t divisor, uint32_t count, bool irqPending);
+static inline uint64_t timerPitToMicros(uint64_t pitClocks);
+static inline void timerRestartPeriod(TimerServiceT *t, uint32_t elapsed, uint32_t divisor);
+ISR_INLINE bool timerServiceTick(TimerServiceT *t);
+static inline uint32_t timerTscElapsed(uint64_t tscNow, uint64_t tscAtIrq, uint64_t tscPeriod, uint32_t periodDivisor, uint32_t divisor);
+
+// Backend primitives used by dvxPlatformIrq.c (platformIrqSave and
+// platformIrqRestore are public, in dvxPlat.h). platformTimerProgram cuts
+// the running period short and restarts the PIT with divisor at once
+// (timerRestartPeriod does the accounting; called with interrupts
+// disabled), so a new rate starts without waiting out a long BIOS-rate
+// period.
+void platformTimerProgram(uint32_t divisor);
+
+// Run (and forget) every shutdown hook (dvxPlatformIrq.c); the DOS backend
+// calls it when DVX ends without its normal shutdown.
+void platformShutdownHooksRun(void);
+
+
+// Decode one byte from the keyboard.
+ISR_INLINE void keyScanFeed(KeyScanStateT *s, uint8_t byte) {
+ if (s->pauseLeft > 0) {
+ s->pauseLeft--;
+ return;
+ }
+
+ if (byte == PS2_PREFIX_E1) {
+ s->pauseLeft = PS2_PAUSE_TAIL_BYTES;
+ keyScanQueue(s, KEY_RAW_PAUSE & KEY_RAW_SCAN_MASK, true, true, false);
+ keyScanQueue(s, KEY_RAW_PAUSE & KEY_RAW_SCAN_MASK, true, false, false);
+ return;
+ }
+
+ if (byte == PS2_PREFIX_E0) {
+ s->prefixE0 = 1;
+ return;
+ }
+
+ bool extended = s->prefixE0 != 0;
+
+ s->prefixE0 = 0;
+
+ if (byte == PS2_REPLY_OVERRUN_LOW || byte == PS2_REPLY_ECHO || byte == PS2_REPLY_ACK || byte == PS2_REPLY_RESEND || byte == PS2_REPLY_OVERRUN_HIGH) {
+ return;
+ }
+
+ uint8_t scan = byte & KEY_RAW_SCAN_MASK;
+ bool down = (byte & PS2_BREAK_BIT) == 0;
+
+ if (extended && (scan == KEY_RAW_LSHIFT || scan == KEY_RAW_RSHIFT)) {
+ return;
+ }
+
+ int32_t code = KEY_RAW_CODE(scan, extended);
+ bool repeat = down && s->down[code] == KEY_STATE_DOWN;
+
+ s->down[code] = down ? KEY_STATE_DOWN : KEY_STATE_UP;
+ keyScanQueue(s, scan, extended, down, repeat);
+}
+
+
+// KEY_MOD_* of the keys held in the table.
+ISR_INLINE int32_t keyScanModifiers(const KeyScanStateT *s) {
+ int32_t mods = 0;
+
+ if (s->down[KEY_RAW_LSHIFT] == KEY_STATE_DOWN) {
+ mods |= KEY_MOD_LSHIFT;
+ }
+
+ if (s->down[KEY_RAW_RSHIFT] == KEY_STATE_DOWN) {
+ mods |= KEY_MOD_RSHIFT;
+ }
+
+ if (s->down[KEY_RAW_LCTRL] == KEY_STATE_DOWN || s->down[KEY_RAW_RCTRL] == KEY_STATE_DOWN) {
+ mods |= KEY_MOD_CTRL;
+ }
+
+ if (s->down[KEY_RAW_LALT] == KEY_STATE_DOWN || s->down[KEY_RAW_RALT] == KEY_STATE_DOWN) {
+ mods |= KEY_MOD_ALT;
+ }
+
+ return mods;
+}
+
+
+// Append one event; dropped when the queue is full.
+ISR_INLINE void keyScanQueue(KeyScanStateT *s, uint8_t scan, bool extended, bool down, bool repeat) {
+ uint32_t head = s->head;
+
+ if (head - s->tail >= PLATFORM_KEY_EVENT_QUEUE) {
+ return;
+ }
+
+ RawKeyEventT *evt = &s->ring[head % PLATFORM_KEY_EVENT_QUEUE];
+
+ evt->scancode = scan;
+ evt->extended = extended;
+ evt->down = down;
+ evt->repeat = repeat;
+ evt->modifiers = keyScanModifiers(s);
+
+ // The reader must never see head move before the event is complete.
+ __atomic_signal_fence(__ATOMIC_RELEASE);
+ s->head = head + 1;
+}
+
+
+// PIT clocks into the running period, from channel 0's latched count
+// (rate generator: counts divisor..1, 0 standing for 65536) and whether
+// the timer interrupt is pending unserviced. A pending interrupt with a
+// count near the top means the counter already reloaded for the next
+// period, which the interrupt has not added yet.
+static inline uint32_t timerPitElapsed(uint32_t divisor, uint32_t count, bool irqPending) {
+ if (count == 0 || count > divisor) {
+ count = divisor;
+ }
+
+ uint32_t elapsed = divisor - count;
+
+ if (irqPending && elapsed < divisor / 2) {
+ elapsed += divisor;
+ }
+
+ return elapsed;
+}
+
+
+static inline uint64_t timerPitToMicros(uint64_t pitClocks) {
+ return pitClocks * PLATFORM_US_PER_SEC / PLATFORM_PIT_HZ;
+}
+
+
+// The PIT is restarted with divisor now, elapsed PIT clocks into the
+// running period: those clocks are counted here (for the clock, the BIOS
+// tick and every timer) since no interrupt will end that period. elapsed
+// may include one whole period whose interrupt is pending unserviced
+// (timerPitElapsed); that interrupt then adds nothing.
+static inline void timerRestartPeriod(TimerServiceT *t, uint32_t elapsed, uint32_t divisor) {
+ t->periodAccounted = elapsed >= t->divisor;
+ t->pitCount += elapsed;
+ t->biosAcc += elapsed;
+
+ for (int32_t i = 0; i < PLATFORM_TIMER_SLOTS; i++) {
+ t->slots[i].acc += elapsed * t->slots[i].hz;
+ }
+
+ t->divisor = divisor;
+}
+
+
+// One timer interrupt: account for the period that just ended, run the
+// timers that are due and say whether the BIOS gets this interrupt (once
+// per PLATFORM_PIT_BIOS_DIVISOR PIT clocks, so its tick keeps 18.2 Hz).
+ISR_INLINE bool timerServiceTick(TimerServiceT *t) {
+ uint32_t period = t->periodAccounted ? 0 : t->divisor;
+
+ t->periodAccounted = false;
+ t->pitCount += period;
+ t->interrupts++;
+
+ for (int32_t i = 0; i < PLATFORM_TIMER_SLOTS; i++) {
+ TimerSlotT *slot = &t->slots[i];
+
+ if (slot->fn) {
+ slot->acc += period * slot->hz;
+
+ if (slot->acc >= PLATFORM_PIT_HZ) {
+ slot->acc -= PLATFORM_PIT_HZ;
+
+ // More than one call owed: this period was longer than the
+ // timer's (the interrupt was still at a slower rate). One
+ // call, no backlog to make up later.
+ if (slot->acc >= PLATFORM_PIT_HZ) {
+ slot->acc = 0;
+ }
+
+ slot->fn(slot->userData);
+ }
+ }
+ }
+
+ t->biosAcc += period;
+
+ if (t->biosAcc < PLATFORM_PIT_BIOS_DIVISOR) {
+ return false;
+ }
+
+ t->biosAcc -= PLATFORM_PIT_BIOS_DIVISOR;
+ t->biosTicks++;
+ return true;
+}
+
+
+// PIT clocks into the running period from the TSC: cycles since the last
+// interrupt at the rate measured over the period before it, capped just
+// short of the period (the interrupt adds the whole period).
+static inline uint32_t timerTscElapsed(uint64_t tscNow, uint64_t tscAtIrq, uint64_t tscPeriod, uint32_t periodDivisor, uint32_t divisor) {
+ uint64_t elapsed = (tscNow - tscAtIrq) * periodDivisor / tscPeriod;
+
+ return elapsed >= divisor ? divisor - 1 : (uint32_t)elapsed;
+}
+
+#endif // DVX_PLAT_ISR_H
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
index 79614b38..37fe584d 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
@@ -29,12 +29,15 @@
// sys/nearptr.h, etc.) appear. Every other DVX module calls through
// the dvxPlatform.h interface, so porting to a new OS (Linux/SDL,
// Win32, or bare-metal ARM) requires replacing only this file and
-// nothing else. The abstraction covers five domains:
+// nothing else. The abstraction covers six domains:
// 1. VESA VBE video init / mode set / LFB mapping
// 2. Backbuffer-to-LFB flush using rep movsl
// 3. Span fill/copy primitives using inline asm (rep stosl / rep movsl)
// 4. Mouse input via INT 33h driver
-// 5. Keyboard input via BIOS INT 16h
+// 5. Keyboard input via BIOS INT 16h, plus a chain on INT 9 that feeds
+// the held-key table and the raw key events
+// 6. The timer service: PIT channel 0 and INT 8, with the BIOS tick
+// passed on at its own rate, and the high-resolution clock
//
// Why BIOS INT 16h for keyboard instead of direct port I/O (scancode
// reading from port 0x60): BIOS handles typematic repeat, keyboard
@@ -49,6 +52,7 @@
// of installing a real-mode callback for mouse events.
#include "dvxPlat.h"
+#include "dvxPlatIsr.h"
#include "dvxPal.h"
#include "dvxPrefs.h"
#include "../../../../tools/hlpcCompile.h"
@@ -68,6 +72,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -89,7 +94,7 @@
#define MODE_SCORE_15BPP 90 // 15bpp: slightly below 16 (some BIOSes conflate them)
#define MODE_SCORE_32BPP 85 // 32bpp: true color but slower bus traffic
#define MODE_SCORE_8BPP 70 // 8bpp: palette management adds complexity
-#define MODE_SCORE_PREF_BPP 20 // bonus if bpp matches user preference
+#define MODE_SCORE_PREF_BPP 40 // bonus if bpp matches user preference: more than the widest base gap (16 vs 8), so the chosen depth always wins
#define MODE_SCORE_EXACT_RES 10 // bonus/penalty for exact/oversize resolution
// RDTSC calibration: measure over this many BIOS timer ticks
@@ -102,13 +107,31 @@
#define BIOS_TICK_US 54925ULL
#define BIOS_TICKS_PER_DAY 0x1800B0UL
-// PIT channel 0 (18.2 Hz system timer): latch command and data port for
-// reading the live 16-bit down-counter (entropy stamps on TSC-less CPUs).
+// PIT channel 0 (the system timer): command port, data port, and the
+// command bytes the timer service uses: latch the count, rate generator
+// (mode 2, low byte then high byte) while the service runs, and the BIOS's
+// own square wave (mode 3) to hand the PIT back. A count of 0 means 65536.
#define PIT_CMD_PORT 0x43
#define PIT_CH0_PORT 0x40
#define PIT_LATCH_CH0 0x00
+#define PIT_CH0_RATE_GEN 0x34
+#define PIT_CH0_SQUARE_WAVE 0x36
#define PIT_COUNT_BITS 16
#define BYTE_BITS 8
+#define BYTE_MASK 0xFF
+
+// Master 8259 PIC: end-of-interrupt, and reading the request register to
+// see whether the timer interrupt (IRQ 0) is pending.
+#define PIC1_CMD_PORT 0x20
+#define PIC_EOI 0x20
+#define PIC_READ_IRR 0x0A
+#define PIC_IRQ0_BIT 0x01
+
+// Protected-mode vector of the timer interrupt (IRQ 0)
+#define TIMER_IRQ_VECTOR 8
+
+// CPU interrupt flag in EFLAGS (platformIrqSave / platformIrqRestore)
+#define EFLAGS_IF_BIT 0x200U
#define TSC_HI_SHIFT 32 // rdtsc: EDX holds the high dword
// Real-mode interrupt vectors
@@ -240,10 +263,6 @@
// PS/2 keyboard data port, hardware IRQ vector, and break-code bit
#define PS2_DATA_PORT 0x60
#define PS2_KBD_IRQ_VECTOR 9
-#define PS2_BREAK_BIT 0x80
-#define PS2_SCAN_MASK 0x7F
-#define PS2_SCAN_F11 0x57 // set-1 make codes; INT 16h reports F11/F12
-#define PS2_SCAN_F12 0x58 // as KEY_SCAN_F11/KEY_SCAN_F12 instead
// INT 21h DOS functions
#define DOS_FN_GET_VERSION 0x3000
@@ -276,28 +295,10 @@
#define BYTES_PER_KB 1024
#define KB_PER_MB 1024
-// Key-up detection ring buffer size
-#define KEYUP_BUF_SIZE 16
-
-// Worst-case byte span of int9Handler that must be locked into physical
-// memory against DPMI paging. Matches the rs232 driver's ISR_SIZE; the
-// handler is small but a generous span guarantees full coverage.
-#define KEYUP_ISR_SIZE 2048
-
-// PS/2 scancode prefix and fake-shift bytes. 0xE0 precedes a single
-// extended-key scancode; 0xE1 precedes the two 2-byte halves of the
-// Pause/Break burst (E1 1D 45 E1 9D C5), which carries no real break code.
-// The grey navigation keys are bracketed by "fake shift" releases (0xAA /
-// 0xB6 emitted right after an 0xE0) that must not be reported as genuine
-// Shift key-ups, while a standalone 0xAA / 0xB6 (real Left / Right Shift
-// release) must be.
-#define PS2_SCAN_PREFIX_E0 0xE0
-#define PS2_SCAN_PREFIX_E1 0xE1
-#define PS2_SCAN_FAKE_SHIFT_LE 0xAA
-#define PS2_SCAN_FAKE_SHIFT_RE 0xB6
-
-// Number of scancodes to swallow after each 0xE1 prefix in the Pause burst.
-#define PS2_E1_TAIL_BYTES 2
+// Bytes of each interrupt handler (with the service steps inlined into
+// it) pinned against DPMI paging. Matches the rs232 driver's ISR_SIZE:
+// the handlers are small, a generous span guarantees full coverage.
+#define ISR_LOCK_SIZE 2048
// Sentinel for sLfbLinearAddr/sLfbMappedSize meaning "no LFB currently mapped".
#define LFB_NOT_MAPPED 0
@@ -348,15 +349,21 @@ typedef struct {
// ============================================================
static uint8_t biosVideoMode(void);
+static bool cpuHasTsc(void);
static uint32_t estimateClockMhz(void);
static uint32_t farPtrToLinear(uint32_t addr);
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d);
static bool hasCpuid(void);
static void installCritErrorHandler(void);
+static void int8Handler(void);
static void int9Handler(void);
+static void irqServicesArmAtExit(void);
+static void irqServicesAtExit(void);
static int32_t mapLfb(DisplayT *d, uint32_t physAddr);
static uint32_t pagesToKb(unsigned long pages);
+static uint32_t pitLatchElapsed(void);
static void platformCrashHandler(int sig);
+ISR_INLINE uint64_t readTsc(void);
static bool regionFromNear(const void *addr, uint32_t size, bool isCode, __dpmi_meminfo *out);
static int32_t scoreMode(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
static dxe_symbol_table sDxeExportTable[];
@@ -364,6 +371,8 @@ static void setBiosMode(uint16_t mode);
static int32_t setVesaMode(uint16_t mode);
static void sysInfoAppend(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void sysInfoDrives(void);
+static bool timerLock(void);
+static void timerUnlock(void);
static void unmapLfb(DisplayT *d);
static uint16_t vbeGetControllerInfo(void);
static bool vbeQueryMode(uint16_t mode, uint16_t vbeVersion, VbeModeT *out);
@@ -464,27 +473,26 @@ static int32_t sSysInfoPos = 0;
// dvx.exe with plain realloc/free, so array growth done through the
// exported stbds_arrgrowf is not charged to the calling app.
-// Key-up detection via INT 9 hook. The BIOS keyboard interrupt (INT 16h)
-// only reports key presses. To detect key releases we chain INT 9 (the
-// hardware keyboard IRQ) and read the scan code directly from port 0x60.
-// Break codes have bit 7 set. We queue them in a small ring buffer that
-// platformKeyUpRead drains.
-//
-// All state the ISR touches lives in one struct so a single DPMI lock pins
-// every byte contiguously; independent statics give no contiguity guarantee
-// (a page fault inside the handler under a paging host would be fatal).
-typedef struct {
- PlatformKeyEventT buf[KEYUP_BUF_SIZE];
- volatile int32_t head;
- volatile int32_t tail;
- volatile uint8_t scanPrefix; // pending 0xE0 prefix, or 0
- volatile int32_t e1Pending; // scancodes left to swallow after 0xE1
-} KeyUpStateT;
-
-static KeyUpStateT sKeyUp = { 0 };
+// Keyboard hook: a chain on INT 9 (the keyboard IRQ) that hands each byte
+// from port 60h to the shared decoder (dvxPlatIsr.h) before the BIOS
+// handler runs. The decoder state lives in platformKeyScan.
static _go32_dpmi_seginfo sOldInt9;
static _go32_dpmi_seginfo sNewInt9;
-static bool sKeyUpInstalled = false;
+static bool sKeyHookInstalled = false;
+
+// Timer service: INT 8 goes to int8Handler (through an iret wrapper),
+// which passes the interrupts owed to the BIOS on to the previous handler
+// with a far call. The far pointer is the only state of its own the
+// handler touches, pinned together with the handler's code.
+typedef struct __attribute__((packed)) {
+ uint32_t offset;
+ uint16_t selector;
+} FarPtrT;
+
+static FarPtrT sOldInt8Far;
+static _go32_dpmi_seginfo sOldInt8;
+static _go32_dpmi_seginfo sNewInt8;
+static bool sAtExitRegistered = false;
// Crash recovery state -- set up by platformInstallCrashHandler and used
// by platformCrashHandler to longjmp back to the caller's recovery point.
@@ -517,6 +525,30 @@ static uint8_t biosVideoMode(void) {
}
+// Whether the CPU has a time-stamp counter (CPUID feature bit), probed
+// once.
+static bool cpuHasTsc(void) {
+ if (sTscState == TscUnknownE) {
+ sTscState = TscAbsentE;
+
+ if (hasCpuid()) {
+ uint32_t eax;
+ uint32_t ebx;
+ uint32_t ecx;
+ uint32_t edx;
+
+ __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES));
+
+ if (edx & CPUID_FEAT_TSC) {
+ sTscState = TscPresentE;
+ }
+ }
+ }
+
+ return sTscState == TscPresentE;
+}
+
+
// RDTSC calibration via BIOS timer.
// Measures TSC ticks over CLOCK_MEAS_TICKS BIOS timer ticks (~165 ms).
// Using several ticks instead of 1 reduces jitter from interrupt latency
@@ -533,10 +565,7 @@ static uint32_t estimateClockMhz(void) {
// spin
}
- uint32_t lo1;
- uint32_t hi1;
-
- __asm__ __volatile__ ("rdtsc" : "=a"(lo1), "=d"(hi1));
+ uint64_t tsc1 = readTsc();
uint32_t tickStart = _farpeekl(_dos_ds, BIOS_TICK_ADDR);
uint32_t now = tickStart;
@@ -550,14 +579,7 @@ static uint32_t estimateClockMhz(void) {
continue;
}
- uint32_t lo2;
- uint32_t hi2;
-
- __asm__ __volatile__ ("rdtsc" : "=a"(lo2), "=d"(hi2));
-
- uint64_t tsc1 = ((uint64_t)hi1 << TSC_HI_SHIFT) | lo1;
- uint64_t tsc2 = ((uint64_t)hi2 << TSC_HI_SHIFT) | lo2;
- uint64_t delta = tsc2 - tsc1;
+ uint64_t delta = readTsc() - tsc1;
return (uint32_t)(delta / (CLOCK_MEAS_TICKS * BIOS_TICK_US));
}
@@ -721,54 +743,56 @@ static void installCritErrorHandler(void) {
}
-// INT 9 handler: reads the PS/2 data port and queues break codes.
-// DJGPP chains to the original handler automatically when using
-// _go32_dpmi_chain_protected_mode_interrupt_vector.
+// Timer interrupt. On a TSC CPU the cycles of the period that just ended
+// are measured first (platformMicros interpolates with them); then the
+// service step runs the due timers. An interrupt owed to the BIOS goes on
+// to the previous handler, as if it had come straight from the PIC (that
+// handler sends the EOI); any other is acknowledged here.
+static void int8Handler(void) {
+ TimerServiceT *t = &platformTimer;
-static void int9Handler(void) {
- uint8_t scan = inportb(PS2_DATA_PORT);
+ if (t->tscPresent) {
+ uint64_t now = readTsc();
- // Pause/Break emits a fixed E1 1D 45 E1 9D C5 burst with no real break
- // code. Swallow the two scancodes following each 0xE1 prefix so 0x9D /
- // 0xC5 are never queued as phantom key-ups.
- if (sKeyUp.e1Pending > 0) {
- sKeyUp.e1Pending--;
- return;
- }
-
- if (scan == PS2_SCAN_PREFIX_E1) {
- sKeyUp.e1Pending = PS2_E1_TAIL_BYTES;
- return;
- }
-
- // 0xE0 marks the following byte as an extended-key scancode.
- if (scan == PS2_SCAN_PREFIX_E0) {
- sKeyUp.scanPrefix = PS2_SCAN_PREFIX_E0;
- return;
- }
-
- bool extended = (sKeyUp.scanPrefix == PS2_SCAN_PREFIX_E0);
- sKeyUp.scanPrefix = 0;
-
- // Drop the keyboard's fake-shift releases that bracket the grey keys; a
- // standalone 0xAA / 0xB6 (a genuine Shift release) still falls through.
- if (extended && (scan == PS2_SCAN_FAKE_SHIFT_LE || scan == PS2_SCAN_FAKE_SHIFT_RE)) {
- return;
- }
-
- // Break code: bit 7 set.
- if (scan & PS2_BREAK_BIT) {
- int32_t next = (sKeyUp.head + 1) % KEYUP_BUF_SIZE;
-
- if (next != sKeyUp.tail) {
- sKeyUp.buf[sKeyUp.head].scancode = scan & PS2_SCAN_MASK;
- sKeyUp.buf[sKeyUp.head].ascii = 0;
-
- sKeyUp.head = next;
+ if (t->tscAtIrq != 0) {
+ t->tscPeriod = now - t->tscAtIrq;
+ t->tscPeriodDivisor = t->divisor;
}
+
+ t->tscAtIrq = now;
}
- // Original handler is called automatically by DJGPP's chain wrapper
+ if (timerServiceTick(t)) {
+ __asm__ __volatile__ ("pushfl\n\tlcall *%0" : : "m"(sOldInt8Far) : "memory", "cc");
+ } else {
+ outportb(PIC1_CMD_PORT, PIC_EOI);
+ }
+}
+
+
+// Keyboard interrupt: every byte goes to the decoder. DJGPP's chain
+// wrapper then runs the BIOS handler, which reads the same byte for the
+// keyboard buffer.
+static void int9Handler(void) {
+ keyScanFeed(&platformKeyScan, inportb(PS2_DATA_PORT));
+}
+
+
+// Register irqServicesAtExit once, from whichever service starts first.
+static void irqServicesArmAtExit(void) {
+ if (!sAtExitRegistered) {
+ sAtExitRegistered = atexit(irqServicesAtExit) == 0;
+ }
+}
+
+
+// exit() path (atexit): give the timer and keyboard interrupts back if
+// DVX ends without its normal shutdown, after the modules' shutdown hooks
+// stopped whatever they drive from interrupts.
+static void irqServicesAtExit(void) {
+ platformShutdownHooksRun();
+ platformTimerShutdown();
+ platformKeyHookShutdown();
}
@@ -849,6 +873,23 @@ static uint32_t pagesToKb(unsigned long pages) {
}
+// PIT clocks into the running timer period, from the latched channel 0
+// count and the PIC's request bit for IRQ 0 (timerPitElapsed). Called
+// with interrupts disabled.
+static uint32_t pitLatchElapsed(void) {
+ outportb(PIT_CMD_PORT, PIT_LATCH_CH0);
+
+ uint32_t countLo = inportb(PIT_CH0_PORT);
+ uint32_t countHi = inportb(PIT_CH0_PORT);
+
+ outportb(PIC1_CMD_PORT, PIC_READ_IRR);
+
+ bool pending = (inportb(PIC1_CMD_PORT) & PIC_IRQ0_BIT) != 0;
+
+ return timerPitElapsed(platformTimer.divisor, (countHi << BYTE_BITS) | countLo, pending);
+}
+
+
// INT 21h AX=3305h (DOS 4.0+). An older DOS leaves DL unchanged (0),
// which reads as "no boot drive".
char platformBootDrive(void) {
@@ -877,25 +918,30 @@ PlatformTicksT platformClockHz(void) {
// Signal handler for fatal exceptions.
// DJGPP uses System V signal semantics where the handler is reset to
-// SIG_DFL after each delivery, so we must re-install before doing
-// anything else. After logging platform-specific diagnostics and
-// storing the signal number, we longjmp back to the caller's recovery
-// point (typically the shell's main loop).
+// SIG_DFL after each delivery, so it is re-installed before the longjmp
+// back to the caller's recovery point (typically the shell's main loop),
+// after the platform-specific diagnostics are logged and the signal
+// number stored. Without a recovery point the default action runs.
static void platformCrashHandler(int sig) {
if (sPlatformLogFn) {
platformLogCrashDetail(sig, sPlatformLogFn);
}
- signal(sig, platformCrashHandler);
-
if (sPlatformCrashSignal) {
*sPlatformCrashSignal = sig;
}
if (sPlatformCrashJmp) {
+ signal(sig, platformCrashHandler);
longjmp(*sPlatformCrashJmp, 1);
}
+
+ // No recovery point: DVX ends here, so the timer and keyboard
+ // interrupts must not be left pointing into it.
+ irqServicesAtExit();
+ signal(sig, SIG_DFL);
+ raise(sig);
}
@@ -976,29 +1022,8 @@ int32_t platformDriveList(PlatformDriveT *out, int32_t max) {
// has one, else the BIOS tick count with the live PIT channel 0 counter
// (~0.84 us resolution) in the low 16 bits.
uint64_t platformEntropyStamp(void) {
- if (sTscState == TscUnknownE) {
- sTscState = TscAbsentE;
-
- if (hasCpuid()) {
- uint32_t eax;
- uint32_t ebx;
- uint32_t ecx;
- uint32_t edx;
-
- __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES));
-
- if (edx & CPUID_FEAT_TSC) {
- sTscState = TscPresentE;
- }
- }
- }
-
- if (sTscState == TscPresentE) {
- uint32_t lo;
- uint32_t hi;
-
- __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
- return ((uint64_t)hi << TSC_HI_SHIFT) | lo;
+ if (cpuHasTsc()) {
+ return readTsc();
}
outportb(PIT_CMD_PORT, PIT_LATCH_CH0);
@@ -1286,13 +1311,9 @@ const char *platformGetSystemInfo(const DisplayT *display) {
sysInfoAppend("DPMI Flags: %s%s%s", bits, v86, virt);
}
- const char *tmpDir = getenv("TEMP");
+ const char *tmpDir = platformTempDir();
- if (!tmpDir) {
- tmpDir = getenv("TMP");
- }
-
- sysInfoAppend("Temp Path: %s", (tmpDir && tmpDir[0]) ? tmpDir : "(current directory)");
+ sysInfoAppend("Temp Path: %s", tmpDir ? tmpDir : "(current directory)");
// ---- Video ----
sysInfoAppend("%s", "");
@@ -1436,6 +1457,23 @@ void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal
}
+void platformIrqRestore(uint32_t state) {
+ if (state & EFLAGS_IF_BIT) {
+ __asm__ __volatile__ ("sti" : : : "memory");
+ }
+}
+
+
+// Previous interrupt flag (EFLAGS) and interrupts off: nesting-safe, so
+// it may also be used where interrupts are already disabled.
+uint32_t platformIrqSave(void) {
+ uint32_t flags;
+
+ __asm__ __volatile__ ("pushfl\n\tpopl %0\n\tcli" : "=r"(flags) : : "memory");
+ return flags;
+}
+
+
// Returns the current modifier key state via INT 16h function 12h
// (enhanced get extended shift flags). The low byte contains:
// bit 0 = right shift, bit 1 = left shift
@@ -1495,27 +1533,27 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) {
}
-void platformKeyUpInit(void) {
- if (sKeyUpInstalled) {
+// The INT 9 handler runs at hardware-interrupt time, so its code and the
+// decoder state it touches are locked into physical memory first: a page
+// fault inside it under a paging DPMI host (e.g. CWSDPMI) would be fatal.
+// If locking fails there is simply no hook.
+void platformKeyHookInit(void) {
+ if (sKeyHookInstalled) {
return;
}
- // The INT 9 handler runs at hardware-interrupt time, so its code and the
- // ring-buffer state it touches must be locked into physical memory. A
- // page fault inside the ISR under a paging DPMI host (e.g. CWSDPMI) would
- // be fatal. On lock failure we degrade gracefully: key-up detection is
- // simply unavailable rather than risking a crash.
- if (platformLockRegion((const void *)int9Handler, KEYUP_ISR_SIZE, true) != 0) {
- fprintf(stderr, "KEYUP: Failed to lock handler code\n");
+ if (platformLockRegion((const void *)int9Handler, ISR_LOCK_SIZE, true) != 0) {
+ dvxLog("Platform: could not lock the keyboard handler; no raw keys");
return;
}
- if (platformLockRegion(&sKeyUp, sizeof(sKeyUp), false) != 0) {
- fprintf(stderr, "KEYUP: Failed to lock ISR state\n");
- platformUnlockRegion((const void *)int9Handler, KEYUP_ISR_SIZE, true);
+ if (platformLockRegion(&platformKeyScan, sizeof(platformKeyScan), false) != 0) {
+ dvxLog("Platform: could not lock the keyboard state; no raw keys");
+ platformUnlockRegion((const void *)int9Handler, ISR_LOCK_SIZE, true);
return;
}
+ memset(&platformKeyScan, 0, sizeof(platformKeyScan));
_go32_dpmi_get_protected_mode_interrupt_vector(PS2_KBD_IRQ_VECTOR, &sOldInt9);
sNewInt9.pm_offset = (unsigned long)int9Handler;
@@ -1523,42 +1561,23 @@ void platformKeyUpInit(void) {
_go32_dpmi_chain_protected_mode_interrupt_vector(PS2_KBD_IRQ_VECTOR, &sNewInt9);
- sKeyUpInstalled = true;
+ sKeyHookInstalled = true;
+ irqServicesArmAtExit();
}
-bool platformKeyUpRead(PlatformKeyEventT *evt) {
- if (sKeyUp.tail == sKeyUp.head) {
- return false;
- }
-
- *evt = sKeyUp.buf[sKeyUp.tail];
- sKeyUp.tail = (sKeyUp.tail + 1) % KEYUP_BUF_SIZE;
-
- // The ISR queues raw set-1 codes; F11/F12 are the only keys whose
- // INT 16h key-down scancode differs, so report the same code for both.
- if (evt->scancode == PS2_SCAN_F11) {
- evt->scancode = KEY_SCAN_F11;
- } else if (evt->scancode == PS2_SCAN_F12) {
- evt->scancode = KEY_SCAN_F12;
- }
-
- return true;
-}
-
-
-void platformKeyUpShutdown(void) {
- if (!sKeyUpInstalled) {
+// The regions are unlocked only once the IRQ no longer points at the
+// handler.
+void platformKeyHookShutdown(void) {
+ if (!sKeyHookInstalled) {
return;
}
_go32_dpmi_set_protected_mode_interrupt_vector(PS2_KBD_IRQ_VECTOR, &sOldInt9);
- sKeyUpInstalled = false;
+ sKeyHookInstalled = false;
- // Unlock the regions pinned in platformKeyUpInit, after the IRQ is no
- // longer pointed at our handler.
- platformUnlockRegion(&sKeyUp, sizeof(sKeyUp), false);
- platformUnlockRegion((const void *)int9Handler, KEYUP_ISR_SIZE, true);
+ platformUnlockRegion(&platformKeyScan, sizeof(platformKeyScan), false);
+ platformUnlockRegion((const void *)int9Handler, ISR_LOCK_SIZE, true);
}
@@ -1609,6 +1628,40 @@ void platformLogCrashDetail(int sig, PlatformLogFnT logFn) {
}
+// PIT clocks up to the last interrupt plus the part of the running
+// period: from the TSC once a period has been measured, else from the
+// latched channel 0 count (and the PIC, for an interrupt not yet
+// serviced). Taken with interrupts off so the interrupt cannot move the
+// base in between; never below a value handed out before.
+uint64_t platformMicros(void) {
+ TimerServiceT *t = &platformTimer;
+
+ if (!t->running) {
+ return 0;
+ }
+
+ uint32_t state = platformIrqSave();
+ uint32_t elapsed = 0;
+
+ if (t->tscPeriod != 0) {
+ elapsed = timerTscElapsed(readTsc(), t->tscAtIrq, t->tscPeriod, t->tscPeriodDivisor, t->divisor);
+ } else {
+ elapsed = pitLatchElapsed();
+ }
+
+ uint64_t now = t->pitCount + elapsed;
+
+ if (now < t->lastRead) {
+ now = t->lastRead;
+ }
+
+ t->lastRead = now;
+ platformIrqRestore(state);
+
+ return timerPitToMicros(now);
+}
+
+
// Initializes the INT 33h mouse driver. The mouse driver is a TSR
// (or emulated by the DOS environment) that tracks position and
// buttons independently of the application.
@@ -2168,6 +2221,89 @@ void platformSplashShutdown(void) {
}
+// Pin the handler and everything it touches, put PIT channel 0 into rate
+// generator mode at the BIOS rate (the BIOS's square wave cannot be read
+// back reliably: it counts down twice per period) and take INT 8.
+void platformTimerInit(void) {
+ if (platformTimer.running) {
+ return;
+ }
+
+ if (!timerLock()) {
+ dvxLog("Platform: could not lock the timer service; no timers");
+ return;
+ }
+
+ _go32_dpmi_get_protected_mode_interrupt_vector(TIMER_IRQ_VECTOR, &sOldInt8);
+ sOldInt8Far.offset = (uint32_t)sOldInt8.pm_offset;
+ sOldInt8Far.selector = sOldInt8.pm_selector;
+ sNewInt8.pm_offset = (unsigned long)int8Handler;
+ sNewInt8.pm_selector = _go32_my_cs();
+
+ if (_go32_dpmi_allocate_iret_wrapper(&sNewInt8) != 0) {
+ dvxLog("Platform: no interrupt wrapper for the timer service; no timers");
+ timerUnlock();
+ return;
+ }
+
+ memset(&platformTimer, 0, sizeof(platformTimer));
+ platformTimer.divisor = PLATFORM_PIT_BIOS_DIVISOR;
+ platformTimer.tscPresent = cpuHasTsc();
+
+ uint32_t state = platformIrqSave();
+
+ outportb(PIT_CMD_PORT, PIT_CH0_RATE_GEN);
+ outportb(PIT_CH0_PORT, 0);
+ outportb(PIT_CH0_PORT, 0);
+ _go32_dpmi_set_protected_mode_interrupt_vector(TIMER_IRQ_VECTOR, &sNewInt8);
+ platformTimer.running = true;
+ platformIrqRestore(state);
+
+ irqServicesArmAtExit();
+ dvxLog("Platform: timer service started (clock from the %s)", platformTimer.tscPresent ? "TSC" : "PIT");
+}
+
+
+// Writing the command byte restarts channel 0 at once with the new count
+// (writing only the count would let the running period, up to 55 ms at
+// the BIOS rate, finish first). The part of the period already run is
+// counted by timerRestartPeriod; on a TSC CPU the interpolation starts
+// over from now.
+void platformTimerProgram(uint32_t divisor) {
+ uint32_t elapsed = pitLatchElapsed();
+
+ outportb(PIT_CMD_PORT, PIT_CH0_RATE_GEN);
+ outportb(PIT_CH0_PORT, divisor & BYTE_MASK);
+ outportb(PIT_CH0_PORT, (divisor >> BYTE_BITS) & BYTE_MASK);
+
+ if (platformTimer.tscPresent) {
+ platformTimer.tscAtIrq = readTsc();
+ }
+
+ timerRestartPeriod(&platformTimer, elapsed, divisor);
+}
+
+
+void platformTimerShutdown(void) {
+ if (!platformTimer.running) {
+ return;
+ }
+
+ uint32_t state = platformIrqSave();
+
+ outportb(PIT_CMD_PORT, PIT_CH0_SQUARE_WAVE);
+ outportb(PIT_CH0_PORT, 0);
+ outportb(PIT_CH0_PORT, 0);
+ _go32_dpmi_set_protected_mode_interrupt_vector(TIMER_IRQ_VECTOR, &sOldInt8);
+ memset(platformTimer.slots, 0, sizeof(platformTimer.slots));
+ platformTimer.running = false;
+ platformIrqRestore(state);
+
+ _go32_dpmi_free_iret_wrapper(&sNewInt8);
+ timerUnlock();
+}
+
+
// Release a range pinned by platformLockRegion.
int32_t platformUnlockRegion(const void *addr, uint32_t size, bool isCode) {
__dpmi_meminfo region;
@@ -2455,6 +2591,16 @@ void platformYield(void) {
}
+// Time-stamp counter (the caller knows the CPU has one: cpuHasTsc).
+ISR_INLINE uint64_t readTsc(void) {
+ uint32_t lo;
+ uint32_t hi;
+
+ __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
+ return ((uint64_t)hi << TSC_HI_SHIFT) | lo;
+}
+
+
// The linear region of a near pointer range in this program's CS (isCode)
// or DS space, for the DPMI lock/unlock calls.
static bool regionFromNear(const void *addr, uint32_t size, bool isCode, __dpmi_meminfo *out) {
@@ -2474,7 +2620,8 @@ static bool regionFromNear(const void *addr, uint32_t size, bool isCode, __dpmi_
// Scores a supported mode against the requested parameters:
//
// Base score by bpp: 16-bit=100, 15-bit=90, 32-bit=85, 8-bit=70
-// +20 if bpp matches preferredBpp
+// +40 if bpp matches preferredBpp (outweighs any base difference, so
+// a depth the user chose is used whenever the resolution has it)
// +10 if exact resolution match, -10 if oversize
// -1 (rejected) if the mode is below the requested resolution
//
@@ -2601,6 +2748,34 @@ static void sysInfoDrives(void) {
}
+// Pin the timer interrupt's code and data; all or nothing.
+static bool timerLock(void) {
+ if (platformLockRegion((const void *)int8Handler, ISR_LOCK_SIZE, true) != 0) {
+ return false;
+ }
+
+ if (platformLockRegion(&platformTimer, sizeof(platformTimer), false) != 0) {
+ platformUnlockRegion((const void *)int8Handler, ISR_LOCK_SIZE, true);
+ return false;
+ }
+
+ if (platformLockRegion(&sOldInt8Far, sizeof(sOldInt8Far), false) != 0) {
+ platformUnlockRegion(&platformTimer, sizeof(platformTimer), false);
+ platformUnlockRegion((const void *)int8Handler, ISR_LOCK_SIZE, true);
+ return false;
+ }
+
+ return true;
+}
+
+
+static void timerUnlock(void) {
+ platformUnlockRegion(&sOldInt8Far, sizeof(sOldInt8Far), false);
+ platformUnlockRegion(&platformTimer, sizeof(platformTimer), false);
+ platformUnlockRegion((const void *)int8Handler, ISR_LOCK_SIZE, true);
+}
+
+
// Unlocks and frees the DPMI physical LFB mapping recorded by mapLfb.
// Safe to call when nothing is mapped (sLfbLinearAddr == LFB_NOT_MAPPED)
// and idempotent: the sentinel is reset so a second call is a no-op.
@@ -2868,6 +3043,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformClock)
DXE_EXPORT(platformClockHz)
DXE_EXPORT(platformCopyFile)
+ DXE_EXPORT(platformDirIsWritable)
DXE_EXPORT(platformDiskSpace)
DXE_EXPORT(platformDriveList)
DXE_EXPORT(platformEntropyStamp)
@@ -2880,15 +3056,24 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformInjectKey)
DXE_EXPORT(platformInjectModifiers)
DXE_EXPORT(platformInjectMouse)
+ DXE_EXPORT(platformInjectScan)
DXE_EXPORT(platformInstallCrashHandler)
+ DXE_EXPORT(platformIrqRestore)
+ DXE_EXPORT(platformIrqSave)
+ DXE_EXPORT(platformIsReadOnly)
DXE_EXPORT(platformKeyboardGetModifiers)
DXE_EXPORT(platformKeyboardRead)
- DXE_EXPORT(platformKeyUpInit)
- DXE_EXPORT(platformKeyUpRead)
- DXE_EXPORT(platformKeyUpShutdown)
+ DXE_EXPORT(platformKeyEventRead)
+ DXE_EXPORT(platformKeyHookInit)
+ DXE_EXPORT(platformKeyHookShutdown)
+ DXE_EXPORT(platformKeyIsDown)
+ DXE_EXPORT(platformKeyStateClear)
+ DXE_EXPORT(platformKeyStateGet)
DXE_EXPORT(platformLineEnding)
DXE_EXPORT(platformLockRegion)
DXE_EXPORT(platformLogCrashDetail)
+ DXE_EXPORT(platformLogMemory)
+ DXE_EXPORT(platformMicros)
DXE_EXPORT(platformMkdirRecursive)
DXE_EXPORT(platformMouseInit)
DXE_EXPORT(platformMousePoll)
@@ -2906,7 +3091,12 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformRegisterDxeExports)
DXE_EXPORT(platformRegisterSymOverrides)
DXE_EXPORT(platformReplaceFile)
+ DXE_EXPORT(platformSetAppExitHook)
+ DXE_EXPORT(platformSetLogMemory)
DXE_EXPORT(platformSetLogPath)
+ DXE_EXPORT(platformSetReadOnly)
+ DXE_EXPORT(platformShutdownHookAdd)
+ DXE_EXPORT(platformShutdownHookRemove)
DXE_EXPORT(platformSpanCopy16)
DXE_EXPORT(platformSpanCopy32)
DXE_EXPORT(platformSpanCopy8)
@@ -2914,6 +3104,14 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformSpanFill32)
DXE_EXPORT(platformSpanFill8)
DXE_EXPORT(platformStripLineEndings)
+ DXE_EXPORT(platformTempDir)
+ DXE_EXPORT(platformTimerAdd)
+ DXE_EXPORT(platformTimerAddFast)
+ DXE_EXPORT(platformTimerInit)
+ DXE_EXPORT(platformTimerRemove)
+ DXE_EXPORT(platformTimerRemoveOwner)
+ DXE_EXPORT(platformTimerShutdown)
+ DXE_EXPORT(platformTimerStats)
DXE_EXPORT(platformUnlockRegion)
DXE_EXPORT(platformValidateFilename)
DXE_EXPORT(platformVideoEnumModes)
@@ -2924,6 +3122,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformYield)
// --- DPMI/go32 (needed by rs232 serial driver) ---
+ DXE_EXPORT(__dpmi_allocate_dos_memory)
+ DXE_EXPORT(__dpmi_free_dos_memory)
DXE_EXPORT(__dpmi_get_segment_base_address)
DXE_EXPORT(__dpmi_lock_linear_region)
DXE_EXPORT(__dpmi_unlock_linear_region)
@@ -3067,9 +3267,10 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(usleep)
// --- process / environment ---
- DXE_EXPORT(abort)
+ // exit() and abort() end only the calling program (platformAppExit)
+ { "_abort", (void *)platformAppAbort },
DXE_EXPORT(atexit)
- DXE_EXPORT(exit)
+ { "_exit", (void *)platformAppExit },
DXE_EXPORT(getenv)
DXE_EXPORT(putenv)
DXE_EXPORT(setenv)
@@ -3183,6 +3384,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(write)
// --- additional POSIX/libc commonly needed by library DXEs ---
+ DXE_EXPORT(_chmod)
DXE_EXPORT(dup)
DXE_EXPORT(dup2)
DXE_EXPORT(fcntl)
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
index 46f3859a..f45e361e 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
@@ -31,19 +31,24 @@
// 2. Flush: memcpy per scanline.
// 3. Spans: plain C fills/copies (16/32-bit fills replicate the value).
// 4. Mouse: a queue fed by hostMousePush; one sample per poll.
-// 5. Keyboard: queues fed by hostKeyPush/hostKeyUpPush; the modifier
-// byte is whatever the harness last set.
+// 5. Keyboard: a queue fed by hostKeyPush; the modifier byte is
+// whatever the harness last set. Raw keys go through the
+// same decoder as on DOS (platformInjectScan), and
+// hostKeyUpPush queues a bare release event.
//
-// Time comes from a fake clock (platformClock) that advances only when
-// the harness says so, which makes double-click, tooltip, blink and timer
-// behaviour deterministic. hostClockSetRealTime switches to wall time
-// for interactive runs.
+// Time comes from a fake clock (platformClock, platformMicros) that
+// advances only when the harness says so, which makes double-click,
+// tooltip, blink and timer behaviour deterministic. Advancing it also
+// delivers the timer interrupts that time would have brought, so the
+// timer service (dvxPlatIsr.h) runs exactly as on DOS, without threads.
+// hostClockSetRealTime switches to wall time for interactive runs.
//
// Everything test-facing is declared in dvxPlatHost.h; everything DVX
// proper sees is the dvxPlat.h contract.
#include "dvxPlat.h"
#include "dvxPlatHost.h"
+#include "dvxPlatIsr.h"
#include "dvxPal.h"
#include "thirdparty/stb_image_write_wrap.h"
@@ -70,8 +75,8 @@
// Fake clock resolution: one tick per millisecond.
#define HOST_CLOCK_HZ 1000
-#define MS_PER_SEC 1000
-#define NS_PER_MS 1000000
+#define US_PER_MS 1000
+#define NS_PER_US 1000
#define NS_PER_SEC 1000000000ULL
// Input queue depths. A frame consumes one mouse sample and every
@@ -118,14 +123,17 @@ typedef struct {
// Prototypes
// ============================================================
+void __wrap_abort(void) __attribute__((noreturn));
+void __wrap_exit(int status) __attribute__((noreturn));
static void clampMouse(int32_t *x, int32_t *y);
static void crashHandler(int sig);
static void fillFormat(DisplayT *d, int32_t bpp);
+static uint64_t hostPitNow(void);
static bool queueIsEmpty(const HostQueueT *q);
static bool queueIsFull(const HostQueueT *q);
static int32_t queuePop(HostQueueT *q);
static int32_t queuePush(HostQueueT *q);
-static uint32_t wallClockMs(void);
+static uint64_t wallClockUs(void);
// ============================================================
@@ -150,11 +158,20 @@ static uint8_t sPalette[PAL_ENTRIES * PAL_CHANNELS];
// platformVideoInit calls still to fail (hostVideoFailNext).
static int32_t sVideoFailCount = 0;
-// Fake clock.
-static uint32_t sTicks = 0;
-static bool sRealTime = false;
-static uint32_t sRealTimeBase = WALL_CLOCK_UNSET; // wall ms when real-time mode began
-static uint32_t sRealTimeStart = 0; // fake ticks when real-time mode began
+// Bytes pinned by platformLockRegion and not yet released (hostLockedBytes).
+static int64_t sLockedBytes = 0;
+
+// Fake clock: UI ticks (ms) and the microsecond clock, which advance
+// together (sTickRemUs carries the part of a millisecond not yet counted).
+static uint32_t sTicks = 0;
+static uint64_t sMicros = 0;
+static uint32_t sTickRemUs = 0;
+static uint32_t sYieldStepUs = 0; // hostClockSetYieldStep
+static bool sRealTime = false;
+static uint64_t sRealTimeBase = WALL_CLOCK_UNSET; // wall us when real-time mode began
+static uint32_t sRealTimeStart = 0; // fake ticks when real-time mode began
+static uint64_t sRealTimeMicros = 0; // fake us when real-time mode began
+static uint64_t sTimerStartUs = 0; // sMicros when the timer service started
// Mouse.
static HostMouseSampleT sMouseQueue[MOUSE_QUEUE_SIZE];
@@ -167,8 +184,6 @@ static int32_t sWheelDelta = 0;
// Keyboard.
static PlatformKeyEventT sKeyQueue[KEY_QUEUE_SIZE];
static HostQueueT sKeyQ = { 0, 0, KEY_QUEUE_SIZE };
-static PlatformKeyEventT sKeyUpQueue[KEY_QUEUE_SIZE];
-static HostQueueT sKeyUpQ = { 0, 0, KEY_QUEUE_SIZE };
static int32_t sModifiers = 0;
// Crash recovery.
@@ -180,6 +195,20 @@ static PlatformLogFnT sCrashLogFn = NULL;
static char sSysInfoBuf[PLATFORM_SYSINFO_MAX];
+// Link-time stand-ins for exit() and abort() in app shared objects built
+// with HOST_APP_LDFLAGS (common.mk: -Wl,--wrap=exit,--wrap=abort), the
+// host counterpart of the DOS export table's mapping. The names are the
+// ones the linker's --wrap option expects.
+void __wrap_abort(void) {
+ platformAppAbort();
+}
+
+
+void __wrap_exit(int status) {
+ platformAppExit(status);
+}
+
+
static void clampMouse(int32_t *x, int32_t *y) {
if (*x < 0) {
*x = 0;
@@ -257,10 +286,36 @@ static void fillFormat(DisplayT *d, int32_t bpp) {
// Fake-clock controls: while real-time mode is on platformClock reads wall
-// time and these only change the fake counter, which real-time mode then
-// overwrites when it is switched off.
+// time and these only change the fake counters, which real-time mode then
+// overwrites when it is switched off. A negative step only moves the UI
+// ticks (the microsecond clock never goes back).
void hostClockAdvance(int32_t ms) {
- sTicks += (uint32_t)ms;
+ if (ms <= 0) {
+ sTicks += (uint32_t)ms;
+ return;
+ }
+
+ hostClockAdvanceUs((uint64_t)ms * US_PER_MS);
+}
+
+
+// The timer interrupts falling into the step run as the PIT would deliver
+// them: one per divisor PIT clocks since the service started.
+void hostClockAdvanceUs(uint64_t us) {
+ sMicros += us;
+ sTickRemUs += (uint32_t)(us % US_PER_MS);
+ sTicks += (uint32_t)(us / US_PER_MS + sTickRemUs / US_PER_MS);
+ sTickRemUs %= US_PER_MS;
+
+ if (!platformTimer.running) {
+ return;
+ }
+
+ uint64_t pitNow = hostPitNow();
+
+ while (pitNow - platformTimer.pitCount >= platformTimer.divisor) {
+ timerServiceTick(&platformTimer);
+ }
}
@@ -275,16 +330,23 @@ void hostClockSetRealTime(bool realTime) {
}
if (realTime) {
- sRealTimeBase = wallClockMs();
- sRealTimeStart = sTicks;
+ sRealTimeBase = wallClockUs();
+ sRealTimeStart = sTicks;
+ sRealTimeMicros = sMicros;
} else {
- sTicks = platformClock();
+ sTicks = platformClock();
+ sMicros = platformMicros();
}
sRealTime = realTime;
}
+void hostClockSetYieldStep(uint32_t us) {
+ sYieldStepUs = us;
+}
+
+
const uint8_t *hostFramebuffer(const DisplayT *d) {
return d->lfb;
}
@@ -305,14 +367,12 @@ void hostKeyPush(int32_t ascii, int32_t scancode, int32_t modifiers) {
void hostKeyUpPush(int32_t scancode) {
- int32_t idx = queuePush(&sKeyUpQ);
+ keyScanQueue(&platformKeyScan, (uint8_t)scancode, false, false, false);
+}
- if (idx < 0) {
- return;
- }
- sKeyUpQueue[idx].ascii = 0;
- sKeyUpQueue[idx].scancode = scancode;
+int64_t hostLockedBytes(void) {
+ return sLockedBytes;
}
@@ -330,6 +390,12 @@ void hostMousePush(int32_t x, int32_t y, int32_t buttons) {
}
+// PIT input clocks since the timer service started, on the fake clock.
+static uint64_t hostPitNow(void) {
+ return (sMicros - sTimerStartUs) * PLATFORM_PIT_HZ / PLATFORM_US_PER_SEC;
+}
+
+
bool hostSavePng(const char *path) {
const DisplayT *d = sDisplay;
@@ -416,7 +482,7 @@ char platformBootDrive(void) {
PlatformTicksT platformClock(void) {
if (sRealTime) {
- return sRealTimeStart + (wallClockMs() - sRealTimeBase);
+ return sRealTimeStart + (uint32_t)((wallClockUs() - sRealTimeBase) / US_PER_MS);
}
return sTicks;
@@ -574,6 +640,17 @@ void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal
}
+void platformIrqRestore(uint32_t state) {
+ (void)state;
+}
+
+
+// Nothing to disable in a host process.
+uint32_t platformIrqSave(void) {
+ return 0;
+}
+
+
int32_t platformKeyboardGetModifiers(void) {
return sModifiers;
}
@@ -592,25 +669,13 @@ bool platformKeyboardRead(PlatformKeyEventT *evt) {
}
-void platformKeyUpInit(void) {
- sKeyUpQ.head = 0;
- sKeyUpQ.tail = 0;
+void platformKeyHookInit(void) {
+ memset(&platformKeyScan, 0, sizeof(platformKeyScan));
}
-bool platformKeyUpRead(PlatformKeyEventT *evt) {
- if (queueIsEmpty(&sKeyUpQ)) {
- return false;
- }
-
- *evt = sKeyUpQueue[queuePop(&sKeyUpQ)];
- return true;
-}
-
-
-void platformKeyUpShutdown(void) {
- sKeyUpQ.head = 0;
- sKeyUpQ.tail = 0;
+void platformKeyHookShutdown(void) {
+ memset(&platformKeyScan, 0, sizeof(platformKeyScan));
}
@@ -622,8 +687,8 @@ const char *platformLineEnding(void) {
// No paging concerns in a host process: nothing to pin.
int32_t platformLockRegion(const void *addr, uint32_t size, bool isCode) {
(void)addr;
- (void)size;
(void)isCode;
+ sLockedBytes += size;
return 0;
}
@@ -646,6 +711,15 @@ void platformLogCrashDetail(int sig, PlatformLogFnT logFn) {
}
+uint64_t platformMicros(void) {
+ if (sRealTime) {
+ return sRealTimeMicros + (wallClockUs() - sRealTimeBase);
+ }
+
+ return sMicros;
+}
+
+
void platformMouseInit(int32_t screenW, int32_t screenH) {
sMouseRangeW = screenW;
sMouseRangeH = screenH;
@@ -781,10 +855,34 @@ void platformSplashShutdown(void) {
}
+void platformTimerInit(void) {
+ if (platformTimer.running) {
+ return;
+ }
+
+ memset(&platformTimer, 0, sizeof(platformTimer));
+ platformTimer.divisor = PLATFORM_PIT_BIOS_DIVISOR;
+ platformTimer.running = true;
+ sTimerStartUs = sMicros;
+}
+
+
+// The simulated PIT restarts at once, like the real one.
+void platformTimerProgram(uint32_t divisor) {
+ timerRestartPeriod(&platformTimer, (uint32_t)(hostPitNow() - platformTimer.pitCount), divisor);
+}
+
+
+void platformTimerShutdown(void) {
+ memset(platformTimer.slots, 0, sizeof(platformTimer.slots));
+ platformTimer.running = false;
+}
+
+
int32_t platformUnlockRegion(const void *addr, uint32_t size, bool isCode) {
(void)addr;
- (void)size;
(void)isCode;
+ sLockedBytes -= size;
return 0;
}
@@ -911,8 +1009,12 @@ void platformVideoShutdown(DisplayT *d) {
}
+// Nothing else is running in a test process; the fake clock moves on by
+// the yield step (hostClockSetYieldStep), in real-time mode not at all.
void platformYield(void) {
- // Nothing else is running in a test process.
+ if (sYieldStepUs > 0 && !sRealTime) {
+ hostClockAdvanceUs(sYieldStepUs);
+ }
}
@@ -948,9 +1050,9 @@ static int32_t queuePush(HostQueueT *q) {
}
-static uint32_t wallClockMs(void) {
+static uint64_t wallClockUs(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
- return (uint32_t)((uint64_t)ts.tv_sec * MS_PER_SEC + (uint64_t)ts.tv_nsec / NS_PER_MS);
+ return (uint64_t)ts.tv_sec * PLATFORM_US_PER_SEC + (uint64_t)ts.tv_nsec / NS_PER_US;
}
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
new file mode 100644
index 00000000..a3f51556
--- /dev/null
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
@@ -0,0 +1,267 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// dvxPlatformIrq.c -- held-key table and timer service, every backend
+//
+// The state of the keyboard decoder and of the timer service (see
+// dvxPlatIsr.h) and the functions of dvxPlat.h that work on it outside
+// interrupt time. Whatever the interrupt can change at the same moment is
+// read and written with interrupts disabled (platformIrqSave). The
+// backends own what is hardware-specific: installing the interrupt hooks,
+// the PIT itself and the clock readout (platformMicros).
+
+#include "dvxPlatIsr.h"
+
+#include
+
+// "No timer registered" as a rate (timerApplyRate).
+#define TIMER_RATE_NONE 0
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void platformInjectScan(uint8_t scan);
+bool platformKeyEventRead(RawKeyEventT *evt);
+bool platformKeyIsDown(int32_t rawCode);
+void platformKeyStateClear(void);
+void platformKeyStateGet(uint8_t out[KEY_RAW_COUNT]);
+bool platformShutdownHookAdd(PlatformShutdownFnT fn);
+void platformShutdownHookRemove(PlatformShutdownFnT fn);
+void platformShutdownHooksRun(void);
+int32_t platformTimerAdd(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
+int32_t platformTimerAddFast(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
+void platformTimerRemove(int32_t timerId);
+void platformTimerRemoveOwner(int32_t ownerAppId);
+void platformTimerStats(PlatformTimerStatsT *out);
+static int32_t timerAdd(uint32_t hz, uint32_t maxHz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
+static void timerApplyRate(void);
+static uint32_t timerDivisorForHz(uint32_t hz);
+
+// ============================================================
+// Module state
+// ============================================================
+
+KeyScanStateT platformKeyScan;
+TimerServiceT platformTimer;
+
+static PlatformShutdownFnT sShutdownHooks[PLATFORM_SHUTDOWN_HOOKS];
+
+
+void platformInjectScan(uint8_t scan) {
+ uint32_t state = platformIrqSave();
+
+ keyScanFeed(&platformKeyScan, scan);
+ platformIrqRestore(state);
+}
+
+
+// Single reader: only the decoder moves head, only this moves tail.
+bool platformKeyEventRead(RawKeyEventT *evt) {
+ uint32_t tail = platformKeyScan.tail;
+
+ if (tail == platformKeyScan.head) {
+ return false;
+ }
+
+ // The event is read only after head showed it complete.
+ __atomic_signal_fence(__ATOMIC_ACQUIRE);
+
+ *evt = platformKeyScan.ring[tail % PLATFORM_KEY_EVENT_QUEUE];
+ platformKeyScan.tail = tail + 1;
+ return true;
+}
+
+
+bool platformKeyIsDown(int32_t rawCode) {
+ if (rawCode < 0 || rawCode >= KEY_RAW_COUNT) {
+ return false;
+ }
+
+ return platformKeyScan.down[rawCode] == KEY_STATE_DOWN;
+}
+
+
+void platformKeyStateClear(void) {
+ uint32_t state = platformIrqSave();
+
+ memset((void *)platformKeyScan.down, KEY_STATE_UP, sizeof(platformKeyScan.down));
+ platformIrqRestore(state);
+}
+
+
+void platformKeyStateGet(uint8_t out[KEY_RAW_COUNT]) {
+ uint32_t state = platformIrqSave();
+
+ memcpy(out, (const void *)platformKeyScan.down, KEY_RAW_COUNT);
+ platformIrqRestore(state);
+}
+
+
+bool platformShutdownHookAdd(PlatformShutdownFnT fn) {
+ int32_t freeSlot = -1;
+
+ for (int32_t i = 0; i < PLATFORM_SHUTDOWN_HOOKS; i++) {
+ if (sShutdownHooks[i] == fn) {
+ return true;
+ }
+
+ if (!sShutdownHooks[i] && freeSlot < 0) {
+ freeSlot = i;
+ }
+ }
+
+ if (freeSlot < 0) {
+ return false;
+ }
+
+ sShutdownHooks[freeSlot] = fn;
+ return true;
+}
+
+
+void platformShutdownHookRemove(PlatformShutdownFnT fn) {
+ for (int32_t i = 0; i < PLATFORM_SHUTDOWN_HOOKS; i++) {
+ if (sShutdownHooks[i] == fn) {
+ sShutdownHooks[i] = NULL;
+ }
+ }
+}
+
+
+// Each hook is forgotten before it runs, so a crash inside one (or a
+// second exit path) never runs it twice.
+void platformShutdownHooksRun(void) {
+ for (int32_t i = 0; i < PLATFORM_SHUTDOWN_HOOKS; i++) {
+ PlatformShutdownFnT fn = sShutdownHooks[i];
+
+ sShutdownHooks[i] = NULL;
+
+ if (fn) {
+ fn();
+ }
+ }
+}
+
+
+int32_t platformTimerAdd(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId) {
+ return timerAdd(hz, PLATFORM_TIMER_MAX_HZ, fn, userData, ownerAppId);
+}
+
+
+int32_t platformTimerAddFast(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId) {
+ return timerAdd(hz, PLATFORM_TIMER_FAST_MAX_HZ, fn, userData, ownerAppId);
+}
+
+
+void platformTimerRemove(int32_t timerId) {
+ if (timerId < 0 || timerId >= PLATFORM_TIMER_SLOTS) {
+ return;
+ }
+
+ uint32_t state = platformIrqSave();
+
+ platformTimer.slots[timerId].fn = NULL;
+ timerApplyRate();
+ platformIrqRestore(state);
+}
+
+
+void platformTimerRemoveOwner(int32_t ownerAppId) {
+ for (int32_t i = 0; i < PLATFORM_TIMER_SLOTS; i++) {
+ if (platformTimer.slots[i].fn && platformTimer.slots[i].owner == ownerAppId) {
+ platformTimerRemove(i);
+ }
+ }
+}
+
+
+void platformTimerStats(PlatformTimerStatsT *out) {
+ uint32_t state = platformIrqSave();
+
+ out->divisor = platformTimer.divisor;
+ out->interrupts = platformTimer.interrupts;
+ out->biosTicks = platformTimer.biosTicks;
+ platformIrqRestore(state);
+
+ out->rateHz = (PLATFORM_PIT_HZ + out->divisor / 2) / out->divisor;
+}
+
+
+// The slot is filled with interrupts off, so the timer interrupt never
+// sees a half-written one. Its accumulator starts empty once the rate is
+// set (a restart credits the cut-short period to the running timers), so
+// the first call comes one interval later, to within an interrupt period.
+static int32_t timerAdd(uint32_t hz, uint32_t maxHz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId) {
+ if (!fn || hz == 0 || hz > maxHz || !platformTimer.running) {
+ return -1;
+ }
+
+ for (int32_t i = 0; i < PLATFORM_TIMER_SLOTS; i++) {
+ TimerSlotT *slot = &platformTimer.slots[i];
+
+ if (!slot->fn) {
+ uint32_t state = platformIrqSave();
+
+ slot->userData = userData;
+ slot->hz = hz;
+ slot->owner = ownerAppId;
+ slot->fn = fn;
+ timerApplyRate();
+ slot->acc = 0;
+ platformIrqRestore(state);
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+
+// Run the interrupt as fast as the fastest timer needs. Called with
+// interrupts disabled; the PIT is only touched when the rate changes.
+static void timerApplyRate(void) {
+ uint32_t fastest = TIMER_RATE_NONE;
+
+ for (int32_t i = 0; i < PLATFORM_TIMER_SLOTS; i++) {
+ if (platformTimer.slots[i].fn && platformTimer.slots[i].hz > fastest) {
+ fastest = platformTimer.slots[i].hz;
+ }
+ }
+
+ uint32_t divisor = timerDivisorForHz(fastest);
+
+ if (divisor != platformTimer.divisor) {
+ platformTimerProgram(divisor);
+ }
+}
+
+
+// The largest divisor whose rate is at least hz, so a timer never needs
+// two calls in one interrupt; never slower than the BIOS rate.
+static uint32_t timerDivisorForHz(uint32_t hz) {
+ if (hz == TIMER_RATE_NONE || PLATFORM_PIT_HZ / hz >= PLATFORM_PIT_BIOS_DIVISOR) {
+ return PLATFORM_PIT_BIOS_DIVISOR;
+ }
+
+ return PLATFORM_PIT_HZ / hz;
+}
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
index 6edbce2f..f08b25dd 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
@@ -78,6 +78,12 @@
#define ALT_NAV_SCAN_FIRST 0x97
#define ALT_NAV_COUNT (KEY_SCAN_DELETE - KEY_SCAN_HOME + 1)
+// Longest line the memory log takes (longer ones are cut)
+#define LOG_LINE_MAX 512
+
+// Scratch file platformDirIsWritable makes and removes
+#define WRITE_PROBE_NAME "DVXWTEST.$$$"
+
// ============================================================
@@ -114,11 +120,17 @@ int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, c
int32_t dvxTrimRight(char *buf);
static const ScanFoldT *findScanFold(int32_t scancode);
static bool globMatchLen(const char *pat, int32_t patLen, const char *name);
+static void logMemAppend(const char *fmt, va_list ap);
char platformAltScanToChar(int32_t scancode);
+void platformAppAbort(void);
+void platformAppExit(int status);
int32_t platformChdir(const char *path);
bool platformCopyFile(const char *srcPath, const char *dstPath);
+bool platformDirIsWritable(const char *dir);
bool platformFilterMatch(const char *name, const char *filter);
bool platformGlobMatch(const char *pattern, const char *name);
+bool platformIsReadOnly(void);
+const char *platformLogMemory(void);
int32_t platformMkdirRecursive(const char *path);
int32_t platformNormalizeFKeyScan(int32_t scancode);
void platformNormalizeKeyEvent(PlatformKeyEventT *evt, int32_t liveMods);
@@ -129,17 +141,32 @@ bool platformPathIsAbsolute(const char *path);
bool platformPathSibling(char *out, int32_t outSize, const char *path, const char *ext);
char *platformReadFile(const char *path, int32_t *outLen);
bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakPath);
+void platformSetAppExitHook(PlatformAppExitFnT fn);
+void platformSetLogMemory(void);
void platformSetLogPath(const char *path);
+void platformSetReadOnly(bool readOnly);
int32_t platformStripLineEndings(char *buf, int32_t len);
+const char *platformTempDir(void);
// ============================================================
// Module state
// ============================================================
-// Current dvxLog destination (see platformSetLogPath).
+// Current dvxLog destination (see platformSetLogPath); NULL = memory
+// (platformSetLogMemory).
static const char *sLogPath = DVX_LOG_FILE;
+// The memory log and its length (without the NUL)
+static char sLogMem[PLATFORM_LOG_MEM_MAX];
+static int32_t sLogMemLen = 0;
+
+// DVX's folder cannot be written to (platformSetReadOnly)
+static bool sReadOnly = false;
+
+// Where exit() and abort() of loaded modules go first (platformAppExit).
+static PlatformAppExitFnT sAppExitHook = NULL;
+
// Every modifier-specific scancode run the keyboard path folds.
static const ScanFoldT sScanFolds[] = {
{ FKEY_SCAN_SHIFT_F1, FKEY_LOW_COUNT, KEY_SCAN_F1, KEY_MOD_SHIFT, 0, KEY_MOD_CTRL | KEY_MOD_ALT },
@@ -202,13 +229,21 @@ bool dvxHasExt(const char *name, const char *ext) {
// Append one formatted line to the log file. Opens/closes the file
// per-write so it is never held open across a crash or a task switch.
void dvxLog(const char *fmt, ...) {
+ va_list ap;
+
+ if (!sLogPath) {
+ va_start(ap, fmt);
+ logMemAppend(fmt, ap);
+ va_end(ap);
+ return;
+ }
+
FILE *f = fopen(sLogPath, "a");
if (!f) {
return;
}
- va_list ap;
va_start(ap, fmt);
vfprintf(f, fmt, ap);
va_end(ap);
@@ -406,6 +441,37 @@ static bool globMatchLen(const char *pat, int32_t patLen, const char *name) {
}
+// One line into the memory log; when it is full the oldest lines go.
+static void logMemAppend(const char *fmt, va_list ap) {
+ char line[LOG_LINE_MAX];
+ int32_t n = vsnprintf(line, sizeof(line) - 1, fmt, ap);
+
+ if (n < 0) {
+ return;
+ }
+
+ if (n > (int32_t)sizeof(line) - 2) {
+ n = (int32_t)sizeof(line) - 2;
+ }
+
+ line[n++] = '\n';
+
+ int32_t over = sLogMemLen + n - (PLATFORM_LOG_MEM_MAX - 1);
+
+ if (over > 0) {
+ const char *cut = (const char *)memchr(sLogMem + over, '\n', (size_t)(sLogMemLen - over));
+ int32_t drop = cut ? (int32_t)(cut - sLogMem) + 1 : sLogMemLen;
+
+ memmove(sLogMem, sLogMem + drop, (size_t)(sLogMemLen - drop));
+ sLogMemLen -= drop;
+ }
+
+ memcpy(sLogMem + sLogMemLen, line, (size_t)n);
+ sLogMemLen += n;
+ sLogMem[sLogMemLen] = '\0';
+}
+
+
char platformAltScanToChar(int32_t scancode) {
if (scancode < 0 || scancode >= ALT_SCAN_TABLE_SIZE) {
return 0;
@@ -415,6 +481,28 @@ char platformAltScanToChar(int32_t scancode) {
}
+// abort() of a loaded module: the program ends like an exit with
+// PLATFORM_ABORT_STATUS. Without a running program, the C library abort.
+void platformAppAbort(void) {
+ if (sAppExitHook) {
+ sAppExitHook(PLATFORM_ABORT_STATUS);
+ }
+
+ abort();
+}
+
+
+// exit() of a loaded module. The hook does not return while a program is
+// running; otherwise this is the C library exit.
+void platformAppExit(int status) {
+ if (sAppExitHook) {
+ sAppExitHook(status);
+ }
+
+ exit(status);
+}
+
+
int32_t platformChdir(const char *path) {
#ifdef __DJGPP__
if (path[0] && path[1] == ':') {
@@ -476,6 +564,26 @@ bool platformCopyFile(const char *srcPath, const char *dstPath) {
// Tokens are the runs between FILTER_SEPARATORS; no copy of the filter is
// made, so its length is not limited.
+bool platformDirIsWritable(const char *dir) {
+ char path[DVX_MAX_PATH];
+ int32_t n = snprintf(path, sizeof(path), "%s" DVX_PATH_SEP WRITE_PROBE_NAME, dir);
+
+ if (n <= 0 || n >= (int32_t)sizeof(path)) {
+ return false;
+ }
+
+ FILE *f = fopen(path, "wb");
+
+ if (!f) {
+ return false;
+ }
+
+ bool ok = fclose(f) == 0;
+
+ return remove(path) == 0 && ok;
+}
+
+
bool platformFilterMatch(const char *name, const char *filter) {
int32_t allLen = (int32_t)strlen(PLATFORM_FILTER_ALL);
bool anyToken = false;
@@ -516,6 +624,16 @@ bool platformGlobMatch(const char *pattern, const char *name) {
// A DOS "X:" drive prefix and the root separator after it are skipped so
// we never try to mkdir("C:") or mkdir("C:\"). A path too long for the
// buffer fails outright instead of creating a truncated directory.
+bool platformIsReadOnly(void) {
+ return sReadOnly;
+}
+
+
+const char *platformLogMemory(void) {
+ return sLogMem;
+}
+
+
int32_t platformMkdirRecursive(const char *path) {
char buf[DVX_MAX_PATH];
@@ -764,11 +882,26 @@ bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakP
}
+void platformSetAppExitHook(PlatformAppExitFnT fn) {
+ sAppExitHook = fn;
+}
+
+
+void platformSetLogMemory(void) {
+ sLogPath = NULL;
+}
+
+
void platformSetLogPath(const char *path) {
sLogPath = path ? path : DVX_LOG_FILE;
}
+void platformSetReadOnly(bool readOnly) {
+ sReadOnly = readOnly;
+}
+
+
// Remove every carriage return in place and NUL-terminate; buf must hold
// len + 1 bytes. Used when reading DOS-format text files on hosts that
// don't auto-strip them.
@@ -785,3 +918,14 @@ int32_t platformStripLineEndings(char *buf, int32_t len) {
return dst;
}
+
+const char *platformTempDir(void) {
+ const char *dir = getenv("TEMP");
+
+ if (!dir || !dir[0]) {
+ dir = getenv("TMP");
+ }
+
+ return (dir && dir[0]) ? dir : NULL;
+}
+
diff --git a/src/libs/kpunch/libdvx/sdkGuide.dhs b/src/libs/kpunch/libdvx/sdkGuide.dhs
index a29aff1a..a13e1d7e 100644
--- a/src/libs/kpunch/libdvx/sdkGuide.dhs
+++ b/src/libs/kpunch/libdvx/sdkGuide.dhs
@@ -71,8 +71,12 @@ The DVX SDK holds everything needed to build DVX modules outside the DVX source
widget A C widget (NAME.wgt) usable from C programs and DVX BASIC
library A C library of shared functions (NAME.lib) with BASIC declarations
basic A DVX BASIC program (NAME.dbp, built to NAME.app)
+ game A C game (NAME.app): a 320x200 screen in a window, frame pacing, keyboard and joystick, sound effects and music
+ basicgame A DVX BASIC game (NAME.dbp): a FRAMEWAIT loop, GameKeyDown, STICK, STRIG and PLAY
.endtable
+The two game templates are explained in Writing Games for DVX.
+
NAME is a letter followed by letters and digits, 8 characters at most, because it becomes a DOS file name. Files are named NAME with its first letter in lower case (camelCase, such as myApp); DVX shows it in upper case. The script refuses DOS device names (CON, PRN, AUX, NUL, COM1 and so on), the name of a widget DVX already has, and an existing directory. The project goes in DIR, or a new directory of that name in the current one.
The template's names become yours everywhere: file names, identifiers in each case style (tplName, TplName, tplname, TPLNAME), help topic ids and the ABI stamp. The Makefile refers to the SDK by a relative path, so a project and the SDK can move together.
@@ -130,6 +134,7 @@ Nothing else is needed: DVX loads dvxnet.lib when it starts and connects the cal
.link sdk.debug Debugging: the Log and Crash Reports
.link sdk.abi Binary Interface Rules
.link sdk.packaging Packaging and Installing
+.link games.intro Writing Games for DVX
.topic sdk.firstapp
.title Your First DVX Program
@@ -430,7 +435,7 @@ make package lays out pkg/ exactly as the files go into the DVX directory; make
.table
Kind Directory Files
---- --------- -----
- Program APPS/// NAME.app, NAME.hlp
+ Program APPS/// NAME.app, NAME.hlp, the DATA files (if any)
Widget WIDGETS/// NAME.wgt, NAME.DHS, NAME.BHS, NAME.dep (if any)
Library LIBS/// NAME.lib, NAME.DHS, NAME.dep (if any)
.endtable
@@ -438,6 +443,7 @@ make package lays out pkg/ exactly as the files go into the DVX directory; make
.list
.item Programs carry their compiled help. Widgets and libraries carry help sources, which DVX compiles into its system help when the set of installed modules changes.
.item .dep files are written with DOS line endings.
+.item A program's data files (levels, graphics, sounds it loads at run time) go in the Makefile's DATA variable, for example DATA = levels sounds; the package puts them beside NAME.app, where the program finds them through appDir in its DxeAppContextT. Small ones can instead travel inside the .app as binary resources (see the .res file).
.item To ship a module, zip the contents of pkg/; the user unpacks it into the DVX directory. A library's header and .bas file go to the developers who use it, not into the DVX directory.
.item Removing a module is deleting its directory.
.endlist
diff --git a/src/libs/kpunch/music/Makefile b/src/libs/kpunch/music/Makefile
new file mode 100644
index 00000000..f384de93
--- /dev/null
+++ b/src/libs/kpunch/music/Makefile
@@ -0,0 +1,84 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+# Music Library Makefile for DJGPP cross-compilation
+#
+# music.lib (see music.h): libxmp-lite (thirdparty/libxmp-lite, built
+# unmodified with warnings off, as its own options say below) and our own
+# sources. The files that run at interrupt time (ISR_SRCS) are linked
+# last, so musicLock pins one span from their first function to the end
+# of the library's data; gcc must not move parts of them elsewhere or
+# turn loops into library calls. The OP2 instrument bank is copied
+# beside the library.
+
+include ../../../common.mk
+
+XMP_DIR = thirdparty/libxmp-lite
+XMP_SRCS = $(wildcard $(XMP_DIR)/src/*.c) $(wildcard $(XMP_DIR)/src/loaders/*.c)
+XMP_OPTS = -DLIBXMP_CORE_PLAYER -DLIBXMP_NO_PROWIZARD -DLIBXMP_NO_DEPACKERS -DBUILDING_STATIC -I$(XMP_DIR)/include -I$(XMP_DIR)/src
+XMP_CFLAGS = $(filter-out $(WARNFLAGS) $(DEPFLAGS),$(DOS_CFLAGS)) -w -fno-analyzer $(XMP_OPTS)
+
+ISR_SRCS = musicFm.c musicMpu.c musicSeq.c
+MAIN_SRCS = $(filter-out $(ISR_SRCS),$(wildcard *.c))
+ISR_CFLAGS = -fno-reorder-blocks-and-partition -fno-reorder-functions -fno-tree-loop-distribute-patterns
+CFLAGS = $(DOS_CFLAGS) $(ISR_CFLAGS) -I. -I$(XMP_DIR)/include -I../audio -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../libtasks -I../dvxshell
+
+OBJDIR = ../../../../obj/music
+LIBSDIR = ../../../../bin/libs
+TARGETDIR = $(LIBSDIR)/kpunch/music
+TARGET = $(TARGETDIR)/music.lib
+BANK = $(TARGETDIR)/genmidi.op2
+
+XMP_OBJS = $(patsubst $(XMP_DIR)/%.c,$(OBJDIR)/xmp/%.o,$(XMP_SRCS))
+OBJS = $(XMP_OBJS) $(patsubst %.c,$(OBJDIR)/%.o,$(MAIN_SRCS) $(ISR_SRCS))
+
+.PHONY: all clean
+
+all: $(TARGET) $(TARGETDIR)/music.dep $(BANK)
+
+$(TARGETDIR)/music.dep: music.dep | $(TARGETDIR)
+ sed 's/$$/\r/' $< > $@
+
+$(BANK): thirdparty/genmidi/GENMIDI.OP2 | $(TARGETDIR)
+ cp $< $@
+
+$(TARGET): $(OBJS) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/music.dxe -U $(OBJS)
+ mv $(TARGETDIR)/music.dxe $@
+
+$(OBJDIR)/xmp/%.o: $(XMP_DIR)/%.c $(DVX_MAKEFILES)
+ @mkdir -p $(dir $@)
+ $(CC) $(XMP_CFLAGS) -c -o $@ $<
+
+$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(OBJDIR):
+ mkdir -p $(OBJDIR)
+
+$(TARGETDIR):
+ mkdir -p $(TARGETDIR)
+
+-include $(patsubst %.c,$(OBJDIR)/%.d,$(MAIN_SRCS) $(ISR_SRCS))
+
+clean:
+ rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/music/music.c b/src/libs/kpunch/music/music.c
new file mode 100644
index 00000000..176bbd5e
--- /dev/null
+++ b/src/libs/kpunch/music/music.c
@@ -0,0 +1,1009 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// music.c -- musics, where they play, programs, the public functions
+//
+// See music.h and musicInt.h. All musics play through one audio context
+// of the library's own (opened for no program, so audio.lib's clean-up
+// of a program never closes it under a music); the library cleans up a
+// program's musics itself. The context is opened with the first music
+// and closed with the last.
+//
+// Where a MIDI music plays is decided at its first play (or when a live
+// MIDI music opens): DVX.INI [audio] midi = mpu401 and the port is free:
+// the MPU-401 port (one music at a time); = software: the software
+// synthesizer; otherwise the FM chip when there is one and no program has
+// it to itself, else the software synthesizer. On the FM chip and the
+// port the music timer (musicTick) plays the song; FM notes without a
+// voice go through the event ring to a software synthesizer on a
+// stream. With the software synthesizer the stream's fill function runs
+// the sequencer, SW_CHUNK_FRAMES frames at a time.
+//
+// The state a player is in lives in its IsrPlayerT for every kind of
+// music, so there is one place to look: the timer, the fill functions
+// and the program side all read it; a music that played to its end sets
+// ended, and musicPump calls the program's end function. A music on a
+// stream is rendered ahead of what is heard (up to its buffer), so when
+// its fill function reaches the end it only notes the frame and plays
+// silence (draining); the music ends when the stream has played past
+// that frame.
+
+#include "musicInt.h"
+#include "stb_ds.h"
+
+#include
+#include
+#include
+#include
+
+// ABI stamp checked by the loader (see DVX_ABI_VERSION).
+DVX_ABI_STAMP(music);
+
+// DVX.INI [audio] midiBank: the OP2 bank (path from the DVX folder)
+#define MUSIC_INI_BANK "midiBank"
+#define MUSIC_BANK_DEFAULT "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "music" DVX_PATH_SEP "genmidi.op2"
+
+#define MODULE_STREAM_MS 400
+#define MIDI_STREAM_MS 150
+#define SW_CHUNK_FRAMES 64
+#define SW_VOICES SYNTH_VOICES_MAX
+#define STREAM_BITS 16
+#define STREAM_CHANNELS 2
+#define US_PER_SEC 1000000ULL
+#define US_PER_MS 1000
+#define GAIN_SHIFT 8
+#define TIMER_NONE (-1)
+#define ERROR_TEXT_UNKNOWN "Unknown error"
+
+struct MusicT {
+ IsrPlayerT isr; // first: the timer reads it (the whole struct is pinned)
+ FmSynthT hwSynth; // MUSIC_OUT_FM: General MIDI on the FM chip
+ MpuSinkT mpu; // MUSIC_OUT_MPU401
+ EventRingT ring; // MUSIC_OUT_FM: notes without a voice, to swSynth
+ MusicT *next;
+ MidiFileT file;
+ XmpPlayerT *xmp;
+ FmSynthT *swSynth; // the software synthesizer (MIDI on it, or FM overflow)
+ SynthChipT *chip;
+ AudioStreamT *stream;
+ MusicEndFnT endFn;
+ void *endData;
+ uint64_t usAcc; // software sequencer: frames x 10^6 not yet turned into time
+ uint64_t framesOut; // frames given to the stream
+ uint64_t endFrame; // draining: the stream frame where the music ended
+ uint32_t rate; // stream rate
+ int32_t appId;
+ int32_t kind;
+ int32_t where;
+ int32_t volume;
+ int32_t isrSlot; // in musicIsrPlayers, -1: none
+ bool loop;
+ bool draining; // rendered to its end, not yet heard to it
+};
+
+static const char *sErrorText[MUSIC_ERR_COUNT] = {
+ "No error",
+ "Invalid argument",
+ "Not enough memory",
+ "The file could not be read",
+ "Not a music file (MOD, S3M, XM, IT or MIDI)",
+ "The music file is damaged or incomplete",
+ "No sound device can play this music",
+ "The instrument bank for MIDI music is missing or damaged",
+ "The music string has a mistake in it"
+};
+
+static MusicT *sMusics = NULL;
+static AudioCtxT *sCtx = NULL;
+static Op2BankT *sBank = NULL;
+static MusicT *sMpuUser = NULL;
+static int32_t sTimerId = TIMER_NONE;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t ctxEnsure(void);
+static int32_t decideMidi(MusicT *m);
+static int32_t gainOf(const MusicT *m);
+static void isrAdd(MusicT *m);
+static void isrRemove(MusicT *m);
+static bool isrSlotFree(void);
+static uint32_t midiFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
+static void midiSeek(MusicT *m, uint32_t us);
+static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
+int32_t musicAppCurrent(void);
+void musicAppEnded(int32_t appId);
+int32_t musicAppEnter(int32_t appId);
+const Op2BankT *musicBank(int32_t *err);
+const char *musicErrorText(int32_t err);
+void musicFree(MusicT *music);
+void musicGetInfo(const MusicT *music, MusicInfoT *info);
+uint32_t musicGetPosition(const MusicT *music);
+int32_t musicGetVolume(const MusicT *music);
+void musicIdle(void *unused);
+bool musicIsPlaying(const MusicT *music);
+int32_t musicLoad(const char *path, MusicT **out);
+int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out);
+int32_t musicMidiControl(MusicT *music, int32_t channel, int32_t controller, int32_t value);
+int32_t musicMidiNoteOff(MusicT *music, int32_t channel, int32_t note);
+int32_t musicMidiNoteOn(MusicT *music, int32_t channel, int32_t note, int32_t velocity);
+int32_t musicMidiOpen(MusicT **out);
+int32_t musicMidiPitchBend(MusicT *music, int32_t channel, int32_t bend);
+int32_t musicMidiProgram(MusicT *music, int32_t channel, int32_t program);
+static MusicT *musicNew(int32_t kind);
+int32_t musicPause(MusicT *music);
+int32_t musicPlay(MusicT *music, bool loop);
+void musicPump(void);
+int32_t musicResume(MusicT *music);
+void musicSetEndFn(MusicT *music, MusicEndFnT fn, void *userData);
+int32_t musicSetPosition(MusicT *music, uint32_t ms);
+int32_t musicSetVolume(MusicT *music, int32_t volume);
+void musicShutdown(void);
+int32_t musicStop(MusicT *music);
+static int32_t openSoftware(MusicT *m);
+static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill);
+static int32_t outputOpen(MusicT *m);
+static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1, int32_t data2);
+static void silence(MusicT *m);
+static void timerUpdate(void);
+
+
+// The library's own context, opened for no program.
+static int32_t ctxEnsure(void) {
+ if (!sCtx) {
+ int32_t prev = musicAppEnter(0);
+
+ sCtx = audioOpen();
+ musicAppEnter(prev);
+ }
+
+ return sCtx ? MUSIC_OK : MUSIC_ERR_NO_DEVICE;
+}
+
+
+// Where a MIDI music plays (see the top); sets it up. Without an
+// instrument bank only the MPU-401 port can play it.
+static int32_t decideMidi(MusicT *m) {
+ PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH);
+ const char *want = prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO);
+ int32_t mode = mpuModeChoices[prefsGetChoice(prefs, AUDIO_INI_SECTION, MUSIC_INI_MPU_MODE, mpuModeChoices, MPU_MODE_COUNT, mpuModeChoices[MPU_MODE_GM].iniName)].value;
+ bool mpu = strcasecmp(want, AUDIO_MIDI_MPU401) == 0;
+ bool soft = strcasecmp(want, AUDIO_MIDI_SOFTWARE) == 0;
+ AudioFmInfoT fm;
+ int32_t rc;
+
+ prefsClose(prefs);
+
+ if (mpu && !sMpuUser && isrSlotFree() && audioMidiAcquire(sCtx) == AUDIO_OK) {
+ sMpuUser = m;
+ m->where = MUSIC_OUT_MPU401;
+ mpuSinkInit(&m->mpu, sCtx, mode, gainOf(m));
+ mpuSinkStart(&m->mpu);
+ m->isr.sink = &m->mpu.sink;
+ m->isr.mpu = &m->mpu;
+ isrAdd(m);
+ return MUSIC_OK;
+ }
+
+ if (!musicBank(&rc)) {
+ return rc;
+ }
+
+ rc = openSoftware(m);
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ audioFmGetInfo(&fm);
+
+ if (!soft && fm.voices > 0 && !fm.rawOwned && isrSlotFree()) {
+ FmBackendT hw;
+
+ m->where = MUSIC_OUT_FM;
+ fmHwBackend(sCtx, &hw);
+ fmSynthInit(&m->hwSynth, &hw, sBank, &m->ring.sink, fm.stereo, gainOf(m));
+ m->isr.sink = &m->hwSynth.sink;
+ isrAdd(m);
+ return MUSIC_OK;
+ }
+
+ m->where = MUSIC_OUT_SOFTWARE;
+ m->isr.sink = &m->swSynth->sink;
+ return MUSIC_OK;
+}
+
+
+// The music's volume times the master volume, for synthesizers the mixer
+// does not scale.
+static int32_t gainOf(const MusicT *m) {
+ return (m->volume * audioGetMasterVolume()) >> GAIN_SHIFT;
+}
+
+
+// Hand the player to the timer (isrSlotFree said there is room).
+static void isrAdd(MusicT *m) {
+ for (int32_t i = 0; i < MUSIC_ISR_PLAYERS_MAX && m->isrSlot < 0; i++) {
+ if (!musicIsrPlayers[i]) {
+ uint32_t flags = platformIrqSave();
+
+ musicIsrPlayers[i] = &m->isr;
+ platformIrqRestore(flags);
+ m->isrSlot = i;
+ }
+ }
+
+ timerUpdate();
+}
+
+
+static void isrRemove(MusicT *m) {
+ if (m->isrSlot < 0) {
+ return;
+ }
+
+ uint32_t flags = platformIrqSave();
+
+ musicIsrPlayers[m->isrSlot] = NULL;
+ platformIrqRestore(flags);
+ m->isrSlot = -1;
+ timerUpdate();
+}
+
+
+// The timer can take one more player (else a software synthesizer plays
+// the music).
+static bool isrSlotFree(void) {
+ for (int32_t i = 0; i < MUSIC_ISR_PLAYERS_MAX; i++) {
+ if (!musicIsrPlayers[i]) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// The software synthesizer's stream: overflow notes from the FM chip, or
+// the song itself, SW_CHUNK_FRAMES at a time.
+static uint32_t midiFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) {
+ MusicT *m = (MusicT *)userData;
+ int16_t *out = (int16_t *)buf;
+
+ (void)stream;
+
+ if (m->where == MUSIC_OUT_FM) {
+ eventRingDrain(&m->ring, &m->swSynth->sink);
+ }
+
+ for (uint32_t done = 0; done < frames;) {
+ uint32_t n = frames - done < SW_CHUNK_FRAMES ? frames - done : SW_CHUNK_FRAMES;
+
+ if (m->where == MUSIC_OUT_SOFTWARE && m->isr.state == PLAY_PLAYING && !m->draining) {
+ m->usAcc += (uint64_t)n * US_PER_SEC;
+
+ uint32_t us = (uint32_t)(m->usAcc / m->rate);
+
+ m->usAcc -= (uint64_t)us * m->rate;
+
+ if (!seqAdvance(&m->isr.seq, us, &m->swSynth->sink)) {
+ m->swSynth->sink.silence(&m->swSynth->sink);
+ m->draining = true;
+ m->endFrame = m->framesOut + done + n;
+ }
+ }
+
+ synthRender(m->chip, out + done * STREAM_CHANNELS, n, STREAM_CHANNELS);
+ done += n;
+ }
+
+ m->framesOut += frames;
+ return frames;
+}
+
+
+// Back to the start (or to us), silenced; the timer may be playing it.
+static void midiSeek(MusicT *m, uint32_t us) {
+ uint32_t flags = platformIrqSave();
+
+ seqSeek(&m->isr.seq, us, m->isr.sink);
+ m->usAcc = 0;
+ platformIrqRestore(flags);
+}
+
+
+static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) {
+ MusicT *m = (MusicT *)userData;
+
+ (void)stream;
+
+ if (m->isr.state != PLAY_PLAYING || m->draining) {
+ memset(buf, 0, (size_t)frames * STREAM_CHANNELS * sizeof(int16_t));
+ } else if (!xmpRender(m->xmp, (int16_t *)buf, frames, m->loop)) {
+ m->draining = true;
+ m->endFrame = m->framesOut + frames;
+ }
+
+ m->framesOut += frames;
+ return frames;
+}
+
+
+// The program running now (0: none, or no shell).
+int32_t musicAppCurrent(void) {
+ return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
+}
+
+
+// The program's macro queues go first: they free musics of their own.
+void musicAppEnded(int32_t appId) {
+ musicBasForget(appId);
+ musicMmlAppEnded(appId);
+
+ MusicT *m = sMusics;
+
+ while (m) {
+ MusicT *next = m->next;
+
+ if (m->appId == appId) {
+ musicFree(m);
+ }
+
+ m = next;
+ }
+}
+
+
+// Make appId the current program and return the one before.
+int32_t musicAppEnter(int32_t appId) {
+ int32_t prev = musicAppCurrent();
+
+ if (dvxMemAppIdPtr) {
+ *dvxMemAppIdPtr = appId;
+ }
+
+ return prev;
+}
+
+
+// Loaded once (DVX.INI [audio] midiBank) and pinned (the timer reads it).
+const Op2BankT *musicBank(int32_t *err) {
+ if (sBank) {
+ *err = MUSIC_OK;
+ return sBank;
+ }
+
+ PrefsHandleT *prefs = prefsLoad(DVX_INI_PATH);
+ int32_t len = 0;
+ char *data = platformReadFile(prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK, MUSIC_BANK_DEFAULT), &len);
+ Op2BankT *bank = (Op2BankT *)malloc(sizeof(Op2BankT));
+
+ prefsClose(prefs);
+ *err = bank ? MUSIC_ERR_BANK : MUSIC_ERR_NO_MEMORY;
+
+ if (bank && data && op2Parse((const uint8_t *)data, (uint32_t)len, bank) == MUSIC_OK && platformLockRegion(bank, sizeof(Op2BankT), false) == 0) {
+ sBank = bank;
+ *err = MUSIC_OK;
+ } else {
+ dvxLog("Music: no usable MIDI instrument bank");
+ free(bank);
+ }
+
+ free(data);
+ return sBank;
+}
+
+
+// Anything from MUSIC_OK up is no error.
+const char *musicErrorText(int32_t err) {
+ if (err >= MUSIC_OK) {
+ return sErrorText[MUSIC_OK];
+ }
+
+ return err > -MUSIC_ERR_COUNT ? sErrorText[-err] : ERROR_TEXT_UNKNOWN;
+}
+
+
+void musicFree(MusicT *music) {
+ if (!music) {
+ return;
+ }
+
+ MusicT *m = music;
+
+ isrRemove(m);
+ silence(m);
+ audioStreamClose(m->stream);
+
+ // The port gets what is queued (the notes off) before it is let go.
+ if (sMpuUser == m) {
+ for (uint32_t i = 0; i < MPU_QUEUE_BYTES && m->mpu.tail != m->mpu.head; i++) {
+ mpuSinkDrain(&m->mpu, MPU_SYSEX_GAP_US);
+ }
+
+ audioMidiRelease(sCtx);
+ sMpuUser = NULL;
+ }
+
+ for (MusicT **mp = &sMusics; *mp; mp = &(*mp)->next) {
+ if (*mp == m) {
+ *mp = m->next;
+ break;
+ }
+ }
+
+ if (m->file.events) {
+ platformUnlockRegion(m->file.events, m->file.count * sizeof(MidiEventT), false);
+ platformUnlockRegion(m->file.sysex, m->file.sysexBytes, false);
+ }
+
+ midiFileFree(&m->file);
+ xmpClose(m->xmp);
+ free(m->swSynth);
+ free(m->chip);
+ platformUnlockRegion(m, sizeof(*m), false);
+ free(m);
+
+ if (!sMusics && sCtx) {
+ audioClose(sCtx);
+ sCtx = NULL;
+ }
+}
+
+
+void musicGetInfo(const MusicT *music, MusicInfoT *info) {
+ memset(info, 0, sizeof(*info));
+
+ if (!music) {
+ return;
+ }
+
+ AudioFmInfoT fm;
+ AudioMidiInfoT port;
+
+ info->kind = music->kind;
+ info->where = music->where;
+ info->playing = music->isr.state == PLAY_PLAYING;
+ info->paused = music->isr.state == PLAY_PAUSED;
+ info->looping = music->loop;
+ info->positionMs = musicGetPosition(music);
+
+ if (music->xmp) {
+ snprintf(info->title, sizeof(info->title), "%s", xmpTitle(music->xmp));
+ info->lengthMs = xmpLength(music->xmp);
+ } else {
+ info->lengthMs = music->file.length / US_PER_MS;
+ }
+
+ switch (music->where) {
+ case MUSIC_OUT_FM:
+ audioFmGetInfo(&fm);
+ snprintf(info->output, sizeof(info->output), "%s", fm.description);
+ break;
+
+ case MUSIC_OUT_MPU401:
+ audioMidiGetInfo(&port);
+ snprintf(info->output, sizeof(info->output), "%s (%s)", port.description, mpuModeChoices[music->mpu.mode].iniName);
+ break;
+
+ case MUSIC_OUT_SOFTWARE:
+ snprintf(info->output, sizeof(info->output), "FM software synthesizer");
+ break;
+
+ case MUSIC_OUT_STREAM:
+ snprintf(info->output, sizeof(info->output), "Sound device");
+ break;
+
+ default:
+ break;
+ }
+}
+
+
+uint32_t musicGetPosition(const MusicT *music) {
+ if (!music) {
+ return 0;
+ }
+
+ return music->xmp ? xmpPosition(music->xmp) : music->isr.seq.now / US_PER_MS;
+}
+
+
+int32_t musicGetVolume(const MusicT *music) {
+ return music ? music->volume : 0;
+}
+
+
+void musicIdle(void *unused) {
+ (void)unused;
+ musicMmlIdle();
+ musicPump();
+}
+
+
+bool musicIsPlaying(const MusicT *music) {
+ return music && music->isr.state == PLAY_PLAYING;
+}
+
+
+int32_t musicLoad(const char *path, MusicT **out) {
+ int32_t len = 0;
+ char *data = path ? platformReadFile(path, &len) : NULL;
+
+ *out = NULL;
+
+ if (!data) {
+ return path ? MUSIC_ERR_OPEN : MUSIC_ERR_ARG;
+ }
+
+ int32_t rc = musicLoadMem(data, (uint32_t)len, out);
+
+ free(data);
+ return rc;
+}
+
+
+// MIDI by its header; anything else is offered to libxmp.
+int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) {
+ const uint8_t *bytes = (const uint8_t *)data;
+
+ *out = NULL;
+
+ if (!data || size == 0) {
+ return MUSIC_ERR_ARG;
+ }
+
+ bool midi = midiIsFile(bytes, size);
+ int32_t rc = ctxEnsure();
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ MusicT *m = musicNew(midi ? MUSIC_KIND_MIDI : MUSIC_KIND_MODULE);
+
+ if (!m) {
+ rc = MUSIC_ERR_NO_MEMORY;
+ } else if (midi) {
+ rc = midiParse(bytes, size, &m->file);
+
+ if (rc == MUSIC_OK && (platformLockRegion(m->file.events, m->file.count * sizeof(MidiEventT), false) != 0 || platformLockRegion(m->file.sysex, m->file.sysexBytes, false) != 0)) {
+ rc = MUSIC_ERR_NO_MEMORY;
+ }
+
+ m->isr.seq.events = m->file.events;
+ m->isr.seq.sysex = m->file.sysex;
+ m->isr.seq.count = m->file.count;
+ m->isr.seq.length = m->file.length;
+ } else {
+ AudioInfoT info;
+
+ audioGetInfo(&info);
+ m->rate = info.rate;
+ rc = xmpOpen(bytes, size, m->rate, STREAM_CHANNELS, &m->xmp);
+ }
+
+ if (m && rc != MUSIC_OK) {
+ musicFree(m);
+ return rc;
+ }
+
+ *out = m;
+ return MUSIC_OK;
+}
+
+
+int32_t musicMidiControl(MusicT *music, int32_t channel, int32_t controller, int32_t value) {
+ return sendMidi(music, channel, MIDI_CONTROL, controller, value);
+}
+
+
+int32_t musicMidiNoteOff(MusicT *music, int32_t channel, int32_t note) {
+ return sendMidi(music, channel, MIDI_NOTE_OFF, note, 0);
+}
+
+
+int32_t musicMidiNoteOn(MusicT *music, int32_t channel, int32_t note, int32_t velocity) {
+ return sendMidi(music, channel, MIDI_NOTE_ON, note, velocity);
+}
+
+
+// A music with an empty song that loops: it plays (and never ends) from
+// the start.
+int32_t musicMidiOpen(MusicT **out) {
+ *out = NULL;
+
+ int32_t rc = ctxEnsure();
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ MusicT *m = musicNew(MUSIC_KIND_LIVE);
+
+ if (!m) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ rc = musicPlay(m, true);
+
+ if (rc != MUSIC_OK) {
+ musicFree(m);
+ return rc;
+ }
+
+ *out = m;
+ return MUSIC_OK;
+}
+
+
+int32_t musicMidiPitchBend(MusicT *music, int32_t channel, int32_t bend) {
+ if (bend < MUSIC_MIDI_BEND_MIN || bend > MUSIC_MIDI_BEND_MAX) {
+ return MUSIC_ERR_ARG;
+ }
+
+ int32_t value = bend + MIDI_BEND_CENTER;
+
+ return sendMidi(music, channel, MIDI_PITCH_BEND, value & MIDI_DATA_MASK, value >> MIDI_DATA_BITS);
+}
+
+
+int32_t musicMidiProgram(MusicT *music, int32_t channel, int32_t program) {
+ return sendMidi(music, channel, MIDI_PROGRAM, program, 0);
+}
+
+
+// A new music of the calling program, pinned (the timer may read it),
+// in the list; NULL when out of memory.
+static MusicT *musicNew(int32_t kind) {
+ MusicT *m = (MusicT *)calloc(1, sizeof(MusicT));
+
+ if (!m) {
+ return NULL;
+ }
+
+ if (platformLockRegion(m, sizeof(*m), false) != 0) {
+ free(m);
+ return NULL;
+ }
+
+ m->appId = musicAppCurrent();
+ m->kind = kind;
+ m->volume = MUSIC_VOLUME_MAX;
+ m->isrSlot = -1;
+ eventRingInit(&m->ring);
+ m->next = sMusics;
+ sMusics = m;
+ return m;
+}
+
+
+int32_t musicPause(MusicT *music) {
+ if (!music || music->isr.state != PLAY_PLAYING) {
+ return MUSIC_ERR_ARG;
+ }
+
+ music->isr.state = PLAY_PAUSED;
+ silence(music);
+ return MUSIC_OK;
+}
+
+
+// The first play decides where the music plays; a music at its end
+// starts over.
+int32_t musicPlay(MusicT *music, bool loop) {
+ if (!music) {
+ return MUSIC_ERR_ARG;
+ }
+
+ MusicT *m = music;
+ int32_t rc = outputOpen(m);
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ m->loop = loop;
+
+ // Rendered to its end but not heard to it yet: as if it had ended.
+ if (m->draining) {
+ m->draining = false;
+ m->isr.state = PLAY_STOPPED;
+ }
+
+ if (m->xmp) {
+ if (m->isr.state == PLAY_STOPPED && xmpPosition(m->xmp) > 0) {
+ xmpRestart(m->xmp);
+ }
+ } else {
+ m->isr.seq.loop = loop || m->kind == MUSIC_KIND_LIVE;
+
+ if (m->isr.state == PLAY_STOPPED && m->isr.seq.next >= m->isr.seq.count && m->isr.seq.count > 0) {
+ midiSeek(m, 0);
+ }
+ }
+
+ m->isr.ended = false;
+ m->isr.state = PLAY_PLAYING;
+ return MUSIC_OK;
+}
+
+
+// Streams first (audioPump), then end calls, each as the program that
+// owns the music; the list is walked again after every call (a call may
+// free musics).
+void musicPump(void) {
+ bool again = true;
+
+ audioPump();
+
+ for (MusicT *m = sMusics; m; m = m->next) {
+ if (m->draining && m->framesOut - audioStreamQueued(m->stream) >= m->endFrame) {
+ m->draining = false;
+ m->isr.state = PLAY_STOPPED;
+ m->isr.ended = true;
+ }
+ }
+
+ while (again) {
+ again = false;
+
+ for (MusicT *m = sMusics; m; m = m->next) {
+ if (!m->isr.ended) {
+ continue;
+ }
+
+ m->isr.ended = false;
+
+ if (m->endFn) {
+ int32_t prev = musicAppEnter(m->appId);
+
+ m->endFn(m, m->endData);
+ musicAppEnter(prev);
+ again = true;
+ break;
+ }
+ }
+ }
+}
+
+
+int32_t musicResume(MusicT *music) {
+ if (!music || music->isr.state != PLAY_PAUSED) {
+ return MUSIC_ERR_ARG;
+ }
+
+ music->isr.state = PLAY_PLAYING;
+ return MUSIC_OK;
+}
+
+
+void musicSetEndFn(MusicT *music, MusicEndFnT fn, void *userData) {
+ if (music) {
+ music->endFn = fn;
+ music->endData = userData;
+ }
+}
+
+
+int32_t musicSetPosition(MusicT *music, uint32_t ms) {
+ if (!music || music->kind == MUSIC_KIND_LIVE) {
+ return MUSIC_ERR_ARG;
+ }
+
+ if (music->xmp) {
+ xmpSeek(music->xmp, ms);
+ return MUSIC_OK;
+ }
+
+ int32_t rc = outputOpen(music);
+
+ if (rc == MUSIC_OK) {
+ midiSeek(music, ms * US_PER_MS);
+ }
+
+ return rc;
+}
+
+
+int32_t musicSetVolume(MusicT *music, int32_t volume) {
+ if (!music) {
+ return MUSIC_ERR_ARG;
+ }
+
+ music->volume = volume < 0 ? 0 : (volume > MUSIC_VOLUME_MAX ? MUSIC_VOLUME_MAX : volume);
+
+ if (music->stream) {
+ audioVoiceSetVolume(sCtx, audioStreamVoice(music->stream), music->volume);
+ }
+
+ uint32_t flags = platformIrqSave();
+
+ if (music->where == MUSIC_OUT_FM) {
+ fmSynthSetGain(&music->hwSynth, gainOf(music));
+ } else if (music->where == MUSIC_OUT_MPU401) {
+ mpuSinkSetGain(&music->mpu, gainOf(music));
+ }
+
+ platformIrqRestore(flags);
+ return MUSIC_OK;
+}
+
+
+// The library is being unloaded: free every music, the bank.
+void musicShutdown(void) {
+ musicMmlShutdown();
+
+ while (sMusics) {
+ musicFree(sMusics);
+ }
+
+ if (sBank) {
+ platformUnlockRegion(sBank, sizeof(Op2BankT), false);
+ free(sBank);
+ sBank = NULL;
+ }
+}
+
+
+int32_t musicStop(MusicT *music) {
+ if (!music) {
+ return MUSIC_ERR_ARG;
+ }
+
+ music->isr.state = PLAY_STOPPED;
+ music->isr.ended = false;
+ music->draining = false;
+ silence(music);
+
+ if (music->xmp) {
+ xmpRestart(music->xmp);
+ } else if (music->isr.sink) {
+ midiSeek(music, 0);
+ }
+
+ return MUSIC_OK;
+}
+
+
+// The software synthesizer (and its stream): for MIDI on it, or as the
+// FM chip's overflow. Its gain is full: the stream's volume is the
+// music's.
+static int32_t openSoftware(MusicT *m) {
+ AudioInfoT info;
+ FmBackendT soft;
+
+ audioGetInfo(&info);
+ m->rate = info.rate;
+ m->swSynth = (FmSynthT *)calloc(1, sizeof(FmSynthT));
+ m->chip = (SynthChipT *)calloc(1, sizeof(SynthChipT));
+
+ if (!m->swSynth || !m->chip) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ synthInit(m->chip, m->rate, SW_VOICES);
+ synthBackend(m->chip, &soft);
+ fmSynthInit(m->swSynth, &soft, sBank, NULL, true, MUSIC_VOLUME_MAX);
+ return openStream(m, MIDI_STREAM_MS, midiFill);
+}
+
+
+static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill) {
+ AudioInfoT info;
+ AudioFormatT fmt;
+
+ audioGetInfo(&info);
+ fmt.rate = m->rate ? m->rate : info.rate;
+ fmt.bits = STREAM_BITS;
+ fmt.channels = STREAM_CHANNELS;
+
+ int32_t rc = audioStreamOpen(sCtx, &fmt, bufferMs, fill, m, &m->stream);
+
+ if (rc != AUDIO_OK) {
+ m->stream = NULL;
+ return rc == AUDIO_ERR_NO_MEMORY ? MUSIC_ERR_NO_MEMORY : MUSIC_ERR_NO_DEVICE;
+ }
+
+ audioVoiceSetVolume(sCtx, audioStreamVoice(m->stream), m->volume);
+ return MUSIC_OK;
+}
+
+
+// Where the music plays, decided and set up at the first need: a
+// module's stream, or where MIDI goes.
+static int32_t outputOpen(MusicT *m) {
+ if (m->where != MUSIC_OUT_NONE) {
+ return MUSIC_OK;
+ }
+
+ int32_t rc;
+
+ if (m->kind == MUSIC_KIND_MODULE) {
+ m->where = MUSIC_OUT_STREAM;
+ rc = openStream(m, MODULE_STREAM_MS, moduleFill);
+ } else {
+ rc = decideMidi(m);
+ }
+
+ if (rc != MUSIC_OK) {
+ m->where = MUSIC_OUT_NONE;
+ }
+
+ return rc;
+}
+
+
+// A live channel message to wherever the music plays (with interrupts
+// off while the timer may be sending too).
+static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1, int32_t data2) {
+ if (!m || m->kind == MUSIC_KIND_MODULE || channel < 0 || channel >= MUSIC_MIDI_CHANNELS || data1 < 0 || data1 > MUSIC_MIDI_DATA_MAX || data2 < 0 || data2 > MUSIC_MIDI_DATA_MAX) {
+ return MUSIC_ERR_ARG;
+ }
+
+ int32_t rc = outputOpen(m);
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ uint32_t flags = platformIrqSave();
+
+ m->isr.sink->message(m->isr.sink, (uint8_t)(type | channel), (uint8_t)data1, (uint8_t)data2);
+ platformIrqRestore(flags);
+ return MUSIC_OK;
+}
+
+
+// Every sounding note off now (MIDI; modules stop by themselves).
+static void silence(MusicT *m) {
+ if (m->isr.sink) {
+ uint32_t flags = platformIrqSave();
+
+ m->isr.sink->silence(m->isr.sink);
+ platformIrqRestore(flags);
+ }
+}
+
+
+// The timer runs while any music is on the synthesizers; the code it
+// runs is pinned meanwhile.
+static void timerUpdate(void) {
+ bool any = false;
+
+ for (int32_t i = 0; i < MUSIC_ISR_PLAYERS_MAX; i++) {
+ any = any || musicIsrPlayers[i] != NULL;
+ }
+
+ if (any && sTimerId == TIMER_NONE) {
+ if (!musicLock(true)) {
+ dvxLog("Music: could not pin the music player in memory");
+ }
+
+ sTimerId = platformTimerAdd(MUSIC_TICK_HZ, musicTick, NULL, 0);
+ } else if (!any && sTimerId != TIMER_NONE) {
+ platformTimerRemove(sTimerId);
+ sTimerId = TIMER_NONE;
+ musicLock(false);
+ }
+}
diff --git a/src/libs/kpunch/music/music.dep b/src/libs/kpunch/music/music.dep
new file mode 100644
index 00000000..4f928468
--- /dev/null
+++ b/src/libs/kpunch/music/music.dep
@@ -0,0 +1,4 @@
+libtasks
+libdvx
+dvxshell
+audio
diff --git a/src/libs/kpunch/music/music.dhs b/src/libs/kpunch/music/music.dhs
new file mode 100644
index 00000000..c44f8b6b
--- /dev/null
+++ b/src/libs/kpunch/music/music.dhs
@@ -0,0 +1,167 @@
+# The MIT License (MIT)
+#
+# Copyright (C) 2026 Scott Duensing
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+
+.section Technical Reference (for Developers)|Libraries
+.topic lib.music
+.title Music Library
+.toc 0 Music Library
+.index music
+.index music.h
+.index musicLoad
+.index musicPlay
+.index musicStop
+.index musicMidiOpen
+.index musicMidiNoteOn
+.index MusicInfoT
+.index MIDI
+.index MOD
+.index S3M
+.index XM
+.index IT
+.index General MIDI
+.index MT-32
+.index GENMIDI
+.index OP2
+.index libxmp-lite
+.index music.bas
+.index MusicLoad
+.index MusicPlay
+.index musicMmlOpen
+.index musicMmlPlay
+.index MusicMmlT
+.index Music macro language
+.index musicBasPlay
+
+.h1 Music Library
+
+Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
+
+Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank is bin/libs/kpunch/music/genmidi.op2.
+
+.h2 Playing a File
+
+.code
+MusicT *tune = NULL;
+int32_t rc = musicLoad("THEME.MID", &tune); // or musicLoadMem(data, size, &tune)
+
+if (rc != MUSIC_OK) {
+ dvxLog("%s", musicErrorText(rc));
+} else {
+ musicSetEndFn(tune, onTuneEnd, NULL);
+ musicPlay(tune, false); // true: loop until stopped
+}
+.endcode
+
+musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state) and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
+
+Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time.
+
+.h2 Where Music Plays
+
+Modules are rendered by libxmp-lite into a stream of the sound device (16-bit stereo at the device's rate, 400 ms of buffer, linear interpolation), so they play on any device that plays sampled sound.
+
+MIDI goes to the best synthesizer there is, decided at the first musicPlay (or musicMidiOpen), with DVX.INI [audio] midi:
+
+.table
+ midi MIDI plays on
+ ---- -------------
+ auto the FM chip (OPL2 / OPL3) when there is one and no program has taken the whole chip; else the software synthesizer
+ mpu401 the MPU-401 port (mpuPort, default 330h) when it was found and no other music is using it; else as auto
+ fm as auto
+ software the software synthesizer
+.endtable
+
+On the FM chip and the MPU-401 port the song is played by the timer service (500 times a second), so it keeps time however busy the programs are. On the FM chip every note takes a voice from audio.lib's shared pool; when none is free the note plays on the software synthesizer instead, which then runs beside the chip (its stream is always open for that). The software synthesizer runs in the stream's fill function (the shell's idle time), 64 frames between event checks, 150 ms ahead of what is heard; a program that keeps the desktop busy every frame should call audioPump once a frame, as for any stream.
+
+.h2 General MIDI on FM
+
+Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank names another OP2 file instead (a path from the DVX folder), for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. The bank is read once, at the first MIDI music that needs it.
+
+What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.
+
+.h2 The Software Synthesizer
+
+When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.
+
+.h2 The MPU-401 Port
+
+With DVX.INI [audio] midi = mpu401, MIDI goes out of the MPU-401 port (audio.lib's mpu401 driver) to an external synthesizer, one music at a time (a second MIDI music meanwhile plays on the FM chip or the software synthesizer). Bytes are queued and sent by the timer as fast as the port takes them, with a 40 ms pause after every system exclusive message (first-generation MT-32s lose data without it). The music's volume scales every channel volume (CC7) sent. DVX.INI [audio] mpuMode says what is on the port:
+
+.table
+ mpuMode Treatment
+ ------- ---------
+ gm A General MIDI synthesizer (Roland SC-55 and the like): the music as it is, after a General MIDI System On message.
+ mt32 A Roland MT-32 (or CM-32L, LAPC-I) playing General MIDI music: the melodic channels move to the MT-32's eight parts (MIDI channels 2 to 9, in the order they are first used; a ninth channel shares the last part), percussion stays on channel 10, programs go through a General MIDI to MT-32 timbre table (our own nearest-sound choice), pan is mirrored (the MT-32 pans the other way), the bend range is set to 2 semitones by system exclusive messages at the start (and RPN 0 changes it the same way, since the MT-32 has no RPNs), and what it does not understand (other RPNs and NRPNs, bank select, reverb and chorus depth, aftertouch, system exclusive messages that are not Roland's) is left out.
+ raw Anything: the music exactly as it is, nothing added (for music written for an MT-32, with its own set-up messages).
+.endtable
+
+Stopping, pausing or freeing a music sends sustain off and all notes off on every channel the synthesizer hears.
+
+.h2 Live MIDI
+
+.code
+MusicT *keys = NULL;
+
+musicMidiOpen(&keys); // plays at once
+musicMidiProgram(keys, 0, 73); // flute on channel 0
+musicMidiNoteOn(keys, 0, 60, 100); // middle C
+musicMidiNoteOff(keys, 0, 60);
+.endcode
+
+musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.
+
+.h2 Modules
+
+MOD (ProTracker and compatibles), S3M, XM and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.
+
+.h2 Music Macros
+
+.code
+MusicMmlT *tune = NULL;
+
+musicMmlOpen(&tune);
+musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");
+.endcode
+
+A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
+
+Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML) or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
+
+.h2 Several Programs at Once
+
+Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music. When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.
+
+.h2 BASIC
+
+src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it.
+
+The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount and musicBasBusy for the statements BEEP, SOUND, PLAY and the PLAY(n) function; each BASIC program has its own macro queue. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
+
+.h2 Licences
+
+libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+The GENMIDI instrument bank is from Freedoom 0.13.0, Copyright (C) 2001-2024 Contributors to the Freedoom project. All rights reserved. BSD 3-clause licence:
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Freedoom project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/libs/kpunch/music/music.h b/src/libs/kpunch/music/music.h
new file mode 100644
index 00000000..9da7a86f
--- /dev/null
+++ b/src/libs/kpunch/music/music.h
@@ -0,0 +1,300 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// music.h -- music library (music.lib)
+//
+// Music for DVX programs on whatever sound hardware the computer has:
+//
+// Modules tracker music (MOD, S3M, XM, IT), played by libxmp-lite
+// into a sound stream, so it plays on any card that plays
+// sampled sound.
+// MIDI Standard MIDI Files (type 0 and 1), played by DVX's own
+// sequencer on the best synthesizer there is: an external
+// MIDI synthesizer on the MPU-401 port when DVX.INI [audio]
+// midi = mpu401 (General MIDI, or a Roland MT-32 with
+// mpuMode), else the FM chip (OPL2 / OPL3) with the General
+// MIDI instrument bank (DVX.INI [audio] midiBank, an OP2
+// file, default the bundled Freedoom GENMIDI bank), else the
+// library's own FM software synthesizer, which sounds like
+// the FM chip and plays on any card that plays sampled
+// sound. When the FM chip runs out of voices, extra notes
+// play on the software synthesizer.
+// Live MIDI note on / off, program change, controllers and pitch bend
+// sent by the program, on the same synthesizers (a music
+// opened with musicMidiOpen, or one playing a file).
+// Macros tunes written in QBasic's music macro language ("T120 O3
+// L8 CDEFG"), played as square-wave tones or on General MIDI
+// instruments (musicMml...; BASIC's PLAY, SOUND and BEEP).
+//
+// Every music belongs to the program that loaded it; it is stopped and
+// freed when the program ends, however it ends. Any number of programs
+// may play music at the same time (they share the FM chip's voices; the
+// MPU-401 port plays one program's MIDI at a time and the others use the
+// FM chip or the software synthesizer).
+//
+// MIDI played on the synthesizers runs from the timer service, so it
+// keeps time however busy the programs are; modules and the software
+// synthesizer are made in the shell's idle time (audioPump), a quarter of
+// a second or so ahead. Module playback uses floating point (libxmp):
+// on a computer without a maths coprocessor it is slow.
+//
+// Functions returning int32_t return MUSIC_OK (0) on success and a
+// negative MUSIC_ERR_* code on failure (musicErrorText says what it
+// means). Nothing here may be called from an interrupt.
+#ifndef MUSIC_LIB_H
+#define MUSIC_LIB_H
+
+#include "dvxTypes.h"
+
+#include
+#include
+
+// Volume of a music: 0 (silent) .. MUSIC_VOLUME_MAX
+#define MUSIC_VOLUME_MAX 256
+
+// MIDI value ranges (live MIDI)
+#define MUSIC_MIDI_CHANNELS 16 // channels 0..15; 9 is percussion
+#define MUSIC_MIDI_DATA_MAX 127 // notes, velocities, programs, controller values
+#define MUSIC_MIDI_BEND_MIN (-8192)
+#define MUSIC_MIDI_BEND_MAX 8191
+
+// Text sizes in MusicInfoT (with the terminating zero)
+#define MUSIC_TITLE_MAX 64
+#define MUSIC_DESC_MAX 64
+
+// Results
+#define MUSIC_OK 0
+#define MUSIC_ERR_ARG (-1) // a bad argument
+#define MUSIC_ERR_NO_MEMORY (-2) // out of memory
+#define MUSIC_ERR_OPEN (-3) // the file could not be read
+#define MUSIC_ERR_FORMAT (-4) // not a music file DVX plays
+#define MUSIC_ERR_CORRUPT (-5) // damaged or incomplete file
+#define MUSIC_ERR_NO_DEVICE (-6) // no sound device, or nothing that can play this music
+#define MUSIC_ERR_BANK (-7) // the FM instrument bank is missing or damaged
+#define MUSIC_ERR_MACRO (-8) // a mistake in a music macro string
+#define MUSIC_ERR_COUNT 9 // MUSIC_OK and the eight errors
+
+// Opaque handle
+typedef struct MusicT MusicT;
+typedef struct MusicMmlT MusicMmlT;
+
+// What a music is
+typedef enum {
+ MUSIC_KIND_MODULE = 0, // MOD, S3M, XM, IT
+ MUSIC_KIND_MIDI = 1, // a MIDI file
+ MUSIC_KIND_LIVE = 2 // live MIDI only (musicMidiOpen)
+} MusicKindE;
+
+// Where it plays
+typedef enum {
+ MUSIC_OUT_NONE = 0, // not decided yet (before the first musicPlay)
+ MUSIC_OUT_STREAM = 1, // modules: a sound stream
+ MUSIC_OUT_MPU401 = 2, // MIDI: the MPU-401 port
+ MUSIC_OUT_FM = 3, // MIDI: the FM chip (extra notes on the software synthesizer)
+ MUSIC_OUT_SOFTWARE = 4 // MIDI: the software FM synthesizer
+} MusicOutputE;
+
+// A music (musicGetInfo).
+typedef struct {
+ char title[MUSIC_TITLE_MAX]; // the module's title ("" for MIDI files)
+ char output[MUSIC_DESC_MAX]; // for people, e.g. "OPL3 FM synthesizer at 388h"
+ int32_t kind; // MusicKindE
+ int32_t where; // MusicOutputE
+ uint32_t lengthMs; // 0 for live MIDI
+ uint32_t positionMs;
+ bool playing; // playing (not stopped, finished or paused)
+ bool paused;
+ bool looping;
+} MusicInfoT;
+
+// music played to its end (not looping). Called from the shell's idle
+// time, never from an interrupt, as the program that owns music.
+typedef void (*MusicEndFnT)(MusicT *music, void *userData);
+
+// ============================================================
+// Loading and playing
+// ============================================================
+
+// Load a module or MIDI file (by its contents, not its name) from a file
+// or from memory (copied). Nothing plays until musicPlay.
+int32_t musicLoad(const char *path, MusicT **out);
+int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out);
+
+// Stop music and free it. NULL is ignored.
+void musicFree(MusicT *music);
+
+// Play from the current position (the start, after a load or a stop);
+// with loop, start over at the end, until stopped. The first play
+// decides where the music plays.
+int32_t musicPlay(MusicT *music, bool loop);
+
+// Pause (silencing it) and go on from the same place.
+int32_t musicPause(MusicT *music);
+int32_t musicResume(MusicT *music);
+
+// Stop and go back to the start.
+int32_t musicStop(MusicT *music);
+
+// True while playing (not stopped, finished or paused).
+bool musicIsPlaying(const MusicT *music);
+
+// Volume of this music (0..MUSIC_VOLUME_MAX, default the maximum).
+int32_t musicGetVolume(const MusicT *music);
+int32_t musicSetVolume(MusicT *music, int32_t volume);
+
+// Position and length in milliseconds. Setting the position of a MIDI
+// file replays its settings (instruments, controllers) up to that point;
+// a module goes to the start of the pattern that holds that time.
+uint32_t musicGetPosition(const MusicT *music);
+int32_t musicSetPosition(MusicT *music, uint32_t ms);
+
+void musicGetInfo(const MusicT *music, MusicInfoT *info);
+
+// Call fn when music plays to its end (NULL: no call).
+void musicSetEndFn(MusicT *music, MusicEndFnT fn, void *userData);
+
+// Deliver end calls and fill the music streams now (audioPump too). The
+// shell does this in its idle time; a program that keeps the desktop
+// busy every frame should call it once a frame.
+void musicPump(void);
+
+// Text for a MUSIC_OK / MUSIC_ERR_* code.
+const char *musicErrorText(int32_t err);
+
+// ============================================================
+// Live MIDI
+// ============================================================
+
+// A music with no file, for live MIDI only; it plays at once (on the
+// same synthesizer a MIDI file would).
+int32_t musicMidiOpen(MusicT **out);
+
+// Channel messages: channel 0..15 (9 is percussion), data 0..127, bend
+// MUSIC_MIDI_BEND_MIN..MUSIC_MIDI_BEND_MAX (0 = no bend). Sent to a MIDI
+// music (a file playing, or musicMidiOpen); MUSIC_ERR_ARG for a module or
+// a value out of range.
+int32_t musicMidiControl(MusicT *music, int32_t channel, int32_t controller, int32_t value);
+int32_t musicMidiNoteOff(MusicT *music, int32_t channel, int32_t note);
+int32_t musicMidiNoteOn(MusicT *music, int32_t channel, int32_t note, int32_t velocity);
+int32_t musicMidiPitchBend(MusicT *music, int32_t channel, int32_t bend);
+int32_t musicMidiProgram(MusicT *music, int32_t channel, int32_t program);
+
+// ============================================================
+// Music macros
+// ============================================================
+//
+// QBasic's music macro language, played through a queue of the calling
+// program's own (freed when the program ends). Commands (letters in
+// either case, spaces ignored):
+// A-G a note in the current octave; # or + after it raises it a
+// semitone, - lowers it; a length may follow (like L), then
+// dots (each makes it half as long again)
+// N n note n (1-84; 0 is a rest) at the current length
+// O n octave 0-6 (default 4; octave 3 starts at middle C);
+// > and < go up and down one
+// L n length of the notes that follow: 1 whole, 2 half, 4 quarter
+// ... 64 (default 4)
+// P n a pause of length n (dots allowed)
+// T n tempo, quarter notes a minute (32-255, default 120)
+// MN ML MS notes sound 7/8 of their length (normal, the default), all
+// of it (legato) or 3/4 (staccato)
+// MF MB foreground (the program waits for the music, the default) or
+// background (it goes on; up to MUSIC_MML_BACKGROUND_MAX notes
+// may wait)
+// I n DVX's own: n 1-128 plays the notes that follow on General
+// MIDI instrument n (1 is the piano) on the music synthesizer,
+// I0 (the default) square-wave tones again
+// The X (execute a string) command and VARPTR$ parameters are not
+// supported. The settings stay from one call to the next.
+
+// Notes a program in background mode (MB) may have waiting before
+// musicMmlBusy asks it to wait (QBasic's music buffer)
+#define MUSIC_MML_BACKGROUND_MAX 32
+
+// A queue for the calling program.
+int32_t musicMmlOpen(MusicMmlT **out);
+
+// Stop and free it. NULL is ignored.
+void musicMmlClose(MusicMmlT *mml);
+
+// Queue the notes of a macro string; MUSIC_ERR_MACRO (nothing queued, the
+// settings unchanged) when it has a mistake.
+int32_t musicMmlPlay(MusicMmlT *mml, const char *macro);
+
+// Queue a square-wave tone of milliHz thousandths of a hertz (0: silence)
+// lasting us microseconds, after what is already queued (QBasic's SOUND).
+int32_t musicMmlTone(MusicMmlT *mml, uint32_t milliHz, uint32_t us);
+
+// Silence the queue now and forget what is in it.
+void musicMmlStop(MusicMmlT *mml);
+
+// Notes queued or playing (rests not counted).
+uint32_t musicMmlQueued(MusicMmlT *mml);
+
+// True while the program should wait before going on: in foreground mode
+// until everything queued has played, in background mode while more than
+// MUSIC_MML_BACKGROUND_MAX notes wait. Also moves the queue along (call
+// it while waiting).
+bool musicMmlBusy(MusicMmlT *mml);
+
+// ============================================================
+// BASIC's BEEP, SOUND and PLAY
+// ============================================================
+//
+// The DVX BASIC runtime calls these for the program running (each BASIC
+// program has a macro queue of its own). BEEP is SOUND 800 Hz for a
+// quarter second. SOUND's duration is in clock ticks (18.2 a second);
+// 0 stops everything queued; frequencies above AUDIO_TONE_MAX_HZ are
+// silent. Busy says whether the statement must still wait
+// (musicMmlBusy); PlayCount is PLAY(n), the notes waiting.
+int32_t musicBasBeep(void);
+bool musicBasBusy(void);
+int32_t musicBasPlay(const char *macro);
+int32_t musicBasPlayCount(void);
+int32_t musicBasSound(double hz, double ticks);
+
+// ============================================================
+// BASIC (src/include/basic/music.bas)
+// ============================================================
+
+// Each BASIC program's musics are numbered from 1 (0 = MUSIC_FAILED;
+// MusicError$ says why). Volumes are percent (0..100).
+void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value);
+const char *MusicError(void);
+void MusicFree(int32_t handle);
+int32_t MusicLength(int32_t handle);
+int32_t MusicLoad(const char *fileName);
+int32_t MusicMidiOpen(void);
+void MusicNoteOff(int32_t handle, int32_t channel, int32_t note);
+void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity);
+void MusicPause(int32_t handle);
+void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend);
+void MusicPlay(int32_t handle, int32_t loopIt);
+int32_t MusicPlaying(int32_t handle);
+int32_t MusicPosition(int32_t handle);
+void MusicProgram(int32_t handle, int32_t channel, int32_t program);
+void MusicResume(int32_t handle);
+void MusicSeek(int32_t handle, int32_t milliseconds);
+void MusicStop(int32_t handle);
+void MusicVolume(int32_t handle, int32_t volume);
+
+#endif // MUSIC_LIB_H
diff --git a/src/libs/kpunch/music/musicBas.c b/src/libs/kpunch/music/musicBas.c
new file mode 100644
index 00000000..e0e5c3c6
--- /dev/null
+++ b/src/libs/kpunch/music/musicBas.c
@@ -0,0 +1,283 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicBas.c -- DVX BASIC entry points of the music library
+//
+// Called through DECLARE LIBRARY (src/include/basic/music.bas), so the
+// signatures use only BASIC-marshalable types. A BASIC program refers
+// to its musics by number (1..BAS_SLOTS, 0 = failed; MusicError$ says
+// why); a number only works for the program that got it. MIDI channels
+// are 1..16 in BASIC (10 is percussion), volumes percent. The library
+// frees a program's musics when it ends (musicAppEnded, which also
+// forgets their numbers here). The musicBas... functions are BASIC's own
+// BEEP, SOUND and PLAY statements, called by the BASIC runtime, on the
+// program's macro queue.
+
+#include "musicInt.h"
+
+#include
+
+#define BAS_TRUE (-1)
+#define BAS_FALSE 0
+#define BAS_SLOTS 64
+#define BAS_FIRST_CHANNEL 1
+#define PERCENT_MAX 100
+
+// QBasic's clock tick (SOUND durations): 65536 PIT clocks of 1,193,182 Hz
+#define QB_TICK_US (65536.0 * 1000000.0 / 1193182.0)
+#define QB_BEEP_MILLIHZ 800000
+#define QB_BEEP_US 250000
+#define ROUND_HALF 0.5
+
+typedef struct {
+ MusicT *music;
+ int32_t appId;
+} BasSlotT;
+
+static BasSlotT sSlots[BAS_SLOTS];
+static int32_t sLastError = MUSIC_OK;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t basAdd(MusicT *music, int32_t rc);
+static MusicT *basGet(int32_t handle);
+static int32_t basResult(int32_t rc);
+int32_t musicBasBeep(void);
+bool musicBasBusy(void);
+void musicBasForget(int32_t appId);
+int32_t musicBasPlay(const char *macro);
+int32_t musicBasPlayCount(void);
+int32_t musicBasSound(double hz, double ticks);
+void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value);
+const char *MusicError(void);
+void MusicFree(int32_t handle);
+int32_t MusicLength(int32_t handle);
+int32_t MusicLoad(const char *fileName);
+int32_t MusicMidiOpen(void);
+void MusicNoteOff(int32_t handle, int32_t channel, int32_t note);
+void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity);
+void MusicPause(int32_t handle);
+void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend);
+void MusicPlay(int32_t handle, int32_t loopIt);
+int32_t MusicPlaying(int32_t handle);
+int32_t MusicPosition(int32_t handle);
+void MusicProgram(int32_t handle, int32_t channel, int32_t program);
+void MusicResume(int32_t handle);
+void MusicSeek(int32_t handle, int32_t milliseconds);
+void MusicStop(int32_t handle);
+void MusicVolume(int32_t handle, int32_t volume);
+
+
+// A number for a newly loaded music (rc its load result), or 0.
+static int32_t basAdd(MusicT *music, int32_t rc) {
+ if (rc != MUSIC_OK) {
+ return basResult(rc);
+ }
+
+ int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
+
+ for (int32_t i = 0; i < BAS_SLOTS; i++) {
+ if (!sSlots[i].music) {
+ sSlots[i].music = music;
+ sSlots[i].appId = appId;
+ sLastError = MUSIC_OK;
+ return i + 1;
+ }
+ }
+
+ musicFree(music);
+ return basResult(MUSIC_ERR_NO_MEMORY);
+}
+
+
+// The calling program's music with this number, else NULL (and the error
+// is noted).
+static MusicT *basGet(int32_t handle) {
+ int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
+
+ if (handle < 1 || handle > BAS_SLOTS || !sSlots[handle - 1].music || sSlots[handle - 1].appId != appId) {
+ sLastError = MUSIC_ERR_ARG;
+ return NULL;
+ }
+
+ return sSlots[handle - 1].music;
+}
+
+
+// 0 with the error kept for MusicError$.
+static int32_t basResult(int32_t rc) {
+ sLastError = rc;
+ return 0;
+}
+
+
+int32_t musicBasBeep(void) {
+ return musicMmlTone(musicMmlBasic(true), QB_BEEP_MILLIHZ, QB_BEEP_US);
+}
+
+
+bool musicBasBusy(void) {
+ return musicMmlBusy(musicMmlBasic(false));
+}
+
+
+// The program ended (its musics are being freed): forget their numbers.
+void musicBasForget(int32_t appId) {
+ for (int32_t i = 0; i < BAS_SLOTS; i++) {
+ if (sSlots[i].appId == appId) {
+ sSlots[i].music = NULL;
+ }
+ }
+}
+
+
+int32_t musicBasPlay(const char *macro) {
+ return musicMmlPlay(musicMmlBasic(true), macro);
+}
+
+
+int32_t musicBasPlayCount(void) {
+ return (int32_t)musicMmlQueued(musicMmlBasic(false));
+}
+
+
+int32_t musicBasSound(double hz, double ticks) {
+ MusicMmlT *m = musicMmlBasic(true);
+
+ if (ticks <= 0.0) {
+ musicMmlStop(m);
+ return MUSIC_OK;
+ }
+
+ // Both are positive here: + 0.5 rounds.
+ uint32_t milliHz = hz > AUDIO_TONE_MAX_HZ ? 0 : (uint32_t)(hz * AUDIO_MILLIHZ_PER_HZ + ROUND_HALF);
+
+ return musicMmlTone(m, milliHz, (uint32_t)(ticks * QB_TICK_US + ROUND_HALF));
+}
+
+
+void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value) {
+ sLastError = musicMidiControl(basGet(handle), channel - BAS_FIRST_CHANNEL, controller, value);
+}
+
+
+const char *MusicError(void) {
+ return musicErrorText(sLastError);
+}
+
+
+void MusicFree(int32_t handle) {
+ MusicT *m = basGet(handle);
+
+ if (m) {
+ musicFree(m);
+ sSlots[handle - 1].music = NULL;
+ }
+}
+
+
+int32_t MusicLength(int32_t handle) {
+ MusicInfoT info;
+
+ musicGetInfo(basGet(handle), &info);
+ return (int32_t)info.lengthMs;
+}
+
+
+int32_t MusicLoad(const char *fileName) {
+ MusicT *m = NULL;
+ int32_t rc = musicLoad(fileName, &m);
+
+ return basAdd(m, rc);
+}
+
+
+int32_t MusicMidiOpen(void) {
+ MusicT *m = NULL;
+ int32_t rc = musicMidiOpen(&m);
+
+ return basAdd(m, rc);
+}
+
+
+void MusicNoteOff(int32_t handle, int32_t channel, int32_t note) {
+ sLastError = musicMidiNoteOff(basGet(handle), channel - BAS_FIRST_CHANNEL, note);
+}
+
+
+void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity) {
+ sLastError = musicMidiNoteOn(basGet(handle), channel - BAS_FIRST_CHANNEL, note, velocity);
+}
+
+
+void MusicPause(int32_t handle) {
+ sLastError = musicPause(basGet(handle));
+}
+
+
+void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend) {
+ sLastError = musicMidiPitchBend(basGet(handle), channel - BAS_FIRST_CHANNEL, bend);
+}
+
+
+void MusicPlay(int32_t handle, int32_t loopIt) {
+ sLastError = musicPlay(basGet(handle), loopIt != BAS_FALSE);
+}
+
+
+int32_t MusicPlaying(int32_t handle) {
+ return musicIsPlaying(basGet(handle)) ? BAS_TRUE : BAS_FALSE;
+}
+
+
+int32_t MusicPosition(int32_t handle) {
+ return (int32_t)musicGetPosition(basGet(handle));
+}
+
+
+void MusicProgram(int32_t handle, int32_t channel, int32_t program) {
+ sLastError = musicMidiProgram(basGet(handle), channel - BAS_FIRST_CHANNEL, program);
+}
+
+
+void MusicResume(int32_t handle) {
+ sLastError = musicResume(basGet(handle));
+}
+
+
+void MusicSeek(int32_t handle, int32_t milliseconds) {
+ sLastError = milliseconds < 0 ? MUSIC_ERR_ARG : musicSetPosition(basGet(handle), (uint32_t)milliseconds);
+}
+
+
+void MusicStop(int32_t handle) {
+ sLastError = musicStop(basGet(handle));
+}
+
+
+void MusicVolume(int32_t handle, int32_t volume) {
+ int32_t percent = volume < 0 ? 0 : (volume > PERCENT_MAX ? PERCENT_MAX : volume);
+
+ sLastError = musicSetVolume(basGet(handle), percent * MUSIC_VOLUME_MAX / PERCENT_MAX);
+}
diff --git a/src/libs/kpunch/music/musicFm.c b/src/libs/kpunch/music/musicFm.c
new file mode 100644
index 00000000..827f9e42
--- /dev/null
+++ b/src/libs/kpunch/music/musicFm.c
@@ -0,0 +1,669 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicFm.c -- General MIDI on two-operator FM voices
+//
+// An FmSynthT plays MIDI with the OP2 bank on voices from a backend: the
+// FM chip (audio.lib's shared voices; fmHwBackend) or the software
+// synthesizer (musicSynth.c). Runs at interrupt time as well (the timer
+// plays MIDI on the FM chip), see musicInt.h.
+//
+// A note takes one voice, or two for an instrument with OP2_FLAG_DOUBLE
+// (the second is skipped when voices are short). Channel 10 (9) plays
+// the bank's percussion instruments, one per note (35..81), at their
+// fixed note. Pitch is worked in 1/32 semitones: the note (or the fixed
+// note) plus the voice's note offset, plus an octave (OP2 banks are made
+// for DMX, whose note numbers sound an octave above MIDI's, so their
+// offsets are mostly -12), the pitch bend times the bend range
+// (RPN 0, default 2 semitones), and on the second voice the detune
+// (fine tune / 2 - 64). Loudness: velocity x volume (CC7) x expression
+// (CC11) x the music's gain, through a 40 log10 curve, added as
+// attenuation to the carrier's level (and to the modulator's when the
+// voice is additive). Pan (CC10) is left / both / right on an OPL3 and
+// the software synthesizer. Sustain (CC64) holds released notes. A note
+// that gets no voice goes to the overflow sink when there is one, else
+// takes over the oldest note's voices.
+
+#include "musicInt.h"
+
+#define FM_FNUM_BASE_NOTE 66 // sFnum[0]: F#4 at block 4
+#define FM_FNUM_BASE_BLOCK 4
+#define FM_OCTAVE_STEPS (12 * FM_PITCH_STEPS)
+#define FM_FNUM_BITS 10
+#define FM_LEVEL_MASK 0x3F
+#define FM_KSL_MASK 0xC0
+#define FM_WAVE_MASK 0x03 // OPL2 waveforms (the bank is made for OPL2)
+#define FM_FEEDBACK_MASK 0x0F // feedback and additive bits
+#define FM_EXPRESSION_MAX 127
+#define FM_BEND_RANGE_MAX 24
+#define FM_DETUNE_CENTER 64
+#define FM_GAIN_SHIFT 8
+
+// sFnum[i]: frequency number of note FM_FNUM_BASE_NOTE + i / 32 at block
+// FM_FNUM_BASE_BLOCK: round(369.99 Hz * 2^(i / 384) * 2^16 / 49716)
+static const uint16_t sFnum[FM_OCTAVE_STEPS] = {
+ 488, 489, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 498, 499, 500, 501,
+ 502, 503, 504, 505, 506, 507, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516,
+ 517, 518, 519, 520, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531,
+ 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 545, 546,
+ 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562,
+ 563, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579,
+ 580, 581, 582, 583, 584, 585, 586, 587, 588, 590, 591, 592, 593, 594, 595, 596,
+ 597, 598, 599, 600, 601, 602, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613,
+ 615, 616, 617, 618, 619, 620, 621, 622, 623, 625, 626, 627, 628, 629, 630, 631,
+ 633, 634, 635, 636, 637, 638, 639, 641, 642, 643, 644, 645, 646, 648, 649, 650,
+ 651, 652, 653, 655, 656, 657, 658, 659, 661, 662, 663, 664, 665, 666, 668, 669,
+ 670, 671, 673, 674, 675, 676, 677, 679, 680, 681, 682, 684, 685, 686, 687, 689,
+ 690, 691, 692, 693, 695, 696, 697, 699, 700, 701, 702, 704, 705, 706, 707, 709,
+ 710, 711, 713, 714, 715, 716, 718, 719, 720, 722, 723, 724, 726, 727, 728, 729,
+ 731, 732, 733, 735, 736, 737, 739, 740, 741, 743, 744, 745, 747, 748, 749, 751,
+ 752, 754, 755, 756, 758, 759, 760, 762, 763, 765, 766, 767, 769, 770, 771, 773,
+ 774, 776, 777, 778, 780, 781, 783, 784, 785, 787, 788, 790, 791, 793, 794, 795,
+ 797, 798, 800, 801, 803, 804, 806, 807, 808, 810, 811, 813, 814, 816, 817, 819,
+ 820, 822, 823, 825, 826, 828, 829, 831, 832, 834, 835, 837, 838, 840, 841, 843,
+ 844, 846, 847, 849, 850, 852, 853, 855, 857, 858, 860, 861, 863, 864, 866, 867,
+ 869, 871, 872, 874, 875, 877, 878, 880, 882, 883, 885, 886, 888, 890, 891, 893,
+ 894, 896, 898, 899, 901, 903, 904, 906, 908, 909, 911, 912, 914, 916, 917, 919,
+ 921, 922, 924, 926, 927, 929, 931, 932, 934, 936, 937, 939, 941, 943, 944, 946,
+ 948, 949, 951, 953, 955, 956, 958, 960, 961, 963, 965, 967, 968, 970, 972, 974};
+
+// Attenuation (0.75 dB steps) for a loudness of 0..127: round(-40
+// log10(v / 127) / 0.75), at most 63
+static const uint8_t sVolTl[MUSIC_MIDI_DATA_MAX + 1] = {
+ 63, 63, 63, 63, 63, 63, 63, 63, 63, 61, 59, 57, 55, 53, 51, 49,
+ 48, 47, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 33,
+ 32, 31, 31, 30, 29, 29, 28, 27, 27, 26, 26, 25, 25, 24, 24, 23,
+ 23, 22, 22, 21, 21, 20, 20, 19, 19, 19, 18, 18, 17, 17, 17, 16,
+ 16, 16, 15, 15, 14, 14, 14, 13, 13, 13, 13, 12, 12, 12, 11, 11,
+ 11, 10, 10, 10, 10, 9, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7,
+ 6, 6, 6, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 3, 3,
+ 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 0, 0, 0
+};
+
+// General MIDI System On (a reset)
+static const uint8_t sGmOn[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void channelReset(FmChannelT *c);
+static void controller(FmSynthT *s, uint8_t channel, uint8_t cc, uint8_t value);
+static int32_t fmHwAlloc(void *owner);
+void fmHwBackend(AudioCtxT *ctx, FmBackendT *out);
+static void fmHwRelease(void *owner, int32_t voice);
+static void fmHwWrite(void *owner, int32_t voice, uint8_t reg, uint8_t value);
+int32_t fmLockItems(AudioLockItemT *items);
+static void fmMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2);
+void fmPitchToFreq(int32_t pitch, uint32_t *fnum, uint32_t *block);
+static void fmSilence(MidiSinkT *sink);
+void fmSynthInit(FmSynthT *synth, const FmBackendT *backend, const Op2BankT *bank, MidiSinkT *overflow, bool stereo, int32_t gain);
+void fmSynthSetGain(FmSynthT *synth, int32_t gain);
+static void fmSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len);
+static void forward(FmSynthT *s, uint8_t status, uint8_t data1, uint8_t data2);
+static uint8_t levelFor(const FmSynthT *s, const FmNoteT *n, uint8_t patchLevel);
+static void noteFree(FmSynthT *s, FmNoteT *n);
+static void noteOff(FmSynthT *s, uint8_t channel, uint8_t note);
+static FmNoteT *noteOldest(FmSynthT *s);
+static void noteOn(FmSynthT *s, uint8_t channel, uint8_t note, uint8_t velocity);
+static void notesUpdate(FmSynthT *s, uint8_t channel, bool freq, bool level, bool pan);
+static uint8_t panBits(const FmSynthT *s, uint8_t channel);
+static void voiceFreq(FmSynthT *s, FmNoteT *n, int32_t i, bool keyOn);
+static void voiceLevel(FmSynthT *s, const FmNoteT *n, int32_t i);
+static void voicePatch(FmSynthT *s, const FmNoteT *n, int32_t i);
+
+
+static void channelReset(FmChannelT *c) {
+ c->bend = 0;
+ c->program = 0;
+ c->volume = MIDI_VOLUME_DEFAULT;
+ c->expression = FM_EXPRESSION_MAX;
+ c->pan = MIDI_CENTER;
+ c->bendRange = MIDI_BEND_RANGE_DEFAULT;
+ c->rpnMsb = MIDI_RPN_NULL;
+ c->rpnLsb = MIDI_RPN_NULL;
+ c->sustain = false;
+}
+
+
+static void controller(FmSynthT *s, uint8_t channel, uint8_t cc, uint8_t value) {
+ FmChannelT *c = &s->channels[channel];
+
+ switch (cc) {
+ case MIDI_CC_VOLUME:
+ c->volume = value;
+ notesUpdate(s, channel, false, true, false);
+ break;
+
+ case MIDI_CC_EXPRESSION:
+ c->expression = value;
+ notesUpdate(s, channel, false, true, false);
+ break;
+
+ case MIDI_CC_PAN:
+ c->pan = value;
+ notesUpdate(s, channel, false, false, true);
+ break;
+
+ case MIDI_CC_SUSTAIN:
+ c->sustain = value >= MIDI_CENTER;
+
+ for (int32_t i = 0; i < FM_NOTES_MAX && !c->sustain; i++) {
+ if (s->notes[i].channel == channel && s->notes[i].state == FM_NOTE_HELD) {
+ noteFree(s, &s->notes[i]);
+ }
+ }
+
+ break;
+
+ case MIDI_CC_RPN_MSB:
+ c->rpnMsb = value;
+ break;
+
+ case MIDI_CC_RPN_LSB:
+ c->rpnLsb = value;
+ break;
+
+ case MIDI_CC_NRPN_MSB:
+ case MIDI_CC_NRPN_LSB:
+ c->rpnMsb = MIDI_RPN_NULL;
+ c->rpnLsb = MIDI_RPN_NULL;
+ break;
+
+ case MIDI_CC_DATA:
+ if (c->rpnMsb == 0 && c->rpnLsb == MIDI_RPN_BEND_RANGE) {
+ c->bendRange = value > FM_BEND_RANGE_MAX ? FM_BEND_RANGE_MAX : value;
+ notesUpdate(s, channel, true, false, false);
+ }
+
+ break;
+
+ case MIDI_CC_RESET:
+ c->expression = FM_EXPRESSION_MAX;
+ c->bend = 0;
+ c->rpnMsb = MIDI_RPN_NULL;
+ c->rpnLsb = MIDI_RPN_NULL;
+ controller(s, channel, MIDI_CC_SUSTAIN, 0);
+ notesUpdate(s, channel, true, true, false);
+ break;
+
+ case MIDI_CC_SOUND_OFF:
+ case MIDI_CC_NOTES_OFF:
+ for (int32_t i = 0; i < FM_NOTES_MAX; i++) {
+ if (s->notes[i].channel == channel && s->notes[i].state != FM_NOTE_FREE) {
+ noteFree(s, &s->notes[i]);
+ }
+ }
+
+ break;
+
+ default:
+ break;
+ }
+}
+
+
+static int32_t fmHwAlloc(void *owner) {
+ return audioFmAlloc((AudioCtxT *)owner);
+}
+
+
+// The FM chip as a backend: audio.lib's shared voices, for ctx.
+void fmHwBackend(AudioCtxT *ctx, FmBackendT *out) {
+ out->alloc = fmHwAlloc;
+ out->release = fmHwRelease;
+ out->write = fmHwWrite;
+ out->owner = ctx;
+}
+
+
+static void fmHwRelease(void *owner, int32_t voice) {
+ audioFmFree((AudioCtxT *)owner, voice);
+}
+
+
+static void fmHwWrite(void *owner, int32_t voice, uint8_t reg, uint8_t value) {
+ audioFmWrite((AudioCtxT *)owner, voice, reg, value);
+}
+
+
+// The functions of this file for musicLock (every one: any may run at
+// interrupt time). Returns how many were stored in items.
+int32_t fmLockItems(AudioLockItemT *items) {
+ const void *fns[] = {
+ (const void *)channelReset,
+ (const void *)controller,
+ (const void *)fmHwAlloc,
+ (const void *)fmHwBackend,
+ (const void *)fmHwRelease,
+ (const void *)fmHwWrite,
+ (const void *)fmLockItems,
+ (const void *)fmMessage,
+ (const void *)fmPitchToFreq,
+ (const void *)fmSilence,
+ (const void *)fmSysex,
+ (const void *)fmSynthInit,
+ (const void *)fmSynthSetGain,
+ (const void *)forward,
+ (const void *)levelFor,
+ (const void *)noteFree,
+ (const void *)noteOff,
+ (const void *)noteOldest,
+ (const void *)noteOn,
+ (const void *)notesUpdate,
+ (const void *)panBits,
+ (const void *)voiceFreq,
+ (const void *)voiceLevel,
+ (const void *)voicePatch
+ };
+ int32_t count = (int32_t)(sizeof(fns) / sizeof(fns[0]));
+
+ for (int32_t i = 0; i < count; i++) {
+ items[i].addr = fns[i];
+ items[i].size = 0;
+ }
+
+ return count;
+}
+
+
+// Note messages act here (and go to the overflow sink only for its own
+// notes); everything else acts here and goes on to it too.
+static void fmMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2) {
+ FmSynthT *s = (FmSynthT *)sink;
+ uint8_t channel = status & MIDI_CHANNEL_MASK;
+
+ data1 &= MIDI_DATA_MASK;
+ data2 &= MIDI_DATA_MASK;
+
+ switch (status & MIDI_TYPE_MASK) {
+ case MIDI_NOTE_ON:
+ if (data2 > 0) {
+ noteOn(s, channel, data1, data2);
+ } else {
+ noteOff(s, channel, data1);
+ }
+
+ return;
+
+ case MIDI_NOTE_OFF:
+ noteOff(s, channel, data1);
+ return;
+
+ case MIDI_CONTROL:
+ controller(s, channel, data1, data2);
+ break;
+
+ case MIDI_PROGRAM:
+ s->channels[channel].program = data1;
+ break;
+
+ case MIDI_PITCH_BEND:
+ s->channels[channel].bend = (int16_t)((((int32_t)data2 << MIDI_DATA_BITS) | data1) - MIDI_BEND_CENTER);
+ notesUpdate(s, channel, true, false, false);
+ break;
+
+ default:
+ break;
+ }
+
+ forward(s, status, data1, data2);
+}
+
+
+// Pitch (1/32 semitones from MIDI note 0) to frequency number and block:
+// the octave above the table's base note shifts the block; outside the
+// chip's blocks the frequency number is halved or doubled instead.
+void fmPitchToFreq(int32_t pitch, uint32_t *fnum, uint32_t *block) {
+ int32_t q = pitch - FM_FNUM_BASE_NOTE * FM_PITCH_STEPS;
+ int32_t octave = q >= 0 ? q / FM_OCTAVE_STEPS : -((-q + FM_OCTAVE_STEPS - 1) / FM_OCTAVE_STEPS);
+ int32_t b = FM_FNUM_BASE_BLOCK + octave;
+ int32_t f = sFnum[q - octave * FM_OCTAVE_STEPS];
+
+ if (b < 0) {
+ f = -b >= FM_FNUM_BITS ? 0 : f >> -b;
+ b = 0;
+ } else if (b > AUDIO_FM_BLOCK_MAX) {
+ f = b - AUDIO_FM_BLOCK_MAX >= FM_FNUM_BITS ? AUDIO_FM_FNUM_MAX : f << (b - AUDIO_FM_BLOCK_MAX);
+ b = AUDIO_FM_BLOCK_MAX;
+ }
+
+ *fnum = f > AUDIO_FM_FNUM_MAX ? AUDIO_FM_FNUM_MAX : (uint32_t)f;
+ *block = (uint32_t)b;
+}
+
+
+static void fmSilence(MidiSinkT *sink) {
+ FmSynthT *s = (FmSynthT *)sink;
+
+ for (int32_t i = 0; i < FM_NOTES_MAX; i++) {
+ if (s->notes[i].state != FM_NOTE_FREE) {
+ noteFree(s, &s->notes[i]);
+ }
+ }
+
+ if (s->overflow) {
+ s->overflow->silence(s->overflow);
+ }
+}
+
+
+void fmSynthInit(FmSynthT *synth, const FmBackendT *backend, const Op2BankT *bank, MidiSinkT *overflow, bool stereo, int32_t gain) {
+ synth->sink.message = fmMessage;
+ synth->sink.sysex = fmSysex;
+ synth->sink.silence = fmSilence;
+ synth->backend = *backend;
+ synth->overflow = overflow;
+ synth->bank = bank;
+ synth->clock = 0;
+ synth->gain = gain;
+ synth->stereo = stereo;
+
+ for (int32_t c = 0; c < MUSIC_MIDI_CHANNELS; c++) {
+ channelReset(&synth->channels[c]);
+ }
+
+ for (int32_t i = 0; i < FM_NOTES_MAX; i++) {
+ synth->notes[i].state = FM_NOTE_FREE;
+ }
+}
+
+
+void fmSynthSetGain(FmSynthT *synth, int32_t gain) {
+ synth->gain = gain;
+
+ for (uint8_t c = 0; c < MUSIC_MIDI_CHANNELS; c++) {
+ notesUpdate(synth, c, false, true, false);
+ }
+}
+
+
+// General MIDI System On resets every channel.
+static void fmSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len) {
+ FmSynthT *s = (FmSynthT *)sink;
+
+ if (len != sizeof(sGmOn)) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < len; i++) {
+ if (data[i] != sGmOn[i]) {
+ return;
+ }
+ }
+
+ fmSilence(sink);
+
+ for (int32_t c = 0; c < MUSIC_MIDI_CHANNELS; c++) {
+ channelReset(&s->channels[c]);
+ }
+}
+
+
+static void forward(FmSynthT *s, uint8_t status, uint8_t data1, uint8_t data2) {
+ if (s->overflow) {
+ s->overflow->message(s->overflow, status, data1, data2);
+ }
+}
+
+
+// The patch's level byte with the note's loudness added as attenuation.
+static uint8_t levelFor(const FmSynthT *s, const FmNoteT *n, uint8_t patchLevel) {
+ const FmChannelT *c = &s->channels[n->channel];
+ int32_t loud = (int32_t)n->velocity * c->volume / MUSIC_MIDI_DATA_MAX * c->expression / MUSIC_MIDI_DATA_MAX;
+ int32_t att;
+
+ loud = (loud * s->gain) >> FM_GAIN_SHIFT;
+ att = (patchLevel & FM_LEVEL_MASK) + sVolTl[loud > MUSIC_MIDI_DATA_MAX ? MUSIC_MIDI_DATA_MAX : loud];
+
+ return (uint8_t)((patchLevel & FM_KSL_MASK) | (att > FM_LEVEL_MASK ? FM_LEVEL_MASK : att));
+}
+
+
+// Key off and give the voices back (or tell the overflow sink).
+static void noteFree(FmSynthT *s, FmNoteT *n) {
+ if (n->state == FM_NOTE_OVERFLOW && s->overflow) {
+ s->overflow->message(s->overflow, (uint8_t)(MIDI_NOTE_OFF | n->channel), n->note, 0);
+ }
+
+ for (int32_t v = 0; v < OP2_VOICES; v++) {
+ if (n->voice[v]) {
+ s->backend.release(s->backend.owner, n->voice[v]);
+ n->voice[v] = 0;
+ }
+ }
+
+ n->state = FM_NOTE_FREE;
+}
+
+
+static void noteOff(FmSynthT *s, uint8_t channel, uint8_t note) {
+ bool found = false;
+
+ for (int32_t i = 0; i < FM_NOTES_MAX; i++) {
+ FmNoteT *n = &s->notes[i];
+
+ if (n->channel != channel || n->note != note || (n->state != FM_NOTE_ON && n->state != FM_NOTE_OVERFLOW)) {
+ continue;
+ }
+
+ found = true;
+
+ if (n->state == FM_NOTE_ON && s->channels[channel].sustain) {
+ n->state = FM_NOTE_HELD;
+ } else {
+ noteFree(s, n);
+ }
+ }
+
+ if (!found) {
+ forward(s, (uint8_t)(MIDI_NOTE_OFF | channel), note, 0);
+ }
+}
+
+
+// The oldest note playing on voices (NULL when none).
+static FmNoteT *noteOldest(FmSynthT *s) {
+ FmNoteT *old = NULL;
+
+ for (int32_t i = 0; i < FM_NOTES_MAX; i++) {
+ FmNoteT *n = &s->notes[i];
+
+ if ((n->state == FM_NOTE_ON || n->state == FM_NOTE_HELD) && (!old || n->age < old->age)) {
+ old = n;
+ }
+ }
+
+ return old;
+}
+
+
+static void noteOn(FmSynthT *s, uint8_t channel, uint8_t note, uint8_t velocity) {
+ int32_t instrument;
+
+ if (channel == MIDI_PERCUSSION) {
+ if (note < OP2_PERC_FIRST || note > OP2_PERC_LAST) {
+ return;
+ }
+
+ instrument = OP2_MELODIC + note - OP2_PERC_FIRST;
+ } else {
+ instrument = s->channels[channel].program;
+ }
+
+ // The same note again: the old one ends first.
+ for (int32_t i = 0; i < FM_NOTES_MAX; i++) {
+ FmNoteT *n = &s->notes[i];
+
+ if (n->state != FM_NOTE_FREE && n->channel == channel && n->note == note) {
+ noteFree(s, n);
+ }
+ }
+
+ FmNoteT *n = NULL;
+
+ for (int32_t i = 0; i < FM_NOTES_MAX && !n; i++) {
+ if (s->notes[i].state == FM_NOTE_FREE) {
+ n = &s->notes[i];
+ }
+ }
+
+ if (!n) {
+ n = noteOldest(s);
+ noteFree(s, n);
+ }
+
+ const Op2InstrumentT *instr = &s->bank->instruments[instrument];
+
+ n->channel = channel;
+ n->note = note;
+ n->velocity = velocity;
+ n->instrument = (uint8_t)instrument;
+ n->age = ++s->clock;
+ n->voice[0] = s->backend.alloc(s->backend.owner);
+ n->voice[1] = 0;
+
+ if (n->voice[0] <= 0 && !s->overflow) {
+ FmNoteT *old = noteOldest(s);
+
+ if (old && old != n) {
+ noteFree(s, old);
+ n->voice[0] = s->backend.alloc(s->backend.owner);
+ }
+ }
+
+ if (n->voice[0] <= 0) {
+ n->voice[0] = 0;
+
+ if (s->overflow) {
+ n->state = FM_NOTE_OVERFLOW;
+ s->overflow->message(s->overflow, (uint8_t)(MIDI_NOTE_ON | channel), note, velocity);
+ }
+
+ return;
+ }
+
+ if (instr->flags & OP2_FLAG_DOUBLE) {
+ int32_t second = s->backend.alloc(s->backend.owner);
+
+ n->voice[1] = second > 0 ? second : 0;
+ }
+
+ n->state = FM_NOTE_ON;
+
+ for (int32_t v = 0; v < OP2_VOICES; v++) {
+ if (n->voice[v]) {
+ voicePatch(s, n, v);
+ voiceLevel(s, n, v);
+ voiceFreq(s, n, v, true);
+ }
+ }
+}
+
+
+// Bring the sounding notes of a channel up to date after it changed.
+static void notesUpdate(FmSynthT *s, uint8_t channel, bool freq, bool level, bool pan) {
+ for (int32_t i = 0; i < FM_NOTES_MAX; i++) {
+ FmNoteT *n = &s->notes[i];
+
+ if (n->channel != channel || (n->state != FM_NOTE_ON && n->state != FM_NOTE_HELD)) {
+ continue;
+ }
+
+ for (int32_t v = 0; v < OP2_VOICES; v++) {
+ if (!n->voice[v]) {
+ continue;
+ }
+
+ if (freq) {
+ voiceFreq(s, n, v, true);
+ }
+
+ if (level) {
+ voiceLevel(s, n, v);
+ }
+
+ if (pan) {
+ const Op2VoiceT *pv = &s->bank->instruments[n->instrument].voice[v];
+
+ s->backend.write(s->backend.owner, n->voice[v], AUDIO_FM_FEEDBACK, (uint8_t)((pv->feedback & FM_FEEDBACK_MASK) | panBits(s, channel)));
+ }
+ }
+ }
+}
+
+
+static uint8_t panBits(const FmSynthT *s, uint8_t channel) {
+ uint8_t pan = s->channels[channel].pan;
+
+ if (!s->stereo || (pan >= FM_PAN_LEFT_MAX && pan < FM_PAN_RIGHT_MIN)) {
+ return AUDIO_FM_OUT_BOTH;
+ }
+
+ return pan < FM_PAN_LEFT_MAX ? AUDIO_FM_OUT_LEFT : AUDIO_FM_OUT_RIGHT;
+}
+
+
+static void voiceFreq(FmSynthT *s, FmNoteT *n, int32_t i, bool keyOn) {
+ const Op2InstrumentT *instr = &s->bank->instruments[n->instrument];
+ const FmChannelT *c = &s->channels[n->channel];
+ int32_t note = (instr->flags & OP2_FLAG_FIXED) ? instr->fixedNote : n->note;
+ int32_t pitch = (note + instr->voice[i].noteOffset + OP2_NOTE_SHIFT) * FM_PITCH_STEPS;
+ uint32_t fnum;
+ uint32_t block;
+
+ pitch += (int32_t)c->bend * c->bendRange * FM_PITCH_STEPS / MIDI_BEND_CENTER;
+
+ if (i == 1) {
+ pitch += instr->fineTune / 2 - FM_DETUNE_CENTER;
+ }
+
+ fmPitchToFreq(pitch, &fnum, &block);
+ n->keyBlock[i] = (uint8_t)((keyOn ? AUDIO_FM_KEY_ON : 0) | (block << AUDIO_FM_BLOCK_SHIFT) | (fnum >> AUDIO_FM_FNUM_HI_SHIFT));
+ s->backend.write(s->backend.owner, n->voice[i], AUDIO_FM_FNUM_LOW, (uint8_t)fnum);
+ s->backend.write(s->backend.owner, n->voice[i], AUDIO_FM_KEY_BLOCK, n->keyBlock[i]);
+}
+
+
+static void voiceLevel(FmSynthT *s, const FmNoteT *n, int32_t i) {
+ const Op2VoiceT *pv = &s->bank->instruments[n->instrument].voice[i];
+
+ s->backend.write(s->backend.owner, n->voice[i], AUDIO_FM_CAR_LEVEL, levelFor(s, n, pv->carLevel));
+ s->backend.write(s->backend.owner, n->voice[i], AUDIO_FM_MOD_LEVEL, (pv->feedback & AUDIO_FM_ADDITIVE) ? levelFor(s, n, pv->modLevel) : pv->modLevel);
+}
+
+
+static void voicePatch(FmSynthT *s, const FmNoteT *n, int32_t i) {
+ const Op2VoiceT *pv = &s->bank->instruments[n->instrument].voice[i];
+ void *owner = s->backend.owner;
+ int32_t voice = n->voice[i];
+
+ s->backend.write(owner, voice, AUDIO_FM_MOD_CHAR, pv->modChar);
+ s->backend.write(owner, voice, AUDIO_FM_CAR_CHAR, pv->carChar);
+ s->backend.write(owner, voice, AUDIO_FM_MOD_ATTACK, pv->modAttack);
+ s->backend.write(owner, voice, AUDIO_FM_CAR_ATTACK, pv->carAttack);
+ s->backend.write(owner, voice, AUDIO_FM_MOD_SUSTAIN, pv->modSustain);
+ s->backend.write(owner, voice, AUDIO_FM_CAR_SUSTAIN, pv->carSustain);
+ s->backend.write(owner, voice, AUDIO_FM_MOD_WAVE, pv->modWave & FM_WAVE_MASK);
+ s->backend.write(owner, voice, AUDIO_FM_CAR_WAVE, pv->carWave & FM_WAVE_MASK);
+ s->backend.write(owner, voice, AUDIO_FM_FEEDBACK, (uint8_t)((pv->feedback & FM_FEEDBACK_MASK) | panBits(s, n->channel)));
+}
diff --git a/src/libs/kpunch/music/musicInt.h b/src/libs/kpunch/music/musicInt.h
new file mode 100644
index 00000000..d8c0d5a3
--- /dev/null
+++ b/src/libs/kpunch/music/musicInt.h
@@ -0,0 +1,535 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicInt.h -- private to music.lib (not in the SDK)
+//
+// The parts of the library and how they connect:
+//
+// musicSmf.c Standard MIDI File -> one list of events, times in
+// microseconds (MidiFileT)
+// musicSeq.c the sequencer (SeqT: sends the events that are due to a
+// MidiSinkT), the event ring that carries MIDI from the
+// timer interrupt to the software synthesizer, and the
+// timer callback that plays MIDI on the synthesizers
+// musicFm.c General MIDI on two-operator FM voices (FmSynthT): the
+// channels' state, notes, the OP2 bank's patches, pitch
+// and loudness, on the FM chip or the software synthesizer
+// musicMpu.c MIDI out of the MPU-401 port (MpuSinkT): General MIDI
+// as it is, or remapped for a Roland MT-32, paced
+// musicSynth.c the software FM synthesizer (SynthChipT): an integer
+// two-operator FM chip rendering into a sound stream
+// musicOp2.c the OP2 instrument bank
+// musicXmp.c tracker modules through libxmp-lite
+// music.c musics, programs, streams, the public functions
+//
+// musicSeq.c, musicFm.c and musicMpu.c also run at interrupt time (the
+// timer service plays MIDI on the FM chip and the MPU-401 port), so they
+// keep to the interrupt rules (audioDrv.h): integer arithmetic, no calls
+// out of those files but audio.lib's interrupt-safe FM and MIDI
+// functions, everything pinned (the Makefile links them last, so one
+// span from their first function to the end of the library's data covers
+// them; musicLock).
+#ifndef MUSIC_INT_H
+#define MUSIC_INT_H
+
+#include "music.h"
+#include "audioDrv.h"
+
+// ============================================================
+// MIDI
+// ============================================================
+
+#define MIDI_NOTE_OFF 0x80
+#define MIDI_NOTE_ON 0x90
+#define MIDI_POLY_PRESSURE 0xA0
+#define MIDI_CONTROL 0xB0
+#define MIDI_PROGRAM 0xC0
+#define MIDI_CHAN_PRESSURE 0xD0
+#define MIDI_PITCH_BEND 0xE0
+#define MIDI_SYSEX 0xF0
+#define MIDI_SYSEX_END 0xF7
+#define MIDI_META 0xFF
+#define MIDI_STATUS_BIT 0x80
+#define MIDI_TYPE_MASK 0xF0
+#define MIDI_CHANNEL_MASK 0x0F
+#define MIDI_DATA_MASK 0x7F
+#define MIDI_PERCUSSION 9
+#define MIDI_BEND_CENTER 8192
+#define MIDI_DATA_BITS 7
+#define MIDI_CENTER 64
+
+// Controllers
+#define MIDI_CC_BANK 0
+#define MIDI_CC_MODULATION 1
+#define MIDI_CC_DATA 6
+#define MIDI_CC_VOLUME 7
+#define MIDI_CC_PAN 10
+#define MIDI_CC_EXPRESSION 11
+#define MIDI_CC_BANK_LSB 32
+#define MIDI_CC_DATA_LSB 38
+#define MIDI_CC_SUSTAIN 64
+#define MIDI_CC_REVERB 91
+#define MIDI_CC_CHORUS 93
+#define MIDI_CC_NRPN_LSB 98
+#define MIDI_CC_NRPN_MSB 99
+#define MIDI_CC_RPN_LSB 100
+#define MIDI_CC_RPN_MSB 101
+#define MIDI_CC_SOUND_OFF 120
+#define MIDI_CC_RESET 121
+#define MIDI_CC_NOTES_OFF 123
+#define MIDI_RPN_BEND_RANGE 0
+#define MIDI_RPN_NULL 127
+
+// Controller defaults (General MIDI)
+#define MIDI_VOLUME_DEFAULT 100
+#define MIDI_BEND_RANGE_DEFAULT 2
+
+// ============================================================
+// MIDI files (musicSmf.c)
+// ============================================================
+
+// One event of a file, at its time from the start. Channel messages
+// keep their status byte (with the channel); a system exclusive message
+// has MIDI_SYSEX and its bytes (from F0 or the first byte after an F7
+// escape, through the end) in MidiFileT.sysex.
+typedef struct {
+ uint32_t time; // microseconds from the start
+ uint32_t sysexAt; // MIDI_SYSEX: offset in MidiFileT.sysex
+ uint16_t sysexLen;
+ uint8_t status;
+ uint8_t data1;
+ uint8_t data2;
+} MidiEventT;
+
+typedef struct {
+ MidiEventT *events; // in time order (a type 1 file's tracks merged)
+ uint8_t *sysex;
+ uint32_t count;
+ uint32_t sysexBytes;
+ uint32_t length; // microseconds: the last event or end of track
+} MidiFileT;
+
+// Most events and system exclusive bytes a file may have
+#define MIDI_EVENTS_MAX (1024U * 1024U)
+#define MIDI_SYSEX_LEN_MAX 0xFFFF
+
+// midiIsFile: data starts like a MIDI file (SMF, or RIFF RMID around
+// one). midiParse: MUSIC_OK, MUSIC_ERR_FORMAT (not a MIDI file, or type
+// 2), _CORRUPT (damaged beyond use: no header or no track), _NO_MEMORY;
+// a track that breaks off plays up to where it breaks.
+void midiFileFree(MidiFileT *file);
+bool midiIsFile(const uint8_t *data, uint32_t size);
+int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out);
+
+// ============================================================
+// Where MIDI goes (musicSeq.c, musicFm.c, musicMpu.c)
+// ============================================================
+
+// Anything that plays MIDI: the FM synthesizer, the MPU-401 port, the
+// event ring. Implementations have a MidiSinkT as their first member.
+// silence ends every sounding note at once (stop, pause, seeking).
+typedef struct MidiSinkT MidiSinkT;
+
+struct MidiSinkT {
+ void (*message)(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2);
+ void (*sysex)(MidiSinkT *sink, const uint8_t *data, uint32_t len);
+ void (*silence)(MidiSinkT *sink);
+};
+
+// The sequencer: plays a MidiFileT to a sink. now is the song position
+// in microseconds; next the first event not sent yet.
+typedef struct {
+ const MidiEventT *events;
+ const uint8_t *sysex;
+ uint32_t count;
+ uint32_t length;
+ uint32_t next;
+ uint32_t now;
+ bool loop;
+} SeqT;
+
+// Move the song on by us microseconds, sending every event that became
+// due. At the end a looping song starts over; returns false once a song
+// that does not loop has finished.
+bool seqAdvance(SeqT *seq, uint32_t us, MidiSinkT *sink);
+// Go to us: sink is silenced and gets every event before us except
+// notes (instruments and controllers as they would be there).
+void seqSeek(SeqT *seq, uint32_t us, MidiSinkT *sink);
+
+// The event ring: MIDI from the timer interrupt (notes the FM chip had no
+// voice for, and every channel message) to the software synthesizer at
+// program time. Single producer (interrupt) at head, single consumer at
+// tail; a full ring drops the message.
+#define EVENT_RING_SIZE 512 // power of two
+#define EVENT_SILENCE 0 // an entry that stands for silence()
+
+typedef struct {
+ MidiSinkT sink;
+ uint32_t events[EVENT_RING_SIZE]; // status | data1 << 8 | data2 << 16
+ volatile uint32_t head;
+ volatile uint32_t tail;
+ uint32_t dropped;
+} EventRingT;
+
+void eventRingInit(EventRingT *ring);
+// Deliver what the ring holds to to (at program time).
+void eventRingDrain(EventRingT *ring, MidiSinkT *to);
+
+// ============================================================
+// The OP2 instrument bank (musicOp2.c)
+// ============================================================
+
+#define OP2_INSTRUMENTS 175 // 128 melodic, then percussion notes 35..81
+#define OP2_MELODIC 128
+#define OP2_PERC_FIRST 35
+#define OP2_PERC_LAST 81
+#define OP2_VOICES 2 // voices per instrument (the second one when OP2_FLAG_DOUBLE)
+#define OP2_FLAG_FIXED 0x0001 // always the fixed note
+#define OP2_FLAG_DOUBLE 0x0004 // both voices
+#define OP2_FINE_TUNE_CENTER 128
+#define OP2_NOTE_SHIFT 12 // OP2 banks are tuned for DMX, which plays note n where MIDI has n + 12
+
+// One voice: register values for AUDIO_FM_* (the level registers without
+// the loudness of the note; waveforms 0-3) and a note offset.
+typedef struct {
+ int16_t noteOffset;
+ uint8_t modChar;
+ uint8_t modAttack;
+ uint8_t modSustain;
+ uint8_t modWave;
+ uint8_t modLevel; // key scale level | attenuation
+ uint8_t feedback;
+ uint8_t carChar;
+ uint8_t carAttack;
+ uint8_t carSustain;
+ uint8_t carWave;
+ uint8_t carLevel;
+} Op2VoiceT;
+
+typedef struct {
+ Op2VoiceT voice[OP2_VOICES];
+ uint16_t flags;
+ uint8_t fineTune; // second voice detune, OP2_FINE_TUNE_CENTER = none
+ uint8_t fixedNote;
+} Op2InstrumentT;
+
+typedef struct {
+ Op2InstrumentT instruments[OP2_INSTRUMENTS];
+} Op2BankT;
+
+// MUSIC_OK or MUSIC_ERR_BANK (not an OP2 bank, too short).
+int32_t op2Parse(const uint8_t *data, uint32_t size, Op2BankT *bank);
+
+// ============================================================
+// General MIDI on FM voices (musicFm.c)
+// ============================================================
+
+#define FM_NOTES_MAX 32 // notes sounding at once per synthesizer
+#define FM_PAN_LEFT_MAX 42 // CC10 below this: left only (OPL3) ...
+#define FM_PAN_RIGHT_MIN 86 // ... from this: right only
+
+// Voices come from a backend: the FM chip (audio.lib's shared voices) or
+// the software synthesizer. alloc returns a voice (> 0) or an error;
+// release keys it off and gives it back.
+typedef struct {
+ int32_t (*alloc)(void *owner);
+ void (*release)(void *owner, int32_t voice);
+ void (*write)(void *owner, int32_t voice, uint8_t reg, uint8_t value);
+ void *owner;
+} FmBackendT;
+
+typedef struct {
+ int16_t bend; // -8192..8191
+ uint8_t program;
+ uint8_t volume;
+ uint8_t expression;
+ uint8_t pan;
+ uint8_t bendRange; // semitones
+ uint8_t rpnMsb;
+ uint8_t rpnLsb;
+ bool sustain;
+} FmChannelT;
+
+// Note states
+#define FM_NOTE_FREE 0
+#define FM_NOTE_ON 1
+#define FM_NOTE_HELD 2 // released while the sustain pedal is down
+#define FM_NOTE_OVERFLOW 3 // no voice: playing on the overflow sink
+
+typedef struct {
+ int32_t voice[OP2_VOICES]; // backend voices (0: none)
+ uint32_t age;
+ uint8_t keyBlock[OP2_VOICES]; // AUDIO_FM_KEY_BLOCK written (key on)
+ uint8_t state;
+ uint8_t channel;
+ uint8_t note;
+ uint8_t velocity;
+ uint8_t instrument;
+} FmNoteT;
+
+// overflow: where notes go that get no voice (NULL: the oldest note is
+// taken over instead); it also gets every other message, so its
+// channels are in step. gain: the music's volume x the master volume.
+typedef struct {
+ MidiSinkT sink;
+ FmBackendT backend;
+ MidiSinkT *overflow;
+ const Op2BankT *bank;
+ FmChannelT channels[MUSIC_MIDI_CHANNELS];
+ FmNoteT notes[FM_NOTES_MAX];
+ uint32_t clock;
+ int32_t gain;
+ bool stereo;
+} FmSynthT;
+
+// The FM chip as a backend (audio.lib's shared voices, for ctx).
+void fmHwBackend(AudioCtxT *ctx, FmBackendT *out);
+void fmSynthInit(FmSynthT *synth, const FmBackendT *backend, const Op2BankT *bank, MidiSinkT *overflow, bool stereo, int32_t gain);
+void fmSynthSetGain(FmSynthT *synth, int32_t gain);
+
+// Pitch in 1/FM_PITCH_STEPS semitones -> frequency number and block
+#define FM_PITCH_STEPS 32
+void fmPitchToFreq(int32_t pitch, uint32_t *fnum, uint32_t *block);
+
+// ============================================================
+// The MPU-401 port (musicMpu.c)
+// ============================================================
+
+// DVX.INI [audio] mpuMode: what is on the MPU-401 port
+#define MUSIC_INI_MPU_MODE "mpuMode"
+#define MPU_MODE_GM 0 // a General MIDI synthesizer: music as it is
+#define MPU_MODE_MT32 1 // a Roland MT-32: General MIDI music remapped for it
+#define MPU_MODE_RAW 2 // anything: music as it is, no set-up (music made for an MT-32)
+#define MPU_MODE_COUNT 3
+extern const PrefsChoiceT mpuModeChoices[MPU_MODE_COUNT];
+
+#define MPU_QUEUE_BYTES 4096 // power of two
+#define MPU_SYSEX_GAP_US 40000 // after each system exclusive message (early MT-32s need it)
+#define MPU_MT32_PARTS 8 // melodic parts, on MIDI channels 1..8 (0-based)
+#define MPU_MT32_FIRST_PART 1
+#define MPU_PART_NONE 0xFF
+
+typedef struct {
+ MidiSinkT sink;
+ AudioCtxT *ctx;
+ uint8_t queue[MPU_QUEUE_BYTES];
+ volatile uint32_t head;
+ volatile uint32_t tail;
+ uint32_t now; // microseconds, advanced by mpuSinkDrain
+ uint32_t holdUntil; // no byte before this (sysex gap)
+ uint32_t dropped; // bytes that did not fit
+ int32_t mode;
+ int32_t gain;
+ uint8_t volume[MUSIC_MIDI_CHANNELS]; // last CC7 value per channel (unscaled)
+ uint8_t partOf[MUSIC_MIDI_CHANNELS]; // MT-32: output channel of each channel
+ uint8_t rpnMsb[MUSIC_MIDI_CHANNELS];
+ uint8_t rpnLsb[MUSIC_MIDI_CHANNELS];
+ uint8_t nextPart;
+} MpuSinkT;
+
+// GM program -> MT-32 preset timbre
+extern const uint8_t mpuGmToMt32[OP2_MELODIC];
+
+void mpuSinkInit(MpuSinkT *mpu, AudioCtxT *ctx, int32_t mode, int32_t gain);
+// Queue the set-up the mode needs (GM System On; MT-32 bend ranges).
+void mpuSinkStart(MpuSinkT *mpu);
+// Advance the clock by us and send what the port takes.
+void mpuSinkDrain(MpuSinkT *mpu, uint32_t us);
+void mpuSinkSetGain(MpuSinkT *mpu, int32_t gain);
+
+// ============================================================
+// The software FM synthesizer (musicSynth.c)
+// ============================================================
+
+#define SYNTH_VOICES_MAX 24
+#define SYNTH_OPS 2
+
+typedef struct {
+ uint32_t phase;
+ uint32_t phaseStep; // per output frame
+ int32_t env; // attenuation, 16.16 (0 loud .. SYNTH_ENV_MAX)
+ int32_t out; // last two outputs (feedback)
+ int32_t prevOut;
+ uint32_t attackStep; // 16.16 per chip tick
+ uint32_t decayStep;
+ uint32_t releaseStep;
+ int32_t sustainLevel; // attenuation units
+ int32_t levelAtt; // attenuation units: level + key scaling
+ uint8_t egState;
+ uint8_t regChar;
+ uint8_t regLevel;
+ uint8_t regAttack;
+ uint8_t regSustain;
+ uint8_t regWave;
+} SynthOpT;
+
+typedef struct {
+ SynthOpT op[SYNTH_OPS];
+ int32_t id; // 0: free
+ uint32_t freedAt;
+ uint16_t fnum;
+ uint8_t block;
+ uint8_t regFeedback;
+ bool keyOn;
+} SynthVoiceT;
+
+typedef struct {
+ SynthVoiceT voices[SYNTH_VOICES_MAX];
+ uint32_t count;
+ uint32_t rate;
+ uint32_t tickStep; // chip ticks per output frame, 16.16
+ uint32_t tickAcc;
+ uint32_t clock;
+ uint32_t generation;
+ uint32_t tremoloPos; // chip ticks, for the tremolo and vibrato LFOs
+ uint32_t vibratoPos;
+} SynthChipT;
+
+// synthRender mixes frames frames (16-bit, channels 1 or 2) into out
+// (written, not added); synthBackend makes the chip a voice backend.
+int32_t synthAlloc(SynthChipT *chip);
+void synthBackend(SynthChipT *chip, FmBackendT *out);
+void synthInit(SynthChipT *chip, uint32_t rate, uint32_t voices);
+void synthRelease(SynthChipT *chip, int32_t voice);
+void synthRender(SynthChipT *chip, int16_t *out, uint32_t frames, uint32_t channels);
+void synthWrite(SynthChipT *chip, int32_t voice, uint8_t reg, uint8_t value);
+
+// ============================================================
+// Tracker modules (musicXmp.c)
+// ============================================================
+
+typedef struct XmpPlayerT XmpPlayerT;
+
+// xmpOpen: MUSIC_OK, _FORMAT (not a module), _CORRUPT, _NO_MEMORY; the
+// module is ready to render at rate (Hz) in channels (1 or 2) 16-bit
+// frames. xmpRender renders frames frames; false once a song that does
+// not loop has ended (the rest of out is silence). Times in ms.
+void xmpClose(XmpPlayerT *player);
+uint32_t xmpLength(XmpPlayerT *player);
+int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out);
+uint32_t xmpPosition(XmpPlayerT *player);
+bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop);
+void xmpRestart(XmpPlayerT *player);
+void xmpSeek(XmpPlayerT *player, uint32_t ms);
+const char *xmpTitle(XmpPlayerT *player);
+
+// ============================================================
+// Players at interrupt time (musicSeq.c) and the library (music.c)
+// ============================================================
+
+// Player states
+#define PLAY_STOPPED 0
+#define PLAY_PLAYING 1
+#define PLAY_PAUSED 2
+
+// The part of a MIDI music the timer interrupt plays (on the FM chip or
+// the MPU-401 port).
+typedef struct {
+ SeqT seq;
+ MidiSinkT *sink;
+ MpuSinkT *mpu; // drained every tick (NULL: not the MPU-401)
+ volatile int32_t state;
+ volatile bool ended; // played to its end: the end call is due
+} IsrPlayerT;
+
+#define MUSIC_TICK_HZ 500
+#define MUSIC_TICK_US (1000000 / MUSIC_TICK_HZ)
+#define MUSIC_ISR_PLAYERS_MAX 16
+
+// The players the timer callback serves (program side adds and removes
+// them with interrupts off).
+extern IsrPlayerT *musicIsrPlayers[MUSIC_ISR_PLAYERS_MAX];
+
+// The timer callback (platformTimerAdd at MUSIC_TICK_HZ).
+void musicTick(void *unused);
+
+// Pin or release the code and data that run at interrupt time; the
+// ...LockItems functions store their file's functions for it.
+int32_t fmLockItems(AudioLockItemT *items);
+int32_t mpuLockItems(AudioLockItemT *items);
+bool musicLock(bool lock);
+
+// ============================================================
+// Music macros (musicMml.c: the language; musicMmlPlay.c: the queues)
+// ============================================================
+
+// The sounding part of a note, in eighths of its length (MN, ML, MS)
+#define MML_EIGHTHS 8
+#define MML_ON_NORMAL 7
+#define MML_ON_LEGATO 8
+#define MML_ON_STACCATO 6
+
+// Instruments (I): 0 plays square-wave tones, 1..MML_PROGRAMS General MIDI
+// instrument n (program n - 1)
+#define MML_PROGRAM_TONE 0
+#define MML_PROGRAMS 128
+
+// The settings a macro changes, which last from one string to the next.
+typedef struct {
+ int32_t octave;
+ int32_t length; // 1 whole .. 64
+ int32_t tempo; // quarter notes a minute
+ int32_t onEighths;
+ int32_t program;
+ bool background;
+} MmlStateT;
+
+// One note or rest of a macro (or a SOUND tone).
+typedef struct {
+ uint32_t milliHz; // 0: a rest
+ uint32_t us; // its whole length
+ uint32_t soundUs; // the part that sounds
+ int32_t midiNote; // MIDI note number (instruments)
+ int32_t program; // MML_PROGRAM_TONE or the instrument
+} MmlNoteT;
+
+// The defaults (O4 L4 T120 MN MF I0).
+void mmlStateInit(MmlStateT *st);
+
+// Append the notes of macro to *notes (stb_ds array) and update st;
+// MUSIC_ERR_MACRO leaves both as they were.
+int32_t mmlParse(MmlStateT *st, const char *macro, MmlNoteT **notes);
+
+// A type 0 Standard MIDI File playing count notes (malloc'd into *out).
+int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_t *size);
+
+// The queues' end of a program's life, of the idle time and of the
+// library's unloading; the calling BASIC program's queue (made when
+// create and there is none).
+void musicMmlAppEnded(int32_t appId);
+MusicMmlT *musicMmlBasic(bool create);
+void musicMmlIdle(void);
+void musicMmlShutdown(void);
+
+// music.c: the program running now, the library's end of a program's
+// life, the shell's idle time and its own unloading (musicShell.c hooks
+// them up), making appId the current program (returns the one before),
+// the MIDI bank in use (loaded on first need; NULL when it cannot be).
+int32_t musicAppCurrent(void);
+void musicAppEnded(int32_t appId);
+int32_t musicAppEnter(int32_t appId);
+const Op2BankT *musicBank(int32_t *err);
+void musicBasForget(int32_t appId);
+void musicIdle(void *unused);
+void musicShutdown(void);
+
+#endif // MUSIC_INT_H
diff --git a/src/libs/kpunch/music/musicMml.c b/src/libs/kpunch/music/musicMml.c
new file mode 100644
index 00000000..faaac9fe
--- /dev/null
+++ b/src/libs/kpunch/music/musicMml.c
@@ -0,0 +1,424 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicMml.c -- QBasic's music macro language (see music.h): the parser
+// and the MIDI file a run of instrument notes is played from
+//
+// Timing follows QBasic: a whole note lasts four beats of 60 / T seconds,
+// a note of length n a 1/n of that, each dot half as long again; the
+// sounding part is 7/8 (MN), all (ML) or 3/4 (MS) of it, the rest is
+// silence. Octave n's C is MIDI note 24 + 12n (octave 3 starts at middle
+// C), and its A (octave 3) is 440 Hz.
+
+#include "musicInt.h"
+#include "stb_ds.h"
+
+#include
+#include
+#include
+
+// Ranges and defaults (QBasic's)
+#define MML_OCTAVE_MAX 6
+#define MML_OCTAVE_DEFAULT 4
+#define MML_LENGTH_MIN 1
+#define MML_LENGTH_MAX 64
+#define MML_LENGTH_DEFAULT 4
+#define MML_TEMPO_MIN 32
+#define MML_TEMPO_MAX 255
+#define MML_TEMPO_DEFAULT 120
+#define MML_NOTE_MAX 84 // N: 1..84 (0 is a rest)
+#define MML_NUMBER_MAX 99999 // bigger numbers stop growing (and are out of range)
+#define DECIMAL_BASE 10
+
+// Durations: a whole note is four quarters of 60 / T seconds
+#define MML_US_PER_MINUTE 60000000ULL
+#define MML_QUARTERS_PER_WHOLE 4
+#define MML_DOT_NUM 3
+#define MML_DOT_DEN 2
+
+// Pitches: semitones from octave 0's C; its MIDI note number
+#define MML_SEMITONES 12
+#define MML_MIDI_BASE 24
+
+// Octave 0 (C to B) in thousandths of a hertz; each octave up doubles
+static const uint32_t sOctave0MilliHz[MML_SEMITONES] = {
+ 32703, 34648, 36708, 38891, 41203, 43654, 46249, 48999, 51913, 55000, 58270, 61735
+};
+
+// Semitone of A..G within the octave
+static const int32_t sLetterSemitone[] = { 9, 11, 0, 2, 4, 5, 7 };
+
+// MIDI file: 0.1 ms ticks (10,000 ticks a quarter note of 1,000,000 us)
+#define SMF_DIVISION 10000
+#define SMF_TEMPO_US 1000000
+#define SMF_US_PER_TICK (SMF_TEMPO_US / SMF_DIVISION)
+#define SMF_HEADER_LEN 6
+#define SMF_FORMAT_0 0
+#define SMF_TRACKS 1
+#define SMF_TAG_BYTES 4
+#define SMF_LEN_BYTES 4
+#define SMF_META_TEMPO 0x51
+#define SMF_META_END 0x2F
+#define SMF_TEMPO_BYTES 3
+#define SMF_VELOCITY 100
+#define SMF_VLQ_MAX_BYTES 5
+#define SMF_WORD_BYTES 2
+#define BITS_PER_BYTE 8
+#define BYTE_MASK 0xFF
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void addNote(MmlNoteT **notes, const MmlStateT *st, int32_t pitch, int32_t length, int32_t dots);
+static int32_t countDots(const char **p);
+int32_t mmlParse(MmlStateT *st, const char *macro, MmlNoteT **notes);
+int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_t *size);
+void mmlStateInit(MmlStateT *st);
+static uint32_t pitchMilliHz(int32_t pitch);
+static bool readNumber(const char **p, int32_t *out);
+static bool readRanged(const char **p, int32_t lo, int32_t hi, int32_t *out);
+static void smfBig(uint8_t **buf, uint32_t value, int32_t bytes);
+static void smfVlq(uint8_t **buf, uint32_t value);
+
+
+// A note of pitch (semitones from octave 0's C; -1 a rest) and length
+// (1..64) with dots, as the settings say.
+static void addNote(MmlNoteT **notes, const MmlStateT *st, int32_t pitch, int32_t length, int32_t dots) {
+ uint64_t us = MML_US_PER_MINUTE * MML_QUARTERS_PER_WHOLE / ((uint64_t)st->tempo * (uint64_t)length);
+ MmlNoteT n;
+
+ for (int32_t i = 0; i < dots; i++) {
+ us = us * MML_DOT_NUM / MML_DOT_DEN;
+ }
+
+ n.us = (uint32_t)us;
+ n.soundUs = pitch < 0 ? 0 : (uint32_t)(us * (uint64_t)st->onEighths / MML_EIGHTHS);
+ n.milliHz = pitch < 0 ? 0 : pitchMilliHz(pitch);
+ n.midiNote = pitch < 0 ? 0 : MML_MIDI_BASE + pitch;
+ n.program = st->program;
+ arrput(*notes, n);
+}
+
+
+static int32_t countDots(const char **p) {
+ int32_t dots = 0;
+
+ while (**p == '.') {
+ dots++;
+ (*p)++;
+ }
+
+ return dots;
+}
+
+
+// The string is parsed on a copy of the settings and into the end of the
+// array; a mistake anywhere puts both back as they were.
+int32_t mmlParse(MmlStateT *st, const char *macro, MmlNoteT **notes) {
+ MmlStateT s = *st;
+ int32_t first = (int32_t)arrlen(*notes);
+ const char *p = macro ? macro : "";
+ bool ok = true;
+
+ while (*p && ok) {
+ int32_t c = toupper((unsigned char)*p++);
+ int32_t n = 0;
+
+ switch (c) {
+ case ' ':
+ case '\t':
+ break;
+
+ case 'A':
+ case 'B':
+ case 'C':
+ case 'D':
+ case 'E':
+ case 'F':
+ case 'G': {
+ int32_t pitch = s.octave * MML_SEMITONES + sLetterSemitone[c - 'A'];
+ int32_t length = s.length;
+
+ if (*p == '#' || *p == '+') {
+ pitch++;
+ p++;
+ } else if (*p == '-') {
+ pitch--;
+ p++;
+ }
+
+ if (isdigit((unsigned char)*p)) {
+ ok = readRanged(&p, MML_LENGTH_MIN, MML_LENGTH_MAX, &length);
+ }
+
+ ok = ok && pitch >= 0 && pitch < MML_NOTE_MAX;
+
+ if (ok) {
+ addNote(notes, &s, pitch, length, countDots(&p));
+ }
+
+ break;
+ }
+
+ case 'N':
+ ok = readRanged(&p, 0, MML_NOTE_MAX, &n);
+
+ if (ok) {
+ addNote(notes, &s, n - 1, s.length, countDots(&p));
+ }
+
+ break;
+
+ case 'O':
+ ok = readRanged(&p, 0, MML_OCTAVE_MAX, &s.octave);
+ break;
+
+ case '>':
+ s.octave = s.octave < MML_OCTAVE_MAX ? s.octave + 1 : s.octave;
+ break;
+
+ case '<':
+ s.octave = s.octave > 0 ? s.octave - 1 : s.octave;
+ break;
+
+ case 'L':
+ ok = readRanged(&p, MML_LENGTH_MIN, MML_LENGTH_MAX, &s.length);
+ break;
+
+ case 'T':
+ ok = readRanged(&p, MML_TEMPO_MIN, MML_TEMPO_MAX, &s.tempo);
+ break;
+
+ case 'P':
+ ok = readRanged(&p, MML_LENGTH_MIN, MML_LENGTH_MAX, &n);
+
+ if (ok) {
+ addNote(notes, &s, -1, n, countDots(&p));
+ }
+
+ break;
+
+ case 'I':
+ ok = readRanged(&p, MML_PROGRAM_TONE, MML_PROGRAMS, &s.program);
+ break;
+
+ case 'M':
+ c = toupper((unsigned char)*p);
+ p = *p ? p + 1 : p;
+
+ switch (c) {
+ case 'N':
+ s.onEighths = MML_ON_NORMAL;
+ break;
+
+ case 'L':
+ s.onEighths = MML_ON_LEGATO;
+ break;
+
+ case 'S':
+ s.onEighths = MML_ON_STACCATO;
+ break;
+
+ case 'F':
+ s.background = false;
+ break;
+
+ case 'B':
+ s.background = true;
+ break;
+
+ default:
+ ok = false;
+ break;
+ }
+
+ break;
+
+ default:
+ ok = false;
+ break;
+ }
+ }
+
+ if (!ok) {
+ arrsetlen(*notes, first);
+ return MUSIC_ERR_MACRO;
+ }
+
+ *st = s;
+ return MUSIC_OK;
+}
+
+
+// Notes on channel 1 (instrument changes where they change), a note off
+// at the end of each sounding part, the end of the track after the last
+// rest. Times are rounded from the whole time so far, so nothing drifts.
+int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_t *size) {
+ uint8_t *trk = NULL;
+ uint64_t startUs = 0;
+ uint32_t lastTick = 0;
+ int32_t program = MML_PROGRAM_TONE;
+
+ smfVlq(&trk, 0);
+ arrput(trk, MIDI_META);
+ arrput(trk, SMF_META_TEMPO);
+ arrput(trk, SMF_TEMPO_BYTES);
+ smfBig(&trk, SMF_TEMPO_US, SMF_TEMPO_BYTES);
+
+ for (int32_t i = 0; i < count; i++) {
+ const MmlNoteT *n = ¬es[i];
+ uint32_t onAt = (uint32_t)(startUs / SMF_US_PER_TICK);
+ uint32_t offAt = (uint32_t)((startUs + n->soundUs) / SMF_US_PER_TICK);
+
+ if (n->program != program && n->program != MML_PROGRAM_TONE) {
+ program = n->program;
+ smfVlq(&trk, onAt - lastTick);
+ arrput(trk, MIDI_PROGRAM);
+ arrput(trk, (uint8_t)(program - 1));
+ lastTick = onAt;
+ }
+
+ if (n->milliHz) {
+ smfVlq(&trk, onAt - lastTick);
+ arrput(trk, MIDI_NOTE_ON);
+ arrput(trk, (uint8_t)n->midiNote);
+ arrput(trk, SMF_VELOCITY);
+ smfVlq(&trk, offAt - onAt);
+ arrput(trk, MIDI_NOTE_OFF);
+ arrput(trk, (uint8_t)n->midiNote);
+ arrput(trk, 0);
+ lastTick = offAt;
+ }
+
+ startUs += n->us;
+ }
+
+ smfVlq(&trk, (uint32_t)(startUs / SMF_US_PER_TICK) - lastTick);
+ arrput(trk, MIDI_META);
+ arrput(trk, SMF_META_END);
+ arrput(trk, 0);
+
+ uint32_t trkLen = (uint32_t)arrlen(trk);
+ uint32_t total = SMF_TAG_BYTES + SMF_LEN_BYTES + SMF_HEADER_LEN + SMF_TAG_BYTES + SMF_LEN_BYTES + trkLen;
+ uint8_t *file = (uint8_t *)malloc(total);
+ uint8_t *hdr = NULL;
+
+ if (!file) {
+ arrfree(trk);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ arrput(hdr, 'M');
+ arrput(hdr, 'T');
+ arrput(hdr, 'h');
+ arrput(hdr, 'd');
+ smfBig(&hdr, SMF_HEADER_LEN, SMF_LEN_BYTES);
+ smfBig(&hdr, SMF_FORMAT_0, SMF_WORD_BYTES);
+ smfBig(&hdr, SMF_TRACKS, SMF_WORD_BYTES);
+ smfBig(&hdr, SMF_DIVISION, SMF_WORD_BYTES);
+ arrput(hdr, 'M');
+ arrput(hdr, 'T');
+ arrput(hdr, 'r');
+ arrput(hdr, 'k');
+ smfBig(&hdr, trkLen, SMF_LEN_BYTES);
+ memcpy(file, hdr, arrlen(hdr));
+ memcpy(file + arrlen(hdr), trk, trkLen);
+ arrfree(hdr);
+ arrfree(trk);
+ *out = file;
+ *size = total;
+ return MUSIC_OK;
+}
+
+
+void mmlStateInit(MmlStateT *st) {
+ st->octave = MML_OCTAVE_DEFAULT;
+ st->length = MML_LENGTH_DEFAULT;
+ st->tempo = MML_TEMPO_DEFAULT;
+ st->onEighths = MML_ON_NORMAL;
+ st->program = MML_PROGRAM_TONE;
+ st->background = false;
+}
+
+
+static uint32_t pitchMilliHz(int32_t pitch) {
+ return sOctave0MilliHz[pitch % MML_SEMITONES] << (pitch / MML_SEMITONES);
+}
+
+
+// Spaces may come before the digits. False without digits.
+static bool readNumber(const char **p, int32_t *out) {
+ int32_t v = 0;
+ bool any = false;
+
+ while (**p == ' ' || **p == '\t') {
+ (*p)++;
+ }
+
+ while (isdigit((unsigned char)**p)) {
+ v = v < MML_NUMBER_MAX ? v * DECIMAL_BASE + (**p - '0') : v;
+ any = true;
+ (*p)++;
+ }
+
+ *out = v;
+ return any;
+}
+
+
+static bool readRanged(const char **p, int32_t lo, int32_t hi, int32_t *out) {
+ int32_t v = 0;
+
+ if (!readNumber(p, &v) || v < lo || v > hi) {
+ return false;
+ }
+
+ *out = v;
+ return true;
+}
+
+
+// value in bytes bytes, most significant first.
+static void smfBig(uint8_t **buf, uint32_t value, int32_t bytes) {
+ for (int32_t i = bytes - 1; i >= 0; i--) {
+ arrput(*buf, (uint8_t)((value >> (i * BITS_PER_BYTE)) & BYTE_MASK));
+ }
+}
+
+
+// A variable-length quantity: 7 bits a byte, most significant first, the
+// top bit set on all but the last.
+static void smfVlq(uint8_t **buf, uint32_t value) {
+ uint8_t tmp[SMF_VLQ_MAX_BYTES];
+ int32_t n = 0;
+
+ do {
+ tmp[n++] = (uint8_t)(value & MIDI_DATA_MASK);
+ value >>= MIDI_DATA_BITS;
+ } while (value);
+
+ while (n > 0) {
+ n--;
+ arrput(*buf, (uint8_t)(tmp[n] | (n > 0 ? MIDI_STATUS_BIT : 0)));
+ }
+}
diff --git a/src/libs/kpunch/music/musicMmlPlay.c b/src/libs/kpunch/music/musicMmlPlay.c
new file mode 100644
index 00000000..38581fbe
--- /dev/null
+++ b/src/libs/kpunch/music/musicMmlPlay.c
@@ -0,0 +1,433 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicMmlPlay.c -- music macro queues (musicMml... in music.h)
+//
+// A queue plays its notes as parts, one after the other: a run of
+// square-wave notes goes into an audio.lib note queue (which plays each to
+// the sample, however late the next one is queued), a run of instrument
+// notes (I1..I128) becomes a small MIDI file played as a music. A part
+// starts when the one before has finished, except that tone parts follow
+// each other straight on in the note queue. The queue moves along
+// whenever the program uses it and in the shell's idle time. When the
+// instruments cannot play (no synthesizer, no bank) their notes play as
+// tones. Each queue has its own audio context opened for no program (so
+// no clean-up order can close it under the queue); its instrument parts
+// belong to its program.
+
+#include "musicInt.h"
+#include "stb_ds.h"
+
+#include
+#include
+
+// Loudness of the square-wave tones
+#define MML_VOLUME (AUDIO_VOLUME_MAX * 3 / 4)
+
+// Note queue entries one note may take (the tone and the silence after it)
+#define MML_ENTRIES_NOTE 2
+
+#define US_PER_MS 1000
+
+// A run of notes played the same way.
+typedef struct {
+ MmlNoteT *notes; // stb_ds array, owned
+ int32_t next; // tone parts: notes already in the note queue
+ bool midi; // on instruments
+} MmlPartT;
+
+struct MusicMmlT {
+ MusicMmlT *next;
+ MmlStateT state;
+ AudioCtxT *ctx; // NULL: no sound device (the queue stays silent)
+ AudioNotesT *tones;
+ MmlPartT *parts; // stb_ds array: waiting, first to play first
+ MmlPartT playing; // the instrument part playing ...
+ MusicT *music; // ... and its music (NULL: none)
+ int32_t appId;
+ bool basic; // the queue of a BASIC program (musicMmlBasic)
+};
+
+static MusicMmlT *sMmls = NULL;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void midiEnd(MusicMmlT *m);
+static bool midiStart(MusicMmlT *m, MmlPartT *part);
+void musicMmlAppEnded(int32_t appId);
+MusicMmlT *musicMmlBasic(bool create);
+bool musicMmlBusy(MusicMmlT *mml);
+void musicMmlClose(MusicMmlT *mml);
+void musicMmlIdle(void);
+int32_t musicMmlOpen(MusicMmlT **out);
+int32_t musicMmlPlay(MusicMmlT *mml, const char *macro);
+uint32_t musicMmlQueued(MusicMmlT *mml);
+void musicMmlShutdown(void);
+void musicMmlStop(MusicMmlT *mml);
+int32_t musicMmlTone(MusicMmlT *mml, uint32_t milliHz, uint32_t us);
+static uint32_t notesLeft(const MmlNoteT *notes, int32_t from, uint32_t afterMs);
+static void partAdd(MusicMmlT *m, const MmlNoteT *notes, int32_t count, bool midi);
+static void pump(MusicMmlT *m);
+static bool tonesFeed(MusicMmlT *m, MmlPartT *part);
+static bool tonesPlaying(const MusicMmlT *m);
+static uint32_t tonesQueued(const MusicMmlT *m);
+
+
+// The instrument part has played: free its music.
+static void midiEnd(MusicMmlT *m) {
+ musicFree(m->music);
+ arrfree(m->playing.notes);
+ m->music = NULL;
+ m->playing.notes = NULL;
+}
+
+
+// Play part on the instruments, as the queue's program; false when it
+// cannot be (the part is left as it was).
+static bool midiStart(MusicMmlT *m, MmlPartT *part) {
+ uint8_t *smf = NULL;
+ uint32_t size = 0;
+ MusicT *mus = NULL;
+ int32_t prev = musicAppEnter(m->appId);
+ int32_t rc = mmlSmfBuild(part->notes, (int32_t)arrlen(part->notes), &smf, &size);
+
+ if (rc == MUSIC_OK) {
+ rc = musicLoadMem(smf, size, &mus);
+ }
+
+ if (rc == MUSIC_OK) {
+ rc = musicPlay(mus, false);
+ }
+
+ free(smf);
+ musicAppEnter(prev);
+
+ if (rc != MUSIC_OK) {
+ musicFree(mus);
+ return false;
+ }
+
+ m->music = mus;
+ m->playing = *part;
+ return true;
+}
+
+
+void musicMmlAppEnded(int32_t appId) {
+ MusicMmlT *m = sMmls;
+
+ while (m) {
+ MusicMmlT *next = m->next;
+
+ if (m->appId == appId) {
+ musicMmlClose(m);
+ }
+
+ m = next;
+ }
+}
+
+
+MusicMmlT *musicMmlBasic(bool create) {
+ int32_t appId = musicAppCurrent();
+
+ for (MusicMmlT *m = sMmls; m; m = m->next) {
+ if (m->basic && m->appId == appId) {
+ return m;
+ }
+ }
+
+ MusicMmlT *m = NULL;
+
+ if (create && musicMmlOpen(&m) == MUSIC_OK) {
+ m->basic = true;
+ }
+
+ return m;
+}
+
+
+bool musicMmlBusy(MusicMmlT *mml) {
+ if (!mml) {
+ return false;
+ }
+
+ pump(mml);
+
+ if (mml->state.background) {
+ return musicMmlQueued(mml) > MUSIC_MML_BACKGROUND_MAX;
+ }
+
+ return arrlen(mml->parts) > 0 || mml->music || tonesPlaying(mml);
+}
+
+
+void musicMmlClose(MusicMmlT *mml) {
+ if (!mml) {
+ return;
+ }
+
+ musicMmlStop(mml);
+ arrfree(mml->parts);
+ audioNotesClose(mml->tones);
+ audioClose(mml->ctx);
+
+ for (MusicMmlT **mp = &sMmls; *mp; mp = &(*mp)->next) {
+ if (*mp == mml) {
+ *mp = mml->next;
+ break;
+ }
+ }
+
+ free(mml);
+}
+
+
+void musicMmlIdle(void) {
+ for (MusicMmlT *m = sMmls; m; m = m->next) {
+ pump(m);
+ }
+}
+
+
+int32_t musicMmlOpen(MusicMmlT **out) {
+ MusicMmlT *m = (MusicMmlT *)calloc(1, sizeof(MusicMmlT));
+
+ *out = NULL;
+
+ if (!m) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ mmlStateInit(&m->state);
+ m->appId = musicAppEnter(0);
+ m->ctx = audioOpen();
+ musicAppEnter(m->appId);
+
+ if (m->ctx) {
+ audioNotesOpen(m->ctx, AUDIO_WAVE_SQUARE, MML_VOLUME, &m->tones);
+ }
+
+ m->next = sMmls;
+ sMmls = m;
+ *out = m;
+ return MUSIC_OK;
+}
+
+
+// Parts are split where the notes change between tones and instruments.
+int32_t musicMmlPlay(MusicMmlT *mml, const char *macro) {
+ MmlNoteT *notes = NULL;
+
+ if (!mml) {
+ return MUSIC_ERR_ARG;
+ }
+
+ int32_t rc = mmlParse(&mml->state, macro, ¬es);
+
+ if (rc == MUSIC_OK) {
+ int32_t count = (int32_t)arrlen(notes);
+ int32_t start = 0;
+
+ for (int32_t i = 1; i <= count; i++) {
+ bool midi = notes[start].program != MML_PROGRAM_TONE;
+
+ if (i == count || (notes[i].program != MML_PROGRAM_TONE) != midi) {
+ partAdd(mml, ¬es[start], i - start, midi);
+ start = i;
+ }
+ }
+
+ pump(mml);
+ }
+
+ arrfree(notes);
+ return rc;
+}
+
+
+uint32_t musicMmlQueued(MusicMmlT *mml) {
+ if (!mml) {
+ return 0;
+ }
+
+ pump(mml);
+
+ uint32_t count = tonesQueued(mml);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(mml->parts); i++) {
+ count += notesLeft(mml->parts[i].notes, mml->parts[i].next, 0);
+ }
+
+ if (mml->music) {
+ count += notesLeft(mml->playing.notes, 0, musicGetPosition(mml->music));
+ }
+
+ return count;
+}
+
+
+void musicMmlShutdown(void) {
+ while (sMmls) {
+ musicMmlClose(sMmls);
+ }
+}
+
+
+void musicMmlStop(MusicMmlT *mml) {
+ if (!mml) {
+ return;
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(mml->parts); i++) {
+ arrfree(mml->parts[i].notes);
+ }
+
+ arrsetlen(mml->parts, 0);
+ audioNotesClear(mml->tones);
+
+ if (mml->music) {
+ midiEnd(mml);
+ }
+}
+
+
+int32_t musicMmlTone(MusicMmlT *mml, uint32_t milliHz, uint32_t us) {
+ if (!mml) {
+ return MUSIC_ERR_ARG;
+ }
+
+ MmlNoteT n;
+
+ memset(&n, 0, sizeof(n));
+ n.milliHz = milliHz;
+ n.us = us;
+ n.soundUs = milliHz ? us : 0;
+ n.program = MML_PROGRAM_TONE;
+ partAdd(mml, &n, 1, false);
+ pump(mml);
+ return MUSIC_OK;
+}
+
+
+// Sounding notes from index from on that end after afterMs.
+static uint32_t notesLeft(const MmlNoteT *notes, int32_t from, uint32_t afterMs) {
+ uint64_t at = 0;
+ uint32_t count = 0;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(notes); i++) {
+ uint64_t endMs = (at + notes[i].soundUs) / US_PER_MS;
+
+ if (i >= from && notes[i].milliHz && endMs >= afterMs) {
+ count++;
+ }
+
+ at += notes[i].us;
+ }
+
+ return count;
+}
+
+
+static void partAdd(MusicMmlT *m, const MmlNoteT *notes, int32_t count, bool midi) {
+ MmlPartT part;
+
+ memset(&part, 0, sizeof(part));
+ part.midi = midi;
+
+ for (int32_t i = 0; i < count; i++) {
+ arrput(part.notes, notes[i]);
+ }
+
+ arrput(m->parts, part);
+}
+
+
+// Start what can start: the first part once what plays before it has
+// finished (tone parts straight after tones).
+static void pump(MusicMmlT *m) {
+ if (m->music && !musicIsPlaying(m->music)) {
+ midiEnd(m);
+ }
+
+ while (arrlen(m->parts) > 0 && !m->music) {
+ MmlPartT *part = &m->parts[0];
+
+ if (part->midi) {
+ if (tonesPlaying(m)) {
+ return;
+ }
+
+ if (!midiStart(m, part)) {
+ part->midi = false;
+ continue;
+ }
+ } else if (!tonesFeed(m, part)) {
+ return;
+ } else {
+ arrfree(part->notes);
+ }
+
+ arrdel(m->parts, 0);
+ }
+}
+
+
+// Queue as much of a tone part as fits; true once all of it is queued (at
+// once without a sound device).
+static bool tonesFeed(MusicMmlT *m, MmlPartT *part) {
+ int32_t count = (int32_t)arrlen(part->notes);
+
+ while (m->tones && part->next < count && audioNotesSpace(m->tones) >= MML_ENTRIES_NOTE) {
+ const MmlNoteT *n = &part->notes[part->next++];
+
+ if (n->milliHz && audioNotesAdd(m->tones, n->milliHz, n->soundUs) == AUDIO_OK) {
+ if (n->us > n->soundUs) {
+ audioNotesAdd(m->tones, 0, n->us - n->soundUs);
+ }
+ } else {
+ audioNotesAdd(m->tones, 0, n->us);
+ }
+ }
+
+ return !m->tones || part->next == count;
+}
+
+
+// The note queue is not quiet yet (tones or the silences between them).
+static bool tonesPlaying(const MusicMmlT *m) {
+ return m->tones && audioNotesQueued(m->tones, NULL) > 0;
+}
+
+
+// Tones in the note queue, playing or waiting.
+static uint32_t tonesQueued(const MusicMmlT *m) {
+ uint32_t tones = 0;
+
+ if (m->tones) {
+ audioNotesQueued(m->tones, &tones);
+ }
+
+ return tones;
+}
diff --git a/src/libs/kpunch/music/musicMpu.c b/src/libs/kpunch/music/musicMpu.c
new file mode 100644
index 00000000..5a6ce681
--- /dev/null
+++ b/src/libs/kpunch/music/musicMpu.c
@@ -0,0 +1,361 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicMpu.c -- MIDI out of the MPU-401 port
+//
+// An MpuSinkT turns MIDI into bytes in a queue, which the music timer
+// drains into the port (audioMidiPut) as fast as it takes them, at most
+// MPU_BYTES_PER_TICK a tick, pausing MPU_SYSEX_GAP_US after each system
+// exclusive message (a first-generation MT-32 loses data without that
+// gap). A message that does not fit in the queue is dropped whole.
+// Runs at interrupt time as well (see musicInt.h).
+//
+// The music's volume scales every channel volume (CC7) sent. Modes
+// (DVX.INI [audio] mpuMode):
+// gm a General MIDI synthesizer: the music as it is, after a General
+// MIDI System On message.
+// mt32 a Roland MT-32 playing General MIDI music: channels move to
+// the MT-32's parts (percussion stays on channel 10, the melodic
+// channels take the eight parts on channels 2..9 in the order
+// they are first used; a ninth shares the last part), programs
+// go through a General MIDI to MT-32 timbre table, pan is
+// mirrored (the MT-32's pan runs the other way), the bend range
+// is set to 2 semitones (and RPN 0 changes it) by system
+// exclusive messages, since the MT-32 has no RPNs, and what it
+// does not understand (other RPNs and NRPNs, bank select, reverb
+// and chorus depth, aftertouch, non-Roland system exclusive
+// messages) is left out.
+// raw anything: the music as it is, nothing added (music made for an
+// MT-32 with its own set-up messages).
+
+#include "musicInt.h"
+
+#define MPU_BYTES_PER_TICK 16
+#define MPU_MSG_BYTES_MAX 3
+#define GAIN_SHIFT 8
+#define DATA_MAX 127
+#define ROLAND_ID 0x41
+
+// Roland DT1 (data set) for the MT-32: F0 41 10 16 12 aa aa aa dd sum F7
+#define DT1_DEVICE 0x10
+#define DT1_MODEL_MT32 0x16
+#define DT1_COMMAND 0x12
+#define DT1_BYTES 11
+#define DT1_CHECKSUM_MOD 128
+#define MT32_PATCH_AREA 0x03 // patch temporary area, 16 bytes a part
+#define MT32_PART_STRIDE 0x10
+#define MT32_BENDER_RANGE 0x04 // offset of the bender range in a part
+
+// General MIDI System On
+static const uint8_t sGmOn[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
+
+// General MIDI program -> MT-32 preset timbre (our own choice of the
+// nearest sound; the MT-32 has no exact match for many of them)
+const uint8_t mpuGmToMt32[OP2_MELODIC] = {
+ 0, 1, 2, 7, 3, 4, 16, 19, 22, 101, 101, 97, 104, 103, 102, 105,
+ 8, 9, 10, 12, 14, 15, 87, 15, 59, 60, 61, 61, 62, 62, 62, 60,
+ 64, 66, 67, 70, 68, 69, 28, 29, 52, 53, 54, 56, 49, 51, 57, 112,
+ 48, 50, 50, 49, 34, 34, 46, 122, 88, 90, 94, 89, 92, 95, 24, 25,
+ 78, 79, 80, 81, 84, 85, 86, 82, 74, 72, 76, 77, 110, 107, 108, 73,
+ 47, 26, 111, 110, 62, 39, 44, 31, 32, 36, 33, 34, 35, 38, 37, 43,
+ 41, 36, 40, 37, 32, 44, 43, 42, 63, 60, 105, 105, 99, 106, 53, 84,
+ 100, 115, 126, 120, 117, 113, 116, 119, 60, 111, 43, 124, 123, 125, 119, 118
+};
+
+const PrefsChoiceT mpuModeChoices[MPU_MODE_COUNT] = {
+ { "General MIDI synthesizer", "gm", MPU_MODE_GM },
+ { "Roland MT-32", "mt32", MPU_MODE_MT32 },
+ { "Send the music as it is", "raw", MPU_MODE_RAW }
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void mpuBendRange(MpuSinkT *m, uint8_t outChannel, uint8_t semitones);
+int32_t mpuLockItems(AudioLockItemT *items);
+static void mpuMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2);
+static uint8_t mpuPart(MpuSinkT *m, uint8_t channel);
+static bool mpuQueue(MpuSinkT *m, const uint8_t *bytes, uint32_t count);
+static void mpuQueueMsg(MpuSinkT *m, uint8_t status, uint8_t data1, uint8_t data2);
+static void mpuSilence(MidiSinkT *sink);
+void mpuSinkDrain(MpuSinkT *mpu, uint32_t us);
+void mpuSinkInit(MpuSinkT *mpu, AudioCtxT *ctx, int32_t mode, int32_t gain);
+void mpuSinkSetGain(MpuSinkT *mpu, int32_t gain);
+void mpuSinkStart(MpuSinkT *mpu);
+static void mpuSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len);
+static uint8_t mpuVolume(const MpuSinkT *m, uint8_t value);
+
+
+// DT1 to a part's bender range (MT-32 parts are on channels 1..8).
+static void mpuBendRange(MpuSinkT *m, uint8_t outChannel, uint8_t semitones) {
+ uint8_t part = (uint8_t)(outChannel - MPU_MT32_FIRST_PART);
+ uint8_t lo = (uint8_t)(part * MT32_PART_STRIDE + MT32_BENDER_RANGE);
+ uint8_t sum = (uint8_t)((DT1_CHECKSUM_MOD - ((MT32_PATCH_AREA + lo + semitones) % DT1_CHECKSUM_MOD)) % DT1_CHECKSUM_MOD);
+ uint8_t msg[DT1_BYTES] = { MIDI_SYSEX, ROLAND_ID, DT1_DEVICE, DT1_MODEL_MT32, DT1_COMMAND, MT32_PATCH_AREA, 0, lo, semitones, sum, MIDI_SYSEX_END };
+
+ mpuQueue(m, msg, DT1_BYTES);
+}
+
+
+// The functions of this file for musicLock (every one: any may run at
+// interrupt time). Returns how many were stored in items.
+int32_t mpuLockItems(AudioLockItemT *items) {
+ const void *fns[] = {
+ (const void *)mpuBendRange,
+ (const void *)mpuLockItems,
+ (const void *)mpuMessage,
+ (const void *)mpuPart,
+ (const void *)mpuQueue,
+ (const void *)mpuQueueMsg,
+ (const void *)mpuSilence,
+ (const void *)mpuSinkDrain,
+ (const void *)mpuSinkInit,
+ (const void *)mpuSinkSetGain,
+ (const void *)mpuSinkStart,
+ (const void *)mpuSysex,
+ (const void *)mpuVolume
+ };
+ int32_t count = (int32_t)(sizeof(fns) / sizeof(fns[0]));
+
+ for (int32_t i = 0; i < count; i++) {
+ items[i].addr = fns[i];
+ items[i].size = 0;
+ }
+
+ return count;
+}
+
+
+static void mpuMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2) {
+ MpuSinkT *m = (MpuSinkT *)sink;
+ uint8_t type = status & MIDI_TYPE_MASK;
+ uint8_t channel = status & MIDI_CHANNEL_MASK;
+
+ data1 &= MIDI_DATA_MASK;
+ data2 &= MIDI_DATA_MASK;
+
+ if (type == MIDI_CONTROL && data1 == MIDI_CC_VOLUME) {
+ m->volume[channel] = data2;
+ data2 = mpuVolume(m, data2);
+ }
+
+ if (m->mode != MPU_MODE_MT32) {
+ mpuQueueMsg(m, status, data1, data2);
+ return;
+ }
+
+ if (type == MIDI_POLY_PRESSURE || type == MIDI_CHAN_PRESSURE || (type == MIDI_PROGRAM && channel == MIDI_PERCUSSION)) {
+ return;
+ }
+
+ uint8_t out = mpuPart(m, channel);
+
+ if (type == MIDI_PROGRAM) {
+ data1 = mpuGmToMt32[data1];
+ } else if (type == MIDI_CONTROL) {
+ switch (data1) {
+ case MIDI_CC_PAN:
+ data2 = (uint8_t)(DATA_MAX - data2);
+ break;
+
+ case MIDI_CC_SOUND_OFF:
+ data1 = MIDI_CC_NOTES_OFF;
+ break;
+
+ case MIDI_CC_RPN_MSB:
+ m->rpnMsb[channel] = data2;
+ return;
+
+ case MIDI_CC_RPN_LSB:
+ m->rpnLsb[channel] = data2;
+ return;
+
+ case MIDI_CC_DATA:
+ if (m->rpnMsb[channel] == 0 && m->rpnLsb[channel] == MIDI_RPN_BEND_RANGE && channel != MIDI_PERCUSSION) {
+ mpuBendRange(m, out, data2);
+ }
+
+ return;
+
+ case MIDI_CC_MODULATION:
+ case MIDI_CC_VOLUME:
+ case MIDI_CC_EXPRESSION:
+ case MIDI_CC_SUSTAIN:
+ case MIDI_CC_RESET:
+ case MIDI_CC_NOTES_OFF:
+ break;
+
+ default:
+ return;
+ }
+ }
+
+ mpuQueueMsg(m, (uint8_t)(type | out), data1, data2);
+}
+
+
+// The output channel of a channel in MT-32 mode (see the top).
+static uint8_t mpuPart(MpuSinkT *m, uint8_t channel) {
+ if (channel == MIDI_PERCUSSION) {
+ return MIDI_PERCUSSION;
+ }
+
+ if (m->partOf[channel] == MPU_PART_NONE) {
+ m->partOf[channel] = (uint8_t)(MPU_MT32_FIRST_PART + (m->nextPart < MPU_MT32_PARTS ? m->nextPart : MPU_MT32_PARTS - 1));
+
+ if (m->nextPart < MPU_MT32_PARTS) {
+ m->nextPart++;
+ }
+ }
+
+ return m->partOf[channel];
+}
+
+
+// All or nothing.
+static bool mpuQueue(MpuSinkT *m, const uint8_t *bytes, uint32_t count) {
+ if (MPU_QUEUE_BYTES - (m->head - m->tail) < count) {
+ m->dropped += count;
+ return false;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ m->queue[(m->head + i) & (MPU_QUEUE_BYTES - 1)] = bytes[i];
+ }
+
+ __atomic_signal_fence(__ATOMIC_RELEASE);
+ m->head += count;
+ return true;
+}
+
+
+static void mpuQueueMsg(MpuSinkT *m, uint8_t status, uint8_t data1, uint8_t data2) {
+ uint8_t type = status & MIDI_TYPE_MASK;
+ uint8_t msg[MPU_MSG_BYTES_MAX] = { status, data1, data2 };
+
+ mpuQueue(m, msg, (type == MIDI_PROGRAM || type == MIDI_CHAN_PRESSURE) ? MPU_MSG_BYTES_MAX - 1 : MPU_MSG_BYTES_MAX);
+}
+
+
+// Sustain off and all notes off on every channel the synthesizer hears.
+static void mpuSilence(MidiSinkT *sink) {
+ MpuSinkT *m = (MpuSinkT *)sink;
+
+ for (uint8_t ch = 0; ch < MUSIC_MIDI_CHANNELS; ch++) {
+ bool heard = m->mode != MPU_MODE_MT32 || ch == MIDI_PERCUSSION || (ch >= MPU_MT32_FIRST_PART && ch < MPU_MT32_FIRST_PART + MPU_MT32_PARTS);
+
+ if (heard) {
+ mpuQueueMsg(m, (uint8_t)(MIDI_CONTROL | ch), MIDI_CC_SUSTAIN, 0);
+ mpuQueueMsg(m, (uint8_t)(MIDI_CONTROL | ch), MIDI_CC_NOTES_OFF, 0);
+ }
+ }
+}
+
+
+void mpuSinkDrain(MpuSinkT *mpu, uint32_t us) {
+ mpu->now += us;
+
+ for (int32_t sent = 0; sent < MPU_BYTES_PER_TICK && mpu->tail != mpu->head; sent++) {
+ if ((int32_t)(mpu->now - mpu->holdUntil) < 0) {
+ break;
+ }
+
+ uint8_t b = mpu->queue[mpu->tail & (MPU_QUEUE_BYTES - 1)];
+
+ if (audioMidiPut(mpu->ctx, b) != AUDIO_OK) {
+ break;
+ }
+
+ mpu->tail++;
+
+ if (b == MIDI_SYSEX_END) {
+ mpu->holdUntil = mpu->now + MPU_SYSEX_GAP_US;
+ }
+ }
+}
+
+
+void mpuSinkInit(MpuSinkT *mpu, AudioCtxT *ctx, int32_t mode, int32_t gain) {
+ mpu->sink.message = mpuMessage;
+ mpu->sink.sysex = mpuSysex;
+ mpu->sink.silence = mpuSilence;
+ mpu->ctx = ctx;
+ mpu->head = 0;
+ mpu->tail = 0;
+ mpu->now = 0;
+ mpu->holdUntil = 0;
+ mpu->dropped = 0;
+ mpu->mode = mode;
+ mpu->gain = gain;
+ mpu->nextPart = 0;
+
+ for (int32_t c = 0; c < MUSIC_MIDI_CHANNELS; c++) {
+ mpu->volume[c] = MIDI_VOLUME_DEFAULT;
+ mpu->partOf[c] = MPU_PART_NONE;
+ mpu->rpnMsb[c] = MIDI_RPN_NULL;
+ mpu->rpnLsb[c] = MIDI_RPN_NULL;
+ }
+}
+
+
+// Every channel volume again at the new gain (in MT-32 mode only the
+// channels that have a part).
+void mpuSinkSetGain(MpuSinkT *mpu, int32_t gain) {
+ mpu->gain = gain;
+
+ for (uint8_t c = 0; c < MUSIC_MIDI_CHANNELS; c++) {
+ if (mpu->mode != MPU_MODE_MT32) {
+ mpuQueueMsg(mpu, (uint8_t)(MIDI_CONTROL | c), MIDI_CC_VOLUME, mpuVolume(mpu, mpu->volume[c]));
+ } else if (c == MIDI_PERCUSSION || mpu->partOf[c] != MPU_PART_NONE) {
+ mpuQueueMsg(mpu, (uint8_t)(MIDI_CONTROL | mpuPart(mpu, c)), MIDI_CC_VOLUME, mpuVolume(mpu, mpu->volume[c]));
+ }
+ }
+}
+
+
+void mpuSinkStart(MpuSinkT *mpu) {
+ if (mpu->mode == MPU_MODE_GM) {
+ mpuQueue(mpu, sGmOn, sizeof(sGmOn));
+ } else if (mpu->mode == MPU_MODE_MT32) {
+ for (uint8_t p = 0; p < MPU_MT32_PARTS; p++) {
+ mpuBendRange(mpu, (uint8_t)(MPU_MT32_FIRST_PART + p), MIDI_BEND_RANGE_DEFAULT);
+ }
+ }
+}
+
+
+// In MT-32 mode only Roland's own messages go through.
+static void mpuSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len) {
+ MpuSinkT *m = (MpuSinkT *)sink;
+
+ if (m->mode == MPU_MODE_MT32 && (len < 2 || data[1] != ROLAND_ID)) {
+ return;
+ }
+
+ mpuQueue(m, data, len);
+}
+
+
+static uint8_t mpuVolume(const MpuSinkT *m, uint8_t value) {
+ return (uint8_t)((value * m->gain) >> GAIN_SHIFT);
+}
diff --git a/src/libs/kpunch/music/musicOp2.c b/src/libs/kpunch/music/musicOp2.c
new file mode 100644
index 00000000..e429c4f2
--- /dev/null
+++ b/src/libs/kpunch/music/musicOp2.c
@@ -0,0 +1,108 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicOp2.c -- OP2 instrument banks (the DMX "GENMIDI" format)
+//
+// "#OPL_II#", then OP2_INSTRUMENTS instruments of 36 bytes (the 128
+// General MIDI programs, then the percussion notes 35..81), then their
+// names (32 bytes each, not needed). An instrument: flags (16 bits),
+// fine tune, fixed note, two voices of 16 bytes: modulator
+// characteristic, attack/decay, sustain/release, waveform, key scale
+// level (bits 6-7, already in place), level; feedback/connection;
+// carrier the same six; one unused byte; note offset (signed 16 bits).
+// All numbers little-endian.
+
+#include "musicInt.h"
+
+#include
+
+#define OP2_SIGNATURE "#OPL_II#"
+#define OP2_SIGNATURE_BYTES 8
+#define OP2_INSTR_BYTES 36
+#define OP2_VOICE_AT 4
+#define OP2_VOICE_BYTES 16
+#define OP2_FINE_TUNE_AT 2
+#define OP2_FIXED_NOTE_AT 3
+#define OP2_NOTE_OFFSET_AT 14
+#define BYTE_BITS 8
+
+// Byte offsets within a voice
+enum {
+ VOICE_MOD_CHAR = 0,
+ VOICE_MOD_ATTACK,
+ VOICE_MOD_SUSTAIN,
+ VOICE_MOD_WAVE,
+ VOICE_MOD_KSL,
+ VOICE_MOD_LEVEL,
+ VOICE_FEEDBACK,
+ VOICE_CAR_CHAR,
+ VOICE_CAR_ATTACK,
+ VOICE_CAR_SUSTAIN,
+ VOICE_CAR_WAVE,
+ VOICE_CAR_KSL,
+ VOICE_CAR_LEVEL
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+int32_t op2Parse(const uint8_t *data, uint32_t size, Op2BankT *bank);
+static void readVoice(const uint8_t *p, Op2VoiceT *v);
+
+
+int32_t op2Parse(const uint8_t *data, uint32_t size, Op2BankT *bank) {
+ if (!data || size < OP2_SIGNATURE_BYTES + OP2_INSTRUMENTS * OP2_INSTR_BYTES || memcmp(data, OP2_SIGNATURE, OP2_SIGNATURE_BYTES) != 0) {
+ return MUSIC_ERR_BANK;
+ }
+
+ for (int32_t i = 0; i < OP2_INSTRUMENTS; i++) {
+ const uint8_t *p = data + OP2_SIGNATURE_BYTES + i * OP2_INSTR_BYTES;
+ Op2InstrumentT *in = &bank->instruments[i];
+
+ in->flags = (uint16_t)(p[0] | (p[1] << BYTE_BITS));
+ in->fineTune = p[OP2_FINE_TUNE_AT];
+ in->fixedNote = p[OP2_FIXED_NOTE_AT];
+
+ for (int32_t v = 0; v < OP2_VOICES; v++) {
+ readVoice(p + OP2_VOICE_AT + v * OP2_VOICE_BYTES, &in->voice[v]);
+ }
+ }
+
+ return MUSIC_OK;
+}
+
+
+static void readVoice(const uint8_t *p, Op2VoiceT *v) {
+ v->modChar = p[VOICE_MOD_CHAR];
+ v->modAttack = p[VOICE_MOD_ATTACK];
+ v->modSustain = p[VOICE_MOD_SUSTAIN];
+ v->modWave = p[VOICE_MOD_WAVE];
+ v->modLevel = (uint8_t)(p[VOICE_MOD_KSL] | p[VOICE_MOD_LEVEL]);
+ v->feedback = p[VOICE_FEEDBACK];
+ v->carChar = p[VOICE_CAR_CHAR];
+ v->carAttack = p[VOICE_CAR_ATTACK];
+ v->carSustain = p[VOICE_CAR_SUSTAIN];
+ v->carWave = p[VOICE_CAR_WAVE];
+ v->carLevel = (uint8_t)(p[VOICE_CAR_KSL] | p[VOICE_CAR_LEVEL]);
+ v->noteOffset = (int16_t)(p[OP2_NOTE_OFFSET_AT] | (p[OP2_NOTE_OFFSET_AT + 1] << BYTE_BITS));
+}
diff --git a/src/libs/kpunch/music/musicSeq.c b/src/libs/kpunch/music/musicSeq.c
new file mode 100644
index 00000000..6f5883d7
--- /dev/null
+++ b/src/libs/kpunch/music/musicSeq.c
@@ -0,0 +1,235 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicSeq.c -- the sequencer, the event ring and the music timer
+//
+// Runs at interrupt time as well as program time (see musicInt.h): the
+// timer callback musicTick plays every MIDI music on the FM chip or the
+// MPU-401 port, MUSIC_TICK_US at a time. The software synthesizer runs
+// the same sequencer from its stream's fill function instead.
+
+#include "musicInt.h"
+
+#include
+
+#define RING_STATUS_SHIFT 0
+#define RING_DATA1_SHIFT 8
+#define RING_DATA2_SHIFT 16
+#define RING_BYTE_MASK 0xFF
+#define LOCK_ITEMS_MAX 64
+
+IsrPlayerT *musicIsrPlayers[MUSIC_ISR_PLAYERS_MAX];
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void eventRingDrain(EventRingT *ring, MidiSinkT *to);
+void eventRingInit(EventRingT *ring);
+bool musicLock(bool lock);
+void musicTick(void *unused);
+static void ringMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2);
+static void ringPush(EventRingT *ring, uint32_t entry);
+static void ringSilence(MidiSinkT *sink);
+static void ringSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len);
+bool seqAdvance(SeqT *seq, uint32_t us, MidiSinkT *sink);
+void seqSeek(SeqT *seq, uint32_t us, MidiSinkT *sink);
+static void seqSend(const SeqT *seq, const MidiEventT *e, MidiSinkT *sink);
+
+
+void eventRingDrain(EventRingT *ring, MidiSinkT *to) {
+ while (ring->tail != ring->head) {
+ uint32_t e = ring->events[ring->tail & (EVENT_RING_SIZE - 1)];
+
+ __atomic_signal_fence(__ATOMIC_ACQUIRE);
+ ring->tail++;
+
+ if (e == EVENT_SILENCE) {
+ to->silence(to);
+ } else {
+ to->message(to, (uint8_t)(e >> RING_STATUS_SHIFT), (uint8_t)((e >> RING_DATA1_SHIFT) & RING_BYTE_MASK), (uint8_t)((e >> RING_DATA2_SHIFT) & RING_BYTE_MASK));
+ }
+ }
+}
+
+
+void eventRingInit(EventRingT *ring) {
+ ring->sink.message = ringMessage;
+ ring->sink.sysex = ringSysex;
+ ring->sink.silence = ringSilence;
+ ring->head = 0;
+ ring->tail = 0;
+ ring->dropped = 0;
+}
+
+
+// Every function of the three files that run at interrupt time
+// (musicFm.c, musicMpu.c, this one) and the player table: one span from
+// the lowest of them to the end of the table, which is the library's
+// last data (the Makefile links these files last).
+bool musicLock(bool lock) {
+ const void *fns[] = {
+ (const void *)eventRingDrain,
+ (const void *)eventRingInit,
+ (const void *)musicLock,
+ (const void *)musicTick,
+ (const void *)ringMessage,
+ (const void *)ringPush,
+ (const void *)ringSilence,
+ (const void *)ringSysex,
+ (const void *)seqAdvance,
+ (const void *)seqSend,
+ (const void *)seqSeek
+ };
+ AudioLockItemT items[LOCK_ITEMS_MAX];
+ int32_t count = (int32_t)(sizeof(fns) / sizeof(fns[0]));
+
+ for (int32_t i = 0; i < count; i++) {
+ items[i].addr = fns[i];
+ items[i].size = 0;
+ }
+
+ count += fmLockItems(&items[count]);
+ count += mpuLockItems(&items[count]);
+ items[count].addr = musicIsrPlayers;
+ items[count].size = sizeof(musicIsrPlayers);
+ count++;
+ return audioLockSpan(items, count, lock);
+}
+
+
+// A player that plays to its end is stopped and silenced here; the end
+// call follows in the idle time (musicIdle). The MPU-401 queue drains
+// even while stopped (its closing messages).
+void musicTick(void *unused) {
+ (void)unused;
+
+ for (int32_t i = 0; i < MUSIC_ISR_PLAYERS_MAX; i++) {
+ IsrPlayerT *p = musicIsrPlayers[i];
+
+ if (!p) {
+ continue;
+ }
+
+ if (p->state == PLAY_PLAYING && !seqAdvance(&p->seq, MUSIC_TICK_US, p->sink)) {
+ p->state = PLAY_STOPPED;
+ p->sink->silence(p->sink);
+ p->ended = true;
+ }
+
+ if (p->mpu) {
+ mpuSinkDrain(p->mpu, MUSIC_TICK_US);
+ }
+ }
+}
+
+
+static void ringMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2) {
+ ringPush((EventRingT *)sink, ((uint32_t)status << RING_STATUS_SHIFT) | ((uint32_t)data1 << RING_DATA1_SHIFT) | ((uint32_t)data2 << RING_DATA2_SHIFT));
+}
+
+
+static void ringPush(EventRingT *ring, uint32_t entry) {
+ if (ring->head - ring->tail >= EVENT_RING_SIZE) {
+ ring->dropped++;
+ return;
+ }
+
+ ring->events[ring->head & (EVENT_RING_SIZE - 1)] = entry;
+ __atomic_signal_fence(__ATOMIC_RELEASE);
+ ring->head++;
+}
+
+
+static void ringSilence(MidiSinkT *sink) {
+ ringPush((EventRingT *)sink, EVENT_SILENCE);
+}
+
+
+// The software synthesizer has no use for system exclusive messages.
+static void ringSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len) {
+ (void)sink;
+ (void)data;
+ (void)len;
+}
+
+
+bool seqAdvance(SeqT *seq, uint32_t us, MidiSinkT *sink) {
+ uint32_t target = seq->now + us < seq->now ? UINT32_MAX : seq->now + us;
+
+ while (seq->next < seq->count && seq->events[seq->next].time <= target) {
+ seqSend(seq, &seq->events[seq->next], sink);
+ seq->next++;
+ }
+
+ seq->now = target;
+
+ if (seq->next < seq->count || seq->now < seq->length) {
+ return true;
+ }
+
+ if (!seq->loop) {
+ seq->now = seq->length;
+ return false;
+ }
+
+ // Start over with the time past the end carried into the new round
+ // (an empty song just stays at the start).
+ uint32_t over = seq->length ? (seq->now - seq->length) % seq->length : 0;
+
+ seq->next = 0;
+ seq->now = 0;
+
+ if (seq->count > 0) {
+ seqAdvance(seq, over, sink);
+ }
+
+ return true;
+}
+
+
+void seqSeek(SeqT *seq, uint32_t us, MidiSinkT *sink) {
+ sink->silence(sink);
+ seq->next = 0;
+
+ while (seq->next < seq->count && seq->events[seq->next].time < us) {
+ const MidiEventT *e = &seq->events[seq->next];
+ uint8_t type = e->status & MIDI_TYPE_MASK;
+
+ if (e->status == MIDI_SYSEX || (type != MIDI_NOTE_ON && type != MIDI_NOTE_OFF)) {
+ seqSend(seq, e, sink);
+ }
+
+ seq->next++;
+ }
+
+ seq->now = us < seq->length ? us : seq->length;
+}
+
+
+static void seqSend(const SeqT *seq, const MidiEventT *e, MidiSinkT *sink) {
+ if (e->status == MIDI_SYSEX) {
+ sink->sysex(sink, seq->sysex + e->sysexAt, e->sysexLen);
+ } else {
+ sink->message(sink, e->status, e->data1, e->data2);
+ }
+}
diff --git a/src/libs/kpunch/music/musicShell.c b/src/libs/kpunch/music/musicShell.c
new file mode 100644
index 00000000..d4f6c443
--- /dev/null
+++ b/src/libs/kpunch/music/musicShell.c
@@ -0,0 +1,48 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicShell.c -- music.lib's hooks into the shell
+//
+// Loaded as a module of the shell: the constructor asks for the shell's
+// idle time (end calls) and to be told when a program ends (its musics
+// are freed then); the destructor frees everything and takes the hooks
+// back. Not in the host tests, which call musicIdle and musicAppEnded
+// directly.
+
+#include "musicInt.h"
+#include "shellApp.h"
+
+static void musicShellInit(void) __attribute__((constructor));
+static void musicShellShutdown(void) __attribute__((destructor));
+
+
+static void musicShellInit(void) {
+ shellRegisterIdle(musicIdle, NULL);
+ shellRegisterAppCleanup(musicAppEnded);
+}
+
+
+static void musicShellShutdown(void) {
+ musicShutdown();
+ shellUnregisterAppCleanup(musicAppEnded);
+ shellUnregisterIdle(musicIdle, NULL);
+}
diff --git a/src/libs/kpunch/music/musicSmf.c b/src/libs/kpunch/music/musicSmf.c
new file mode 100644
index 00000000..fd3410c9
--- /dev/null
+++ b/src/libs/kpunch/music/musicSmf.c
@@ -0,0 +1,473 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicSmf.c -- Standard MIDI Files (type 0 and 1) into one event list
+//
+// A file is a header chunk (MThd: type, tracks, time division) and track
+// chunks (MTrk: events with delta times); a RIFF RMID file holds one in
+// its "data" chunk. Every track is read into (tick, track, order)
+// entries, the entries are sorted into one time line, and tempo changes
+// (from any track) turn ticks into microseconds on the way. Channel
+// messages (with running status), system exclusive messages (F0, and F7
+// escapes) and the tempo and end-of-track meta events are kept; other
+// meta events are skipped. A note on with velocity 0 becomes a note
+// off.
+//
+// Damage: an unknown chunk is skipped; a track that breaks off (short
+// data, a variable-length number over four bytes, a data byte where a
+// status byte must be, a system message a file cannot hold) plays up to
+// where it breaks; a chunk longer than the file is read to the end of the
+// file. Only a missing header or no track at all makes the file
+// unusable. Type 2 files (independent patterns) are not played.
+
+#include "musicInt.h"
+#include "stb_ds.h"
+
+#include
+#include
+
+#define CHUNK_ID_BYTES 4
+#define CHUNK_HEADER_BYTES 8
+#define SMF_HEADER_BYTES 6
+#define SMF_TYPE_MAX 1
+#define SMF_SMPTE_BIT 0x8000
+#define SMF_SMPTE_FPS_SHIFT 8
+#define SMF_SMPTE_TPF_MASK 0xFF
+#define SMF_SMPTE_FPS_NEG 0x100 // the frame rate is stored negated in the high byte
+#define SMF_FPS_DROP 29 // 29.97 frames per second
+#define SMF_FPS_DROP_X100 2997
+#define PERCENT_SCALE 100
+#define RIFF_HEADER_BYTES 12
+#define RIFF_FORM_AT 8
+#define VLQ_BYTES_MAX 4
+#define VLQ_MORE 0x80
+#define META_TEMPO 0x51
+#define META_END_OF_TRACK 0x2F
+#define META_TEMPO_BYTES 3
+#define TEMPO_DEFAULT_US 500000 // 120 beats a minute
+#define US_PER_SEC 1000000ULL
+#define BYTE_BITS 8
+#define NOTE_OFF_VELOCITY 64
+#define SYSTEM_FIRST 0xF0
+
+// One entry while tracks are read
+typedef struct {
+ uint32_t tick;
+ uint32_t track;
+ uint32_t order;
+ uint32_t tempo; // tempo change (microseconds a beat), else 0
+ MidiEventT ev;
+} RawEventT;
+
+typedef struct {
+ const uint8_t *p;
+ const uint8_t *end;
+} SmfCursorT;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static uint32_t be16(const uint8_t *p);
+static uint32_t be32(const uint8_t *p);
+static bool findSmf(const uint8_t *data, uint32_t size, const uint8_t **smf, uint32_t *smfSize);
+static bool idIs(const uint8_t *p, const char *id);
+void midiFileFree(MidiFileT *file);
+bool midiIsFile(const uint8_t *data, uint32_t size);
+int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out);
+static int rawCompare(const void *a, const void *b);
+static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t **sysex);
+static bool readVlq(SmfCursorT *c, uint32_t *value);
+
+
+static uint32_t be16(const uint8_t *p) {
+ return ((uint32_t)p[0] << BYTE_BITS) | p[1];
+}
+
+
+static uint32_t be32(const uint8_t *p) {
+ return (be16(p) << (BYTE_BITS * 2)) | be16(p + 2);
+}
+
+
+// The SMF inside data: the data itself, or a RIFF RMID file's "data"
+// chunk.
+static bool findSmf(const uint8_t *data, uint32_t size, const uint8_t **smf, uint32_t *smfSize) {
+ if (size >= CHUNK_HEADER_BYTES && idIs(data, "MThd")) {
+ *smf = data;
+ *smfSize = size;
+ return true;
+ }
+
+ if (size < RIFF_HEADER_BYTES || !idIs(data, "RIFF") || !idIs(data + RIFF_FORM_AT, "RMID")) {
+ return false;
+ }
+
+ uint32_t at = RIFF_HEADER_BYTES;
+
+ while (at + CHUNK_HEADER_BYTES <= size) {
+ uint32_t len = data[at + 4] | ((uint32_t)data[at + 5] << BYTE_BITS) | ((uint32_t)data[at + 6] << (BYTE_BITS * 2)) | ((uint32_t)data[at + 7] << (BYTE_BITS * 3));
+
+ if (idIs(data + at, "data")) {
+ uint32_t avail = size - at - CHUNK_HEADER_BYTES;
+
+ *smf = data + at + CHUNK_HEADER_BYTES;
+ *smfSize = len < avail ? len : avail;
+ return *smfSize >= CHUNK_HEADER_BYTES && idIs(*smf, "MThd");
+ }
+
+ if (len > size - at - CHUNK_HEADER_BYTES) {
+ break;
+ }
+
+ at += CHUNK_HEADER_BYTES + len + (len & 1);
+ }
+
+ return false;
+}
+
+
+static bool idIs(const uint8_t *p, const char *id) {
+ return memcmp(p, id, CHUNK_ID_BYTES) == 0;
+}
+
+
+void midiFileFree(MidiFileT *file) {
+ free(file->events);
+ free(file->sysex);
+ memset(file, 0, sizeof(*file));
+}
+
+
+bool midiIsFile(const uint8_t *data, uint32_t size) {
+ const uint8_t *smf;
+ uint32_t smfSize;
+
+ return data && findSmf(data, size, &smf, &smfSize);
+}
+
+
+int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out) {
+ const uint8_t *smf;
+ uint32_t smfSize;
+
+ memset(out, 0, sizeof(*out));
+
+ if (!data || !findSmf(data, size, &smf, &smfSize)) {
+ return MUSIC_ERR_FORMAT;
+ }
+
+ uint32_t hdrLen = be32(smf + CHUNK_ID_BYTES);
+
+ if (hdrLen < SMF_HEADER_BYTES || smfSize < CHUNK_HEADER_BYTES + SMF_HEADER_BYTES) {
+ return MUSIC_ERR_CORRUPT;
+ }
+
+ uint32_t type = be16(smf + CHUNK_HEADER_BYTES);
+ uint32_t division = be16(smf + CHUNK_HEADER_BYTES + 4);
+
+ if (type > SMF_TYPE_MAX) {
+ return MUSIC_ERR_FORMAT;
+ }
+
+ // Microseconds per tick: from the tempo (ticks per beat), or fixed
+ // (SMPTE frames per second x ticks per frame).
+ uint64_t smpteTicksX100 = 0;
+
+ if (division & SMF_SMPTE_BIT) {
+ uint32_t fps = SMF_SMPTE_FPS_NEG - (division >> SMF_SMPTE_FPS_SHIFT);
+ uint32_t tpf = division & SMF_SMPTE_TPF_MASK;
+
+ smpteTicksX100 = (uint64_t)(fps == SMF_FPS_DROP ? SMF_FPS_DROP_X100 : fps * PERCENT_SCALE) * tpf;
+ }
+
+ if (division == 0 || (division & SMF_SMPTE_BIT && smpteTicksX100 == 0)) {
+ return MUSIC_ERR_CORRUPT;
+ }
+
+ RawEventT *raw = NULL;
+ uint8_t *sysex = NULL;
+ uint32_t tracks = 0;
+ uint32_t at = CHUNK_HEADER_BYTES + (hdrLen < smfSize - CHUNK_HEADER_BYTES ? hdrLen : smfSize - CHUNK_HEADER_BYTES);
+ uint32_t endTick = 0;
+
+ while (at + CHUNK_HEADER_BYTES <= smfSize) {
+ uint32_t len = be32(smf + at + CHUNK_ID_BYTES);
+ uint32_t avail = smfSize - at - CHUNK_HEADER_BYTES;
+
+ if (len > avail) {
+ len = avail;
+ }
+
+ if (idIs(smf + at, "MTrk")) {
+ SmfCursorT c = { smf + at + CHUNK_HEADER_BYTES, smf + at + CHUNK_HEADER_BYTES + len };
+ uint32_t last = readTrack(c, tracks, &raw, &sysex);
+
+ endTick = last > endTick ? last : endTick;
+ tracks++;
+ }
+
+ at += CHUNK_HEADER_BYTES + len;
+
+ if ((uint32_t)arrlen(raw) > MIDI_EVENTS_MAX) {
+ break;
+ }
+ }
+
+ if (tracks == 0) {
+ arrfree(raw);
+ arrfree(sysex);
+ return MUSIC_ERR_CORRUPT;
+ }
+
+ uint32_t count = (uint32_t)arrlen(raw);
+
+ if (count > 0) {
+ qsort(raw, count, sizeof(RawEventT), rawCompare);
+ }
+
+ uint32_t events = 0;
+
+ for (uint32_t i = 0; i < count; i++) {
+ events += raw[i].tempo ? 0 : 1;
+ }
+
+ out->events = (MidiEventT *)malloc((events ? events : 1) * sizeof(MidiEventT));
+ out->sysex = (uint8_t *)malloc(arrlen(sysex) ? (size_t)arrlen(sysex) : 1);
+
+ if (!out->events || !out->sysex) {
+ arrfree(raw);
+ arrfree(sysex);
+ midiFileFree(out);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ out->sysexBytes = (uint32_t)arrlen(sysex);
+
+ if (out->sysexBytes > 0) {
+ memcpy(out->sysex, sysex, out->sysexBytes);
+ }
+
+ // Ticks to microseconds through the tempo changes (in time order).
+ uint64_t baseUs = 0;
+ uint32_t baseTick = 0;
+ uint32_t tempo = TEMPO_DEFAULT_US;
+
+ for (uint32_t i = 0; i <= count; i++) {
+ uint32_t tick = i < count ? raw[i].tick : (endTick > baseTick ? endTick : baseTick);
+ uint64_t us;
+
+ if (smpteTicksX100) {
+ us = (uint64_t)tick * US_PER_SEC * PERCENT_SCALE / smpteTicksX100;
+ } else {
+ us = baseUs + (uint64_t)(tick - baseTick) * tempo / division;
+ }
+
+ uint32_t time = us > UINT32_MAX ? UINT32_MAX : (uint32_t)us;
+
+ if (i == count) {
+ out->length = time > out->length ? time : out->length;
+ break;
+ }
+
+ if (raw[i].tempo) {
+ baseUs = us;
+ baseTick = tick;
+ tempo = raw[i].tempo;
+ continue;
+ }
+
+ out->events[out->count] = raw[i].ev;
+ out->events[out->count].time = time;
+ out->length = time;
+ out->count++;
+ }
+
+ arrfree(raw);
+ arrfree(sysex);
+ return MUSIC_OK;
+}
+
+
+// Time, then track, then order within the track.
+static int rawCompare(const void *a, const void *b) {
+ const RawEventT *x = (const RawEventT *)a;
+ const RawEventT *y = (const RawEventT *)b;
+
+ if (x->tick != y->tick) {
+ return x->tick < y->tick ? -1 : 1;
+ }
+
+ if (x->track != y->track) {
+ return x->track < y->track ? -1 : 1;
+ }
+
+ return x->order < y->order ? -1 : (x->order > y->order ? 1 : 0);
+}
+
+
+// Append a track's events to raw (system exclusive bytes to sysex);
+// returns the tick where it ends.
+static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t **sysex) {
+ uint32_t tick = 0;
+ uint32_t order = 0;
+ uint8_t running = 0;
+
+ while (c.p < c.end && (uint32_t)arrlen(*raw) <= MIDI_EVENTS_MAX) {
+ uint32_t delta;
+
+ if (!readVlq(&c, &delta) || c.p >= c.end) {
+ break;
+ }
+
+ tick += delta;
+
+ RawEventT e;
+
+ memset(&e, 0, sizeof(e));
+ e.tick = tick;
+ e.track = track;
+ e.order = order++;
+
+ uint8_t status = *c.p;
+
+ if (status & MIDI_STATUS_BIT) {
+ c.p++;
+ } else if (running) {
+ status = running;
+ } else {
+ break;
+ }
+
+ if (status < SYSTEM_FIRST) {
+ uint8_t type = status & MIDI_TYPE_MASK;
+ uint32_t bytes = (type == MIDI_PROGRAM || type == MIDI_CHAN_PRESSURE) ? 1 : 2;
+
+ if (c.end - c.p < (ptrdiff_t)bytes || (c.p[0] & MIDI_STATUS_BIT) || (bytes == 2 && (c.p[1] & MIDI_STATUS_BIT))) {
+ break;
+ }
+
+ running = status;
+ e.ev.status = status;
+ e.ev.data1 = c.p[0];
+ e.ev.data2 = bytes == 2 ? c.p[1] : 0;
+ c.p += bytes;
+
+ if (type == MIDI_NOTE_ON && e.ev.data2 == 0) {
+ e.ev.status = (uint8_t)(MIDI_NOTE_OFF | (status & MIDI_CHANNEL_MASK));
+ e.ev.data2 = NOTE_OFF_VELOCITY;
+ }
+
+ arrput(*raw, e);
+ continue;
+ }
+
+ running = 0;
+
+ if (status == MIDI_META) {
+ uint32_t len;
+
+ if (c.p >= c.end) {
+ break;
+ }
+
+ uint8_t meta = *c.p++;
+
+ if (!readVlq(&c, &len) || len > (uint32_t)(c.end - c.p)) {
+ break;
+ }
+
+ if (meta == META_TEMPO && len == META_TEMPO_BYTES) {
+ e.tempo = ((uint32_t)c.p[0] << (BYTE_BITS * 2)) | ((uint32_t)c.p[1] << BYTE_BITS) | c.p[2];
+
+ if (e.tempo) {
+ arrput(*raw, e);
+ }
+ }
+
+ c.p += len;
+
+ if (meta == META_END_OF_TRACK) {
+ break;
+ }
+
+ continue;
+ }
+
+ if (status != MIDI_SYSEX && status != MIDI_SYSEX_END) {
+ break;
+ }
+
+ uint32_t len;
+
+ if (!readVlq(&c, &len) || len > (uint32_t)(c.end - c.p)) {
+ break;
+ }
+
+ // F0: the message is F0 and the bytes; F7: the bytes as they are.
+ uint32_t total = len + (status == MIDI_SYSEX ? 1 : 0);
+
+ if (total > 0 && total <= MIDI_SYSEX_LEN_MAX) {
+ e.ev.status = MIDI_SYSEX;
+ e.ev.sysexAt = (uint32_t)arrlen(*sysex);
+ e.ev.sysexLen = (uint16_t)total;
+
+ if (status == MIDI_SYSEX) {
+ arrput(*sysex, MIDI_SYSEX);
+ }
+
+ if (len > 0) {
+ memcpy(arraddnptr(*sysex, len), c.p, len);
+ }
+
+ arrput(*raw, e);
+ }
+
+ c.p += len;
+ }
+
+ return tick;
+}
+
+
+// A variable-length number: seven bits a byte, most significant first,
+// the top bit set on all but the last (at most four bytes).
+static bool readVlq(SmfCursorT *c, uint32_t *value) {
+ uint32_t v = 0;
+
+ for (int32_t i = 0; i < VLQ_BYTES_MAX; i++) {
+ if (c->p >= c->end) {
+ return false;
+ }
+
+ uint8_t b = *c->p++;
+
+ v = (v << MIDI_DATA_BITS) | (b & MIDI_DATA_MASK);
+
+ if (!(b & VLQ_MORE)) {
+ *value = v;
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/libs/kpunch/music/musicSynth.c b/src/libs/kpunch/music/musicSynth.c
new file mode 100644
index 00000000..403fab02
--- /dev/null
+++ b/src/libs/kpunch/music/musicSynth.c
@@ -0,0 +1,631 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicSynth.c -- the software FM synthesizer
+//
+// A two-operator FM "chip" in integer arithmetic, programmed through the
+// same channel registers as the real one (AUDIO_FM_*), so General MIDI
+// sounds alike on both (musicFm.c drives either). Our own code (MIT),
+// modelled on how the OPL2 behaves:
+//
+// Operators a 32-bit phase (top 10 bits index one turn); the output is
+// 2^-(log-sine + attenuation), from a quarter-wave log-sine
+// table and an exponential table like the chip's, 13 bits
+// signed; waveforms 0-3 (sine, half sine, absolute sine,
+// quarter sine pulses).
+// Voice the modulator's output shifts the carrier's phase (or, for
+// an additive voice, both are heard); the modulator feeds
+// back on itself ((last two outputs) >> (9 - feedback)).
+// Envelope attenuation in 0.1875 dB units (0..511): attack (the chip's
+// exponential approach), decay to the sustain level, sustain
+// (or on down at the release rate for a non-sustaining
+// operator), release. Rates as the chip's (4 x rate + key
+// scaling), counted in chip ticks (49,716 a second) and
+// applied every SYNTH_EG_FRAMES output frames.
+// Level attenuation (0.75 dB steps) plus key scale level; tremolo
+// (1 dB, 3.7 Hz) and vibrato (the chip's 8-step pattern,
+// 6.1 Hz) where the operator asks for them.
+// Output voices summed, doubled, panned by the feedback register's
+// left / right bits (neither: both), clipped to 16 bits.
+//
+// Program time only (the stream fill function); not pinned.
+
+#include "musicInt.h"
+
+#include
+
+#define SYNTH_EG_FRAMES 16 // envelope and LFO update interval
+#define SYNTH_PHASE_SHIFT 22 // 32-bit phase -> 10-bit index
+#define SYNTH_INDEX_MASK 0x3FF
+#define SYNTH_HALF 0x200 // second half of a turn: negative
+#define SYNTH_QUARTER 0x100 // second and fourth quarters mirror
+#define SYNTH_QUARTER_MASK 0xFF
+#define SYNTH_LOG_SHIFT 3 // attenuation units -> log-sine units
+#define SYNTH_LOG_FRAC 0xFF
+#define SYNTH_LOG_INT_SHIFT 8
+#define SYNTH_LOG_SILENT (13 << SYNTH_LOG_INT_SHIFT) // quieter than one output step
+#define SYNTH_EXP_ONE 1024
+#define SYNTH_ENV_UNITS_MAX 511
+#define SYNTH_FIX_SHIFT 16
+#define SYNTH_FIX_ONE (1 << SYNTH_FIX_SHIFT)
+#define SYNTH_ENV_MAX (SYNTH_ENV_UNITS_MAX << SYNTH_FIX_SHIFT)
+#define SYNTH_ATTACK_SHIFT (SYNTH_FIX_SHIFT + 3) // attack: attenuation / 8 per step
+#define SYNTH_RATE_INSTANT 60 // effective rates from here: instant attack
+#define SYNTH_RATE_MAX 63
+#define SYNTH_RATE_LOW_MASK 3
+#define SYNTH_RATE_HI_SHIFT 2
+#define SYNTH_RATE_BASE 4
+#define SYNTH_TL_SHIFT 2 // 0.75 dB level steps -> 0.1875 dB units
+#define SYNTH_SL_SHIFT 4 // 3 dB sustain steps -> units
+#define SYNTH_SL_MAX_REG 15 // sustain level 15 means 93 dB
+#define SYNTH_SL_MAX_UNITS 496
+#define SYNTH_FB_BASE 9
+#define SYNTH_OUT_GAIN 2 // output doubled
+#define SYNTH_SAMPLE_MAX 32767
+#define SYNTH_SAMPLE_MIN (-32768)
+#define SYNTH_PHASE_FRAC 11 // phase step = fnum * 2^block * mult2 * CHIP_HZ * 2^11 / rate
+
+// Register bits
+#define REG_AM 0x80
+#define REG_VIB 0x40
+#define REG_EGT 0x20
+#define REG_KSR 0x10
+#define REG_MULT_MASK 0x0F
+#define REG_TL_MASK 0x3F
+#define REG_KSL_SHIFT 6
+#define REG_RATE_SHIFT 4
+#define REG_NIBBLE 0x0F
+#define REG_WAVE_MASK 0x03
+#define REG_FB_SHIFT 1
+#define REG_FB_MASK 0x07
+#define REG_FNUM_LOW_MASK 0xFF
+#define REG_FNUM_HI_MASK 0x03
+#define REG_BLOCK_MASK 0x07
+#define REG_OP_MASK 0x1F
+#define REG_GROUP_MASK 0xE0
+#define REG_CARRIER 3
+#define FNUM_BITS_LOW 8
+#define FNUM_NTS_SHIFT 9 // key scaling uses fnum bit 9 (NTS off)
+
+// Key scale level: attenuation by frequency (the chip's table), shifted
+// by the KSL register (none, 3, 1.5, 6 dB an octave)
+#define KSL_FNUM_SHIFT 6
+#define KSL_UNIT_SHIFT 2
+#define KSL_BLOCK_TOP 8
+#define KSL_BLOCK_SHIFT 5
+#define KSL_NONE_SHIFT 31
+
+// LFOs, in chip ticks
+#define TREMOLO_PERIOD 13432 // 3.7 Hz
+#define TREMOLO_DEPTH 6 // units at the peak (about 1 dB)
+#define VIBRATO_STEP_SHIFT 10 // 8 steps of 1024 ticks: 6.07 Hz
+#define VIBRATO_STEPS_MASK 7
+#define VIBRATO_FNUM_SHIFT 7
+#define VIBRATO_RANGE_MASK 7
+
+// Envelope states
+#define EG_OFF 0
+#define EG_ATTACK 1
+#define EG_DECAY 2
+#define EG_SUSTAIN 3
+#define EG_RELEASE 4
+
+// Voice ids: generation * SYNTH_ID_STRIDE + voice + 1
+#define SYNTH_ID_STRIDE 32
+#define SYNTH_GEN_MAX (INT32_MAX / SYNTH_ID_STRIDE - 1)
+
+// -log2(sin((i + 0.5) * pi / 512)) * 256, a quarter turn
+static const uint16_t sLogSin[SYNTH_QUARTER] = {
+ 2137, 1731, 1543, 1419, 1326, 1252, 1190, 1137, 1091, 1050, 1013, 979, 949, 920, 894, 869,
+ 846, 825, 804, 785, 767, 749, 732, 717, 701, 687, 672, 659, 646, 633, 621, 609,
+ 598, 587, 576, 566, 556, 546, 536, 527, 518, 509, 501, 492, 484, 476, 468, 461,
+ 453, 446, 439, 432, 425, 418, 411, 405, 399, 392, 386, 380, 375, 369, 363, 358,
+ 352, 347, 341, 336, 331, 326, 321, 316, 311, 307, 302, 297, 293, 289, 284, 280,
+ 276, 271, 267, 263, 259, 255, 251, 248, 244, 240, 236, 233, 229, 226, 222, 219,
+ 215, 212, 209, 205, 202, 199, 196, 193, 190, 187, 184, 181, 178, 175, 172, 169,
+ 167, 164, 161, 159, 156, 153, 151, 148, 146, 143, 141, 138, 136, 134, 131, 129,
+ 127, 125, 122, 120, 118, 116, 114, 112, 110, 108, 106, 104, 102, 100, 98, 96,
+ 94, 92, 91, 89, 87, 85, 83, 82, 80, 78, 77, 75, 74, 72, 70, 69,
+ 67, 66, 64, 63, 62, 60, 59, 57, 56, 55, 53, 52, 51, 49, 48, 47,
+ 46, 45, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30,
+ 29, 28, 27, 26, 25, 24, 23, 23, 22, 21, 20, 20, 19, 18, 17, 17,
+ 16, 15, 15, 14, 13, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7,
+ 7, 7, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2,
+ 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0};
+
+// (2^(i / 256) - 1) * 1024
+static const uint16_t sExp[SYNTH_QUARTER] = {
+ 0, 3, 6, 8, 11, 14, 17, 20, 22, 25, 28, 31, 34, 37, 40, 42,
+ 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90,
+ 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123, 126, 130, 133, 136, 139,
+ 142, 145, 148, 152, 155, 158, 161, 164, 168, 171, 174, 177, 181, 184, 187, 190,
+ 194, 197, 200, 204, 207, 210, 214, 217, 220, 224, 227, 231, 234, 237, 241, 244,
+ 248, 251, 255, 258, 262, 265, 268, 272, 276, 279, 283, 286, 290, 293, 297, 300,
+ 304, 308, 311, 315, 318, 322, 326, 329, 333, 337, 340, 344, 348, 352, 355, 359,
+ 363, 367, 370, 374, 378, 382, 385, 389, 393, 397, 401, 405, 409, 412, 416, 420,
+ 424, 428, 432, 436, 440, 444, 448, 452, 456, 460, 464, 468, 472, 476, 480, 484,
+ 488, 492, 496, 501, 505, 509, 513, 517, 521, 526, 530, 534, 538, 542, 547, 551,
+ 555, 560, 564, 568, 572, 577, 581, 585, 590, 594, 599, 603, 607, 612, 616, 621,
+ 625, 630, 634, 639, 643, 648, 652, 657, 661, 666, 670, 675, 680, 684, 689, 693,
+ 698, 703, 708, 712, 717, 722, 726, 731, 736, 741, 745, 750, 755, 760, 765, 770,
+ 774, 779, 784, 789, 794, 799, 804, 809, 814, 819, 824, 829, 834, 839, 844, 849,
+ 854, 859, 864, 869, 874, 880, 885, 890, 895, 900, 906, 911, 916, 921, 927, 932,
+ 937, 942, 948, 953, 959, 964, 969, 975, 980, 986, 991, 996, 1002, 1007, 1013, 1018};
+
+// Frequency multiple x 2 (the register's 0 means one half)
+static const uint8_t sMult2[REG_MULT_MASK + 1] = { 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30 };
+
+// Key scale level by the frequency number's top 4 bits (0.75 dB steps)
+static const uint8_t sKslRom[REG_NIBBLE + 1] = { 0, 32, 40, 45, 48, 51, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64 };
+static const uint8_t sKslShift[] = { KSL_NONE_SHIFT, 1, 2, 0 };
+
+// Vibrato: the frequency number moves by its top bits times this pattern,
+// halved (the shallow depth)
+static const int8_t sVibrato[VIBRATO_STEPS_MASK + 1] = { 0, 1, 2, 1, 0, -1, -2, -1 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void egAdvance(SynthOpT *op, uint32_t ticks);
+static uint32_t egRate(uint32_t rate, uint32_t keyScale, bool ksr);
+static uint32_t egStep(uint32_t eff);
+static int32_t opOutput(uint32_t index, uint8_t wave, int32_t att);
+static void opUpdate(const SynthChipT *chip, SynthVoiceT *v, SynthOpT *op, int32_t vibrato);
+static void renderVoice(SynthVoiceT *v, int32_t *acc, uint32_t frames, int32_t tremolo);
+int32_t synthAlloc(SynthChipT *chip);
+static int32_t synthBackAlloc(void *owner);
+void synthBackend(SynthChipT *chip, FmBackendT *out);
+static void synthBackRelease(void *owner, int32_t voice);
+static void synthBackWrite(void *owner, int32_t voice, uint8_t reg, uint8_t value);
+void synthInit(SynthChipT *chip, uint32_t rate, uint32_t voices);
+void synthRelease(SynthChipT *chip, int32_t voice);
+void synthRender(SynthChipT *chip, int16_t *out, uint32_t frames, uint32_t channels);
+void synthWrite(SynthChipT *chip, int32_t voice, uint8_t reg, uint8_t value);
+static void voiceKey(SynthVoiceT *v, bool on);
+static SynthVoiceT *voiceLookup(SynthChipT *chip, int32_t voice);
+
+
+static void egAdvance(SynthOpT *op, uint32_t ticks) {
+ switch (op->egState) {
+ case EG_ATTACK: {
+ uint64_t k = (uint64_t)op->attackStep * ticks;
+ uint64_t dec = ((uint64_t)(op->env + SYNTH_FIX_ONE) * k) >> SYNTH_ATTACK_SHIFT;
+
+ if (op->attackStep == UINT32_MAX || dec >= (uint64_t)op->env) {
+ op->env = 0;
+ op->egState = EG_DECAY;
+ } else {
+ op->env -= (int32_t)dec;
+ }
+
+ break;
+ }
+
+ case EG_DECAY: {
+ int32_t sl = op->sustainLevel << SYNTH_FIX_SHIFT;
+
+ op->env += (int32_t)(op->decayStep * ticks);
+
+ if (op->env >= sl) {
+ op->env = sl;
+ op->egState = EG_SUSTAIN;
+ }
+
+ break;
+ }
+
+ case EG_SUSTAIN:
+ if (!(op->regChar & REG_EGT)) {
+ op->env += (int32_t)(op->releaseStep * ticks);
+ }
+
+ break;
+
+ case EG_RELEASE:
+ op->env += (int32_t)(op->releaseStep * ticks);
+ break;
+
+ default:
+ break;
+ }
+
+ if (op->env >= SYNTH_ENV_MAX && op->egState != EG_ATTACK) {
+ op->env = SYNTH_ENV_MAX;
+ op->egState = EG_OFF;
+ }
+}
+
+
+// The chip's effective rate for a 4-bit rate register: 4 x rate + key
+// scaling (all of it with KSR, else its top two bits), at most 63; 0
+// stays 0.
+static uint32_t egRate(uint32_t rate, uint32_t keyScale, bool ksr) {
+ if (rate == 0) {
+ return 0;
+ }
+
+ uint32_t eff = rate * SYNTH_RATE_BASE + (ksr ? keyScale : keyScale >> SYNTH_RATE_HI_SHIFT);
+
+ return eff > SYNTH_RATE_MAX ? SYNTH_RATE_MAX : eff;
+}
+
+
+// Attenuation units per chip tick (16.16) at an effective rate:
+// (4 + low two bits) x 2^(high bits) / 2^15.
+static uint32_t egStep(uint32_t eff) {
+ if (eff == 0) {
+ return 0;
+ }
+
+ return (SYNTH_RATE_BASE + (eff & SYNTH_RATE_LOW_MASK)) << ((eff >> SYNTH_RATE_HI_SHIFT) + 1);
+}
+
+
+// One output: index (10 bits of a turn), waveform, attenuation (units).
+static int32_t opOutput(uint32_t index, uint8_t wave, int32_t att) {
+ uint32_t i = index & SYNTH_INDEX_MASK;
+ bool negative = (i & SYNTH_HALF) != 0;
+
+ switch (wave) {
+ case 1:
+ if (negative) {
+ return 0;
+ }
+
+ break;
+
+ case 2:
+ negative = false;
+ break;
+
+ case 3:
+ if (i & SYNTH_QUARTER) {
+ return 0;
+ }
+
+ negative = false;
+ break;
+
+ default:
+ break;
+ }
+
+ uint32_t q = (i & SYNTH_QUARTER) ? SYNTH_QUARTER_MASK - (i & SYNTH_QUARTER_MASK) : (i & SYNTH_QUARTER_MASK);
+ int32_t level = sLogSin[q] + (att << SYNTH_LOG_SHIFT);
+
+ if (level >= SYNTH_LOG_SILENT) {
+ return 0;
+ }
+
+ int32_t out = ((sExp[SYNTH_LOG_FRAC - (level & SYNTH_LOG_FRAC)] + SYNTH_EXP_ONE) << 1) >> (level >> SYNTH_LOG_INT_SHIFT);
+
+ return negative ? -out : out;
+}
+
+
+// Phase step, key scale level and envelope rates of an operator from its
+// registers and the voice's frequency (moved by vibrato).
+static void opUpdate(const SynthChipT *chip, SynthVoiceT *v, SynthOpT *op, int32_t vibrato) {
+ uint32_t fnum = v->fnum;
+ uint32_t block = v->block;
+ uint32_t keyScale = (block << 1) | (fnum >> FNUM_NTS_SHIFT);
+ bool ksr = (op->regChar & REG_KSR) != 0;
+
+ if (op->regChar & REG_VIB) {
+ fnum = (uint32_t)((int32_t)fnum + vibrato);
+ }
+
+ op->phaseStep = (uint32_t)((((uint64_t)fnum << block) * sMult2[op->regChar & REG_MULT_MASK] * AUDIO_FM_CHIP_HZ << SYNTH_PHASE_FRAC) / chip->rate);
+
+ int32_t ksl = (sKslRom[v->fnum >> KSL_FNUM_SHIFT] << KSL_UNIT_SHIFT) - (int32_t)((KSL_BLOCK_TOP - block) << KSL_BLOCK_SHIFT);
+ uint8_t sel = op->regLevel >> REG_KSL_SHIFT;
+ uint32_t attack = egRate(op->regAttack >> REG_RATE_SHIFT, keyScale, ksr);
+
+ if (ksl < 0) {
+ ksl = 0;
+ }
+
+ op->levelAtt = ((op->regLevel & REG_TL_MASK) << SYNTH_TL_SHIFT) + (ksl >> sKslShift[sel]);
+ op->attackStep = attack >= SYNTH_RATE_INSTANT ? UINT32_MAX : egStep(attack);
+ op->decayStep = egStep(egRate(op->regAttack & REG_NIBBLE, keyScale, ksr));
+ op->releaseStep = egStep(egRate(op->regSustain & REG_NIBBLE, keyScale, ksr));
+ op->sustainLevel = (op->regSustain >> REG_RATE_SHIFT) == SYNTH_SL_MAX_REG ? SYNTH_SL_MAX_UNITS : (op->regSustain >> REG_RATE_SHIFT) << SYNTH_SL_SHIFT;
+}
+
+
+// Mix frames frames of a voice into acc (stereo pairs).
+static void renderVoice(SynthVoiceT *v, int32_t *acc, uint32_t frames, int32_t tremolo) {
+ SynthOpT *mod = &v->op[0];
+ SynthOpT *car = &v->op[1];
+ uint32_t fb = (v->regFeedback >> REG_FB_SHIFT) & REG_FB_MASK;
+ bool additive = (v->regFeedback & AUDIO_FM_ADDITIVE) != 0;
+ bool left = (v->regFeedback & AUDIO_FM_OUT_LEFT) != 0;
+ bool right = (v->regFeedback & AUDIO_FM_OUT_RIGHT) != 0;
+ int32_t modAtt = (mod->env >> SYNTH_FIX_SHIFT) + mod->levelAtt + ((mod->regChar & REG_AM) ? tremolo : 0);
+ int32_t carAtt = (car->env >> SYNTH_FIX_SHIFT) + car->levelAtt + ((car->regChar & REG_AM) ? tremolo : 0);
+ uint8_t modWave = mod->regWave & REG_WAVE_MASK;
+ uint8_t carWave = car->regWave & REG_WAVE_MASK;
+
+ if (!left && !right) {
+ left = true;
+ right = true;
+ }
+
+ if (modAtt > SYNTH_ENV_UNITS_MAX) {
+ modAtt = SYNTH_ENV_UNITS_MAX;
+ }
+
+ if (carAtt > SYNTH_ENV_UNITS_MAX) {
+ carAtt = SYNTH_ENV_UNITS_MAX;
+ }
+
+ for (uint32_t i = 0; i < frames; i++) {
+ int32_t fbMod = fb ? (mod->out + mod->prevOut) >> (SYNTH_FB_BASE - fb) : 0;
+ int32_t modOut = opOutput((mod->phase >> SYNTH_PHASE_SHIFT) + (uint32_t)fbMod, modWave, modAtt);
+ int32_t carOut = opOutput((car->phase >> SYNTH_PHASE_SHIFT) + (additive ? 0 : (uint32_t)modOut), carWave, carAtt);
+ int32_t s = (additive ? modOut + carOut : carOut) * SYNTH_OUT_GAIN;
+
+ mod->prevOut = mod->out;
+ mod->out = modOut;
+ mod->phase += mod->phaseStep;
+ car->phase += car->phaseStep;
+
+ if (left) {
+ acc[i * 2] += s;
+ }
+
+ if (right) {
+ acc[i * 2 + 1] += s;
+ }
+ }
+}
+
+
+int32_t synthAlloc(SynthChipT *chip) {
+ int32_t best = -1;
+
+ for (uint32_t i = 0; i < chip->count; i++) {
+ const SynthVoiceT *v = &chip->voices[i];
+
+ if (!v->id && (best < 0 || v->freedAt < chip->voices[best].freedAt)) {
+ best = (int32_t)i;
+ }
+ }
+
+ if (best < 0) {
+ return AUDIO_ERR_NO_VOICE;
+ }
+
+ if (++chip->generation > SYNTH_GEN_MAX) {
+ chip->generation = 1;
+ }
+
+ chip->voices[best].id = (int32_t)(chip->generation * SYNTH_ID_STRIDE) + best + 1;
+ return chip->voices[best].id;
+}
+
+
+static int32_t synthBackAlloc(void *owner) {
+ return synthAlloc((SynthChipT *)owner);
+}
+
+
+// The chip as a voice backend for musicFm.c.
+void synthBackend(SynthChipT *chip, FmBackendT *out) {
+ out->alloc = synthBackAlloc;
+ out->release = synthBackRelease;
+ out->write = synthBackWrite;
+ out->owner = chip;
+}
+
+
+static void synthBackRelease(void *owner, int32_t voice) {
+ synthRelease((SynthChipT *)owner, voice);
+}
+
+
+static void synthBackWrite(void *owner, int32_t voice, uint8_t reg, uint8_t value) {
+ synthWrite((SynthChipT *)owner, voice, reg, value);
+}
+
+
+void synthInit(SynthChipT *chip, uint32_t rate, uint32_t voices) {
+ memset(chip, 0, sizeof(*chip));
+ chip->rate = rate;
+ chip->count = voices > SYNTH_VOICES_MAX ? SYNTH_VOICES_MAX : voices;
+ chip->tickStep = (uint32_t)(((uint64_t)AUDIO_FM_CHIP_HZ << SYNTH_FIX_SHIFT) / rate);
+
+ for (uint32_t i = 0; i < chip->count; i++) {
+ for (int32_t o = 0; o < SYNTH_OPS; o++) {
+ chip->voices[i].op[o].env = SYNTH_ENV_MAX;
+ }
+ }
+}
+
+
+// Key off (the release plays on) and free.
+void synthRelease(SynthChipT *chip, int32_t voice) {
+ SynthVoiceT *v = voiceLookup(chip, voice);
+
+ if (v) {
+ voiceKey(v, false);
+ v->id = 0;
+ v->freedAt = ++chip->clock;
+ }
+}
+
+
+void synthRender(SynthChipT *chip, int16_t *out, uint32_t frames, uint32_t channels) {
+ int32_t acc[SYNTH_EG_FRAMES * 2];
+
+ while (frames > 0) {
+ uint32_t n = frames < SYNTH_EG_FRAMES ? frames : SYNTH_EG_FRAMES;
+
+ // Envelopes and LFOs move on by the chip ticks of this block.
+ chip->tickAcc += chip->tickStep * n;
+
+ uint32_t ticks = chip->tickAcc >> SYNTH_FIX_SHIFT;
+
+ chip->tickAcc &= SYNTH_FIX_ONE - 1;
+ chip->tremoloPos = (chip->tremoloPos + ticks) % TREMOLO_PERIOD;
+ chip->vibratoPos += ticks;
+
+ uint32_t half = TREMOLO_PERIOD / 2;
+ uint32_t tri = chip->tremoloPos < half ? chip->tremoloPos : TREMOLO_PERIOD - chip->tremoloPos;
+ int32_t tremolo = (int32_t)(tri * TREMOLO_DEPTH / half);
+ int32_t step = sVibrato[(chip->vibratoPos >> VIBRATO_STEP_SHIFT) & VIBRATO_STEPS_MASK];
+
+ memset(acc, 0, sizeof(acc));
+
+ for (uint32_t i = 0; i < chip->count; i++) {
+ SynthVoiceT *v = &chip->voices[i];
+
+ // Silent: the carrier has ended (and, for an additive
+ // voice, the modulator too).
+ if (v->op[1].egState == EG_OFF && (v->op[0].egState == EG_OFF || !(v->regFeedback & AUDIO_FM_ADDITIVE))) {
+ continue;
+ }
+
+ int32_t vibrato = (int32_t)(((v->fnum >> VIBRATO_FNUM_SHIFT) & VIBRATO_RANGE_MASK) * step) / 2;
+
+ for (int32_t o = 0; o < SYNTH_OPS; o++) {
+ egAdvance(&v->op[o], ticks);
+
+ if (v->op[o].regChar & REG_VIB) {
+ opUpdate(chip, v, &v->op[o], vibrato);
+ }
+ }
+
+ renderVoice(v, acc, n, tremolo);
+ }
+
+ for (uint32_t i = 0; i < n; i++) {
+ int32_t l = acc[i * 2];
+ int32_t r = acc[i * 2 + 1];
+
+ l = l > SYNTH_SAMPLE_MAX ? SYNTH_SAMPLE_MAX : (l < SYNTH_SAMPLE_MIN ? SYNTH_SAMPLE_MIN : l);
+ r = r > SYNTH_SAMPLE_MAX ? SYNTH_SAMPLE_MAX : (r < SYNTH_SAMPLE_MIN ? SYNTH_SAMPLE_MIN : r);
+
+ if (channels == 2) {
+ *out++ = (int16_t)l;
+ *out++ = (int16_t)r;
+ } else {
+ *out++ = (int16_t)((l + r) / 2);
+ }
+ }
+
+ frames -= n;
+ }
+}
+
+
+void synthWrite(SynthChipT *chip, int32_t voice, uint8_t reg, uint8_t value) {
+ SynthVoiceT *v = voiceLookup(chip, voice);
+
+ if (!v) {
+ return;
+ }
+
+ switch (reg) {
+ case AUDIO_FM_FNUM_LOW:
+ v->fnum = (uint16_t)((v->fnum & ~REG_FNUM_LOW_MASK) | value);
+ break;
+
+ case AUDIO_FM_KEY_BLOCK:
+ v->fnum = (uint16_t)((v->fnum & REG_FNUM_LOW_MASK) | ((value & REG_FNUM_HI_MASK) << FNUM_BITS_LOW));
+ v->block = (value >> AUDIO_FM_BLOCK_SHIFT) & REG_BLOCK_MASK;
+ voiceKey(v, (value & AUDIO_FM_KEY_ON) != 0);
+ break;
+
+ case AUDIO_FM_FEEDBACK:
+ v->regFeedback = value;
+ return;
+
+ default: {
+ SynthOpT *op = &v->op[(reg & REG_OP_MASK) == REG_CARRIER ? 1 : 0];
+
+ switch (reg & REG_GROUP_MASK) {
+ case AUDIO_FM_MOD_CHAR:
+ op->regChar = value;
+ break;
+
+ case AUDIO_FM_MOD_LEVEL:
+ op->regLevel = value;
+ break;
+
+ case AUDIO_FM_MOD_ATTACK:
+ op->regAttack = value;
+ break;
+
+ case AUDIO_FM_MOD_SUSTAIN:
+ op->regSustain = value;
+ break;
+
+ case AUDIO_FM_MOD_WAVE:
+ op->regWave = value;
+ break;
+
+ default:
+ return;
+ }
+
+ break;
+ }
+ }
+
+ opUpdate(chip, v, &v->op[0], 0);
+ opUpdate(chip, v, &v->op[1], 0);
+}
+
+
+// Key on starts both operators' attack from where their envelopes are
+// and restarts their phase; key off releases them.
+static void voiceKey(SynthVoiceT *v, bool on) {
+ if (on == v->keyOn) {
+ return;
+ }
+
+ v->keyOn = on;
+
+ for (int32_t o = 0; o < SYNTH_OPS; o++) {
+ SynthOpT *op = &v->op[o];
+
+ if (on) {
+ op->phase = 0;
+ op->egState = EG_ATTACK;
+ } else if (op->egState != EG_OFF) {
+ op->egState = EG_RELEASE;
+ }
+ }
+}
+
+
+static SynthVoiceT *voiceLookup(SynthChipT *chip, int32_t voice) {
+ if (voice <= 0) {
+ return NULL;
+ }
+
+ uint32_t i = (uint32_t)(voice - 1) % SYNTH_ID_STRIDE;
+
+ return i < chip->count && chip->voices[i].id == voice ? &chip->voices[i] : NULL;
+}
diff --git a/src/libs/kpunch/music/musicXmp.c b/src/libs/kpunch/music/musicXmp.c
new file mode 100644
index 00000000..95104843
--- /dev/null
+++ b/src/libs/kpunch/music/musicXmp.c
@@ -0,0 +1,164 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// musicXmp.c -- tracker modules (MOD, S3M, XM, IT) through libxmp-lite
+//
+// libxmp-lite (thirdparty/libxmp-lite, unmodified) loads the module from
+// memory and renders 16-bit frames at the stream's rate with linear
+// interpolation; the stream's fill function calls xmpRender at program
+// time (libxmp uses floating point). xmp_play_buffer's loop count ends
+// a song that does not loop after one play.
+
+#include "musicInt.h"
+#include "xmp.h"
+
+#include
+#include
+#include
+
+#define XMP_DEFAULT_PAN 50 // percent of full separation (softer than the Amiga's hard left / right)
+#define XMP_LOOP_ONCE 1
+#define XMP_LOOP_FOREVER 0
+#define XMP_BYTES_16 2
+
+struct XmpPlayerT {
+ xmp_context ctx;
+ uint32_t channels;
+ char title[MUSIC_TITLE_MAX];
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void xmpClose(XmpPlayerT *player);
+uint32_t xmpLength(XmpPlayerT *player);
+int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out);
+uint32_t xmpPosition(XmpPlayerT *player);
+bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop);
+void xmpRestart(XmpPlayerT *player);
+void xmpSeek(XmpPlayerT *player, uint32_t ms);
+const char *xmpTitle(XmpPlayerT *player);
+
+
+void xmpClose(XmpPlayerT *player) {
+ if (!player) {
+ return;
+ }
+
+ xmp_end_player(player->ctx);
+ xmp_release_module(player->ctx);
+ xmp_free_context(player->ctx);
+ free(player);
+}
+
+
+uint32_t xmpLength(XmpPlayerT *player) {
+ struct xmp_frame_info fi;
+
+ xmp_get_frame_info(player->ctx, &fi);
+ return fi.total_time > 0 ? (uint32_t)fi.total_time : 0;
+}
+
+
+int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out) {
+ *out = NULL;
+
+ XmpPlayerT *p = (XmpPlayerT *)calloc(1, sizeof(XmpPlayerT));
+
+ if (!p) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ p->ctx = xmp_create_context();
+ p->channels = channels;
+
+ if (!p->ctx) {
+ free(p);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ xmp_set_player(p->ctx, XMP_PLAYER_DEFPAN, XMP_DEFAULT_PAN);
+
+ int rc = xmp_load_module_from_memory(p->ctx, data, (long)size);
+
+ if (rc != 0) {
+ xmp_free_context(p->ctx);
+ free(p);
+
+ if (rc == -XMP_ERROR_FORMAT) {
+ return MUSIC_ERR_FORMAT;
+ }
+
+ return rc == -XMP_ERROR_SYSTEM ? MUSIC_ERR_NO_MEMORY : MUSIC_ERR_CORRUPT;
+ }
+
+ if (xmp_start_player(p->ctx, (int)rate, channels == 1 ? XMP_FORMAT_MONO : 0) != 0) {
+ xmp_release_module(p->ctx);
+ xmp_free_context(p->ctx);
+ free(p);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ xmp_set_player(p->ctx, XMP_PLAYER_INTERP, XMP_INTERP_LINEAR);
+
+ struct xmp_module_info mi;
+
+ xmp_get_module_info(p->ctx, &mi);
+ snprintf(p->title, sizeof(p->title), "%s", mi.mod ? mi.mod->name : "");
+ *out = p;
+ return MUSIC_OK;
+}
+
+
+uint32_t xmpPosition(XmpPlayerT *player) {
+ struct xmp_frame_info fi;
+
+ xmp_get_frame_info(player->ctx, &fi);
+ return fi.time > 0 ? (uint32_t)fi.time : 0;
+}
+
+
+bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) {
+ int bytes = (int)(frames * player->channels * XMP_BYTES_16);
+ int rc = xmp_play_buffer(player->ctx, out, bytes, loop ? XMP_LOOP_FOREVER : XMP_LOOP_ONCE);
+
+ return rc == 0;
+}
+
+
+// Back to the start, with the loop count and the render buffer reset.
+void xmpRestart(XmpPlayerT *player) {
+ xmp_restart_module(player->ctx);
+ xmp_play_buffer(player->ctx, NULL, 0, 0);
+}
+
+
+void xmpSeek(XmpPlayerT *player, uint32_t ms) {
+ xmp_seek_time(player->ctx, (int)ms);
+ xmp_play_buffer(player->ctx, NULL, 0, 0);
+}
+
+
+const char *xmpTitle(XmpPlayerT *player) {
+ return player->title;
+}
diff --git a/src/libs/kpunch/music/thirdparty/genmidi/DVX-VERSION.md b/src/libs/kpunch/music/thirdparty/genmidi/DVX-VERSION.md
new file mode 100644
index 00000000..82382394
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/genmidi/DVX-VERSION.md
@@ -0,0 +1,31 @@
+# Freedoom GENMIDI instrument bank for DVX
+
+| Item | Value |
+|---|---|
+| Project | Freedoom, https://freedoom.github.io/ (bank sources: lumps/genmidi in https://github.com/freedoom/freedoom) |
+| Version | 0.13.0 |
+| Download | https://github.com/freedoom/freedoom/releases/download/v0.13.0/freedoom-0.13.0.zip |
+| Zip SHA-256 | 3f9b264f3e3ce503b4fb7f6bdcb1f419d93c7b546f4df3e874dd878db9688f59 (as in the release's signed freedoom-0.13.0-CHECKSUM) |
+| File kept | GENMIDI.OP2: the GENMIDI lump of freedoom2.wad (the same bytes are in freedoom1.wad), 11,908 bytes, SHA-256 0bea035cca1ef715c8bbbc00a7eb207084f0f8c47f8900dc053e9b1923a7d770 |
+| Format | OP2 (DMX GENMIDI): "#OPL_II#", 175 instruments of 36 bytes (128 General MIDI programs, 47 percussion notes 35..81), 175 names of 32 bytes |
+| Licence | BSD 3-clause (Freedoom's COPYING.txt, in LICENSE here with its non-ASCII characters written as ASCII). Freedoom accepts only original content; the bank is built from SBI instrument files by the project's own tools. Reproduced in the music library's developer help (topic lib.music). |
+| Patches | None |
+
+## How it was extracted
+
+The WAD directory (little-endian: "PWAD", lump count, directory offset;
+16-byte entries of offset, size, 8-byte name) was read with a few lines of
+Python and the lump named GENMIDI copied out unchanged.
+
+## Other banks
+
+The bank is installed as bin/libs/kpunch/music/genmidi.op2. A user can
+use any other OP2-format bank (for example DMXOPL, MIT, from
+https://github.com/sneakernets/DMXOPL, file GENMIDI.op2) by copying it
+into the DVX folder and naming it in DVX.INI:
+
+ [audio]
+ midiBank = CONFIG\MYBANK.OP2
+
+DVX does not ship DMXOPL (a few of its instruments may come from id
+Software's original bank).
diff --git a/src/libs/kpunch/music/thirdparty/genmidi/GENMIDI.OP2 b/src/libs/kpunch/music/thirdparty/genmidi/GENMIDI.OP2
new file mode 100644
index 00000000..a931371f
Binary files /dev/null and b/src/libs/kpunch/music/thirdparty/genmidi/GENMIDI.OP2 differ
diff --git a/src/libs/kpunch/music/thirdparty/genmidi/LICENSE b/src/libs/kpunch/music/thirdparty/genmidi/LICENSE
new file mode 100644
index 00000000..eec45188
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/genmidi/LICENSE
@@ -0,0 +1,30 @@
+Copyright (C) 2001-2024
+Contributors to the Freedoom project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the Freedoom project nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+For a list of contributors to the Freedoom project, see the file
+CREDITS.
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
new file mode 100644
index 00000000..0f51b70f
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
@@ -0,0 +1,37 @@
+# libxmp-lite for DVX
+
+| Item | Value |
+|---|---|
+| Project | libxmp (Extended Module Player library), lite edition, https://github.com/libxmp/libxmp |
+| Version | 4.7.3 (released 2026-09-11) |
+| Download | https://github.com/libxmp/libxmp/releases/download/libxmp-4.7.3/libxmp-lite-4.7.3.tar.gz |
+| Tarball SHA-256 | e199da3f7552f5ac688a091b8d28ea3c9ffd2beb845d15aac17d4d264f851d71 (262,671 bytes) |
+| Files kept | src/*.c, src/*.h, src/loaders/*.c, src/loaders/*.h, include/libxmp-lite/xmp.h (as include/xmp.h), README (it holds the licence), test/test.it and test/test.xm (used by the host tests) -- byte for byte as in the tarball. Not kept: the build files (configure, CMake, the Makefiles, including src/Makefile and src/loaders/Makefile), jni, the rest of test/. |
+| Licence | MIT ("Extended Module Player Lite, Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr", in README). Keep the notice with the source; reproduced in the music library's developer help (topic lib.music) for binaries. |
+| Patches | None |
+
+## Build options
+
+Set in `src/libs/kpunch/music/Makefile` (DOS) and `src/test/music/Makefile`
+(host test), the only two places:
+
+- `-DLIBXMP_CORE_PLAYER` (MOD, S3M, XM, IT only), `-DLIBXMP_NO_PROWIZARD`,
+ `-DLIBXMP_NO_DEPACKERS`, `-DBUILDING_STATIC`.
+- Warnings off (`-w`; the sources are not built with DVX's warning set),
+ DOS `-O2 -march=i486`, plus `-fno-analyzer`.
+- The objects are linked first into music.lib, so the span music.lib pins
+ for its interrupt-time code (linked last) does not include them.
+
+libxmp is only used at program time (it uses floating point). Modules are
+loaded from memory (`xmp_load_module_from_memory`); `src/filetype.c`
+(DJGPP branch) calls `_chmod`, which the DVX loader exports for it
+(dvxPlatformDos.c), so the file is built unchanged.
+
+## Updating
+
+1. Download the new libxmp-lite tarball, check it against the published
+ release, record the version and SHA-256 here.
+2. Replace src/, include/xmp.h, README and the two test modules.
+3. Rebuild music.lib; run the music host suite (`make -C src/test/music`,
+ `UPDATE_GOLDENS=1` if the module render hashes change after a check by
+ ear) and the music DOS scripts (src/test/dos/scripts/mus*.scr).
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/README b/src/libs/kpunch/music/thirdparty/libxmp-lite/README
new file mode 100644
index 00000000..1c5aa5b2
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/README
@@ -0,0 +1,42 @@
+ __ _ __ ___ __
+ / / (_) / __ __ __ _ ___ ____/ (_) /____
+ / /__/ / _ \\ \ // ' \/ _ \/___/ / / __/ -_)
+/____/_/_.__/_\_\/_/_/_/ .__/ /_/_/\__/\__/
+ /_/
+
+Libxmp-lite is a lean and lightweight subset of Libxmp that plays MOD, S3M,
+XM, and IT modules and retains full compatibility with the original API.
+It's intended for games and small or embedded applications where module
+format diversity and file depacking are not required.
+
+Library size can be further reduced by disabling Impulse Tracker format
+support (configure with --disable-it). This option will also disable IT
+effects and lowpass filtering.
+
+Please refer to http://xmp.sf.net/libxmp.html for details on the current
+Libxmp API.
+
+
+LICENSE
+
+Extended Module Player Lite
+Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
new file mode 100644
index 00000000..9f7aa781
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
@@ -0,0 +1,416 @@
+#ifndef XMP_H
+#define XMP_H
+
+#if defined(EMSCRIPTEN)
+# include
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define XMP_VERSION "4.7.3"
+#define XMP_VERCODE 0x040703
+#define XMP_VER_MAJOR 4
+#define XMP_VER_MINOR 7
+#define XMP_VER_RELEASE 3
+
+#if defined(_WIN32) && !defined(__CYGWIN__)
+# if defined(LIBXMP_STATIC)
+# define LIBXMP_EXPORT
+# elif defined(BUILDING_DLL)
+# define LIBXMP_EXPORT __declspec(dllexport)
+# else
+# define LIBXMP_EXPORT __declspec(dllimport)
+# endif
+#elif defined(__OS2__) && defined(__WATCOMC__)
+# if defined(LIBXMP_STATIC)
+# define LIBXMP_EXPORT
+# elif defined(BUILDING_DLL)
+# define LIBXMP_EXPORT __declspec(dllexport)
+# else
+# define LIBXMP_EXPORT
+# endif
+#elif (defined(__GNUC__) || defined(__clang__) || defined(__HP_cc)) && defined(XMP_SYM_VISIBILITY)
+# if defined(LIBXMP_STATIC)
+# define LIBXMP_EXPORT
+# else
+# define LIBXMP_EXPORT __attribute__((visibility("default")))
+# endif
+#elif defined(__SUNPRO_C) && defined(XMP_LDSCOPE_GLOBAL)
+# if defined(LIBXMP_STATIC)
+# define LIBXMP_EXPORT
+# else
+# define LIBXMP_EXPORT __global
+# endif
+#elif defined(EMSCRIPTEN)
+# define LIBXMP_EXPORT EMSCRIPTEN_KEEPALIVE
+# define LIBXMP_EXPORT_VAR
+#else
+# define LIBXMP_EXPORT
+#endif
+
+#if !defined(LIBXMP_EXPORT_VAR)
+# define LIBXMP_EXPORT_VAR LIBXMP_EXPORT
+#endif
+
+#define XMP_NAME_SIZE 64 /* Size of module name and type */
+
+#define XMP_KEY_OFF 0x81 /* Note number for key off event */
+#define XMP_KEY_CUT 0x82 /* Note number for key cut event */
+#define XMP_KEY_FADE 0x83 /* Note number for fade event */
+
+/* mixer parameter macros */
+
+/* sample format flags */
+#define XMP_FORMAT_8BIT (1 << 0) /* Mix to 8-bit instead of 16 */
+#define XMP_FORMAT_UNSIGNED (1 << 1) /* Mix to unsigned samples */
+#define XMP_FORMAT_MONO (1 << 2) /* Mix to mono instead of stereo */
+#define XMP_FORMAT_32BIT (1 << 3) /* Mix to 32-bit int instead of 16 */
+
+/* player parameters */
+#define XMP_PLAYER_AMP 0 /* Amplification factor */
+#define XMP_PLAYER_MIX 1 /* Stereo mixing */
+#define XMP_PLAYER_INTERP 2 /* Interpolation type */
+#define XMP_PLAYER_DSP 3 /* DSP effect flags */
+#define XMP_PLAYER_FLAGS 4 /* Player flags */
+#define XMP_PLAYER_CFLAGS 5 /* Player flags for current module */
+#define XMP_PLAYER_SMPCTL 6 /* Sample control flags */
+#define XMP_PLAYER_VOLUME 7 /* Player module volume */
+#define XMP_PLAYER_STATE 8 /* Internal player state (read only) */
+#define XMP_PLAYER_SMIX_VOLUME 9 /* SMIX volume */
+#define XMP_PLAYER_DEFPAN 10 /* Default pan setting */
+#define XMP_PLAYER_MODE 11 /* Player personality */
+#define XMP_PLAYER_MIXER_TYPE 12 /* Current mixer (read only) */
+#define XMP_PLAYER_VOICES 13 /* Maximum number of mixer voices */
+
+/* interpolation types */
+#define XMP_INTERP_NEAREST 0 /* Nearest neighbor */
+#define XMP_INTERP_LINEAR 1 /* Linear (default) */
+#define XMP_INTERP_SPLINE 2 /* Cubic spline */
+
+/* dsp effect types */
+#define XMP_DSP_LOWPASS (1 << 0) /* Lowpass filter effect */
+#define XMP_DSP_ALL (XMP_DSP_LOWPASS)
+
+/* player state */
+#define XMP_STATE_UNLOADED 0 /* Context created */
+#define XMP_STATE_LOADED 1 /* Module loaded */
+#define XMP_STATE_PLAYING 2 /* Module playing */
+
+/* player flags */
+#define XMP_FLAGS_VBLANK (1 << 0) /* Use vblank timing */
+#define XMP_FLAGS_FX9BUG (1 << 1) /* Emulate FX9 bug */
+#define XMP_FLAGS_FIXLOOP (1 << 2) /* Emulate sample loop bug */
+#define XMP_FLAGS_A500 (1 << 3) /* Use Paula mixer in Amiga modules */
+
+/* player modes */
+#define XMP_MODE_AUTO 0 /* Autodetect mode (default) */
+#define XMP_MODE_MOD 1 /* Play as a generic MOD player */
+#define XMP_MODE_NOISETRACKER 2 /* Play using Noisetracker quirks */
+#define XMP_MODE_PROTRACKER 3 /* Play using Protracker quirks */
+#define XMP_MODE_S3M 4 /* Play as a generic S3M player */
+#define XMP_MODE_ST3 5 /* Play using ST3 bug emulation */
+#define XMP_MODE_ST3GUS 6 /* Play using ST3+GUS quirks */
+#define XMP_MODE_XM 7 /* Play as a generic XM player */
+#define XMP_MODE_FT2 8 /* Play using FT2 bug emulation */
+#define XMP_MODE_IT 9 /* Play using IT quirks */
+#define XMP_MODE_ITSMP 10 /* Play using IT sample mode quirks */
+
+/* mixer types */
+#define XMP_MIXER_STANDARD 0 /* Standard mixer */
+#define XMP_MIXER_A500 1 /* Amiga 500 */
+#define XMP_MIXER_A500F 2 /* Amiga 500 with led filter */
+
+/* sample flags */
+#define XMP_SMPCTL_SKIP (1 << 0) /* Don't load samples */
+
+/* limits */
+#define XMP_MAX_KEYS 121 /* Number of valid keys */
+#define XMP_MAX_ENV_POINTS 32 /* Max number of envelope points */
+#define XMP_MAX_MOD_LENGTH 256 /* Max number of patterns in module */
+#define XMP_MAX_CHANNELS 64 /* Max number of channels in module */
+#define XMP_MAX_SRATE 768000 /* max sampling rate (Hz) */
+#define XMP_MIN_SRATE 4000 /* min sampling rate (Hz) */
+#define XMP_MIN_BPM 20 /* min BPM */
+/* frame rate = (50 * bpm / 125) Hz */
+/* frame size = (sampling rate * channels * size) / frame rate */
+#define XMP_MAX_FRAMESIZE (5 * XMP_MAX_SRATE * 2 / XMP_MIN_BPM)
+
+/* error codes */
+#define XMP_END 1
+#define XMP_ERROR_INTERNAL 2 /* Internal error */
+#define XMP_ERROR_FORMAT 3 /* Unsupported module format */
+#define XMP_ERROR_LOAD 4 /* Error loading file */
+#define XMP_ERROR_DEPACK 5 /* Error depacking file */
+#define XMP_ERROR_SYSTEM 6 /* System error */
+#define XMP_ERROR_INVALID 7 /* Invalid parameter */
+#define XMP_ERROR_STATE 8 /* Invalid player state */
+
+struct xmp_channel {
+ int pan; /* Channel pan (0x80 is center) */
+ int vol; /* Channel volume */
+#define XMP_CHANNEL_SYNTH (1 << 0) /* Channel is synthesized */
+#define XMP_CHANNEL_MUTE (1 << 1) /* Channel is muted */
+#define XMP_CHANNEL_SPLIT (1 << 2) /* Split Amiga channel in bits 5-4 */
+#define XMP_CHANNEL_SURROUND (1 << 4) /* Surround channel */
+ int flg; /* Channel flags */
+};
+
+struct xmp_pattern {
+ int rows; /* Number of rows */
+ int index[1]; /* Track index */
+};
+
+struct xmp_event {
+ unsigned char note; /* Note number (0 means no note) */
+ unsigned char ins; /* Patch number */
+ unsigned char vol; /* Volume (0 to basevol) */
+ unsigned char fxt; /* Effect type */
+ unsigned char fxp; /* Effect parameter */
+ unsigned char f2t; /* Secondary effect type */
+ unsigned char f2p; /* Secondary effect parameter */
+ unsigned char _flag; /* Internal (reserved) flags */
+};
+
+struct xmp_track {
+ int rows; /* Number of rows */
+ struct xmp_event event[1]; /* Event data */
+};
+
+struct xmp_envelope {
+#define XMP_ENVELOPE_ON (1 << 0) /* Envelope is enabled */
+#define XMP_ENVELOPE_SUS (1 << 1) /* Envelope has sustain point */
+#define XMP_ENVELOPE_LOOP (1 << 2) /* Envelope has loop */
+#define XMP_ENVELOPE_FLT (1 << 3) /* Envelope is used for filter */
+#define XMP_ENVELOPE_SLOOP (1 << 4) /* Envelope has sustain loop */
+#define XMP_ENVELOPE_CARRY (1 << 5) /* Don't reset envelope position */
+ int flg; /* Flags */
+ int npt; /* Number of envelope points */
+ int scl; /* Envelope scaling */
+ int sus; /* Sustain start point */
+ int sue; /* Sustain end point */
+ int lps; /* Loop start point */
+ int lpe; /* Loop end point */
+ short data[XMP_MAX_ENV_POINTS * 2];
+};
+
+struct xmp_subinstrument {
+ int vol; /* Default volume */
+ int gvl; /* Global volume */
+#define XMP_INST_NO_DEFAULT_PAN -1
+ int pan; /* Pan */
+ int xpo; /* Transpose */
+ int fin; /* Finetune */
+ int vwf; /* Vibrato waveform */
+ int vde; /* Vibrato depth */
+ int vra; /* Vibrato rate */
+ int vsw; /* Vibrato sweep */
+ int rvv; /* Random volume/pan variation (IT) */
+ int sid; /* Sample number */
+#define XMP_INST_NNA_CUT 0x00
+#define XMP_INST_NNA_CONT 0x01
+#define XMP_INST_NNA_OFF 0x02
+#define XMP_INST_NNA_FADE 0x03
+ int nna; /* New note action */
+#define XMP_INST_DCT_OFF 0x00
+#define XMP_INST_DCT_NOTE 0x01
+#define XMP_INST_DCT_SMP 0x02
+#define XMP_INST_DCT_INST 0x03
+ int dct; /* Duplicate check type */
+#define XMP_INST_DCA_CUT XMP_INST_NNA_CUT
+#define XMP_INST_DCA_OFF XMP_INST_NNA_OFF
+#define XMP_INST_DCA_FADE XMP_INST_NNA_FADE
+ int dca; /* Duplicate check action */
+ int ifc; /* Initial filter cutoff */
+ int ifr; /* Initial filter resonance */
+};
+
+struct xmp_instrument {
+ char name[32]; /* Instrument name */
+ int vol; /* Instrument volume */
+ int nsm; /* Number of samples */
+ int rls; /* Release (fadeout) */
+ struct xmp_envelope aei; /* Amplitude envelope info */
+ struct xmp_envelope pei; /* Pan envelope info */
+ struct xmp_envelope fei; /* Frequency envelope info */
+
+ struct {
+ unsigned char ins; /* Instrument number for each key */
+ signed char xpo; /* Instrument transpose for each key */
+ } map[XMP_MAX_KEYS];
+
+ struct xmp_subinstrument *sub;
+
+ void *extra; /* Extra fields */
+};
+
+struct xmp_sample {
+ char name[32]; /* Sample name */
+ int len; /* Sample length */
+ int lps; /* Loop start */
+ int lpe; /* Loop end */
+#define XMP_SAMPLE_16BIT (1 << 0) /* 16bit sample */
+#define XMP_SAMPLE_LOOP (1 << 1) /* Sample is looped */
+#define XMP_SAMPLE_LOOP_BIDIR (1 << 2) /* Bidirectional sample loop */
+#define XMP_SAMPLE_LOOP_REVERSE (1 << 3) /* Backwards sample loop */
+#define XMP_SAMPLE_LOOP_FULL (1 << 4) /* Play full sample before looping */
+#define XMP_SAMPLE_SLOOP (1 << 5) /* Sample has sustain loop */
+#define XMP_SAMPLE_SLOOP_BIDIR (1 << 6) /* Bidirectional sustain loop */
+#define XMP_SAMPLE_STEREO (1 << 7) /* Interlaced stereo sample */
+#define XMP_SAMPLE_SYNTH (1 << 15) /* Data contains synth patch */
+ int flg; /* Flags */
+ unsigned char *data; /* Sample data */
+};
+
+struct xmp_sequence {
+ int entry_point;
+ int duration;
+};
+
+struct xmp_module {
+ char name[XMP_NAME_SIZE]; /* Module title */
+ char type[XMP_NAME_SIZE]; /* Module format */
+ int pat; /* Number of patterns */
+ int trk; /* Number of tracks */
+ int chn; /* Tracks per pattern */
+ int ins; /* Number of instruments */
+ int smp; /* Number of samples */
+ int spd; /* Initial speed */
+ int bpm; /* Initial BPM */
+ int len; /* Module length in patterns */
+ int rst; /* Restart position */
+ int gvl; /* Global volume */
+
+ struct xmp_pattern **xxp; /* Patterns */
+ struct xmp_track **xxt; /* Tracks */
+ struct xmp_instrument *xxi; /* Instruments */
+ struct xmp_sample *xxs; /* Samples */
+ struct xmp_channel xxc[XMP_MAX_CHANNELS]; /* Channel info */
+
+#define XMP_MARK_SKIP 0xfe /* S3M/IT-only: skip position */
+#define XMP_MARK_END 0xff /* S3M/IT-only: end position */
+ unsigned char xxo[XMP_MAX_MOD_LENGTH]; /* Orders */
+};
+
+struct xmp_test_info {
+ char name[XMP_NAME_SIZE]; /* Module title */
+ char type[XMP_NAME_SIZE]; /* Module format */
+};
+
+struct xmp_module_info {
+ unsigned char md5[16]; /* MD5 message digest */
+ int vol_base; /* Volume scale */
+ struct xmp_module *mod; /* Pointer to module data */
+ char *comment; /* Comment text, if any */
+ int num_sequences; /* Number of valid sequences */
+ struct xmp_sequence *seq_data; /* Pointer to sequence data */
+};
+
+struct xmp_channel_info {
+ unsigned int period; /* Sample period (* 4096) */
+ unsigned int position; /* Sample position */
+ short pitchbend; /* Linear bend from base note*/
+ unsigned char note; /* Current base note number */
+ unsigned char instrument; /* Current instrument number */
+ unsigned char sample; /* Current sample number */
+ unsigned char volume; /* Current volume */
+ unsigned char pan; /* Current stereo pan */
+ unsigned char reserved; /* Reserved */
+ struct xmp_event event; /* Current track event */
+};
+
+struct xmp_frame_info { /* Current frame information */
+ int pos; /* Current position */
+ int pattern; /* Current pattern */
+ int row; /* Current row in pattern */
+ int num_rows; /* Number of rows in current pattern */
+ int frame; /* Current frame */
+ int speed; /* Current replay speed */
+ int bpm; /* Current bpm */
+ int time; /* Current module time in ms */
+ int total_time; /* Estimated replay time in ms*/
+ int frame_time; /* Frame replay time in us */
+ void *buffer; /* Pointer to sound buffer */
+ int buffer_size; /* Used buffer size */
+ int total_size; /* Total buffer size */
+ int volume; /* Current master volume */
+ int loop_count; /* Loop counter */
+ int virt_channels; /* Number of virtual channels */
+ int virt_used; /* Used virtual channels */
+ int sequence; /* Current sequence */
+
+ struct xmp_channel_info channel_info[XMP_MAX_CHANNELS]; /* Current channel information */
+};
+
+struct xmp_callbacks {
+ unsigned long (*read_func)(void *dest, unsigned long len,
+ unsigned long nmemb, void *priv);
+ int (*seek_func)(void *priv, long offset, int whence);
+ long (*tell_func)(void *priv);
+ int (*close_func)(void *priv);
+};
+
+typedef char *xmp_context;
+
+LIBXMP_EXPORT_VAR extern const char *xmp_version;
+LIBXMP_EXPORT_VAR extern const unsigned int xmp_vercode;
+
+LIBXMP_EXPORT int xmp_syserrno (void);
+
+LIBXMP_EXPORT xmp_context xmp_create_context (void);
+LIBXMP_EXPORT void xmp_free_context (xmp_context);
+
+LIBXMP_EXPORT int xmp_load_module (xmp_context, const char *);
+LIBXMP_EXPORT int xmp_load_module_from_memory (xmp_context, const void *, long);
+LIBXMP_EXPORT int xmp_load_module_from_file (xmp_context, void *, long);
+LIBXMP_EXPORT int xmp_load_module_from_callbacks (xmp_context, void *, struct xmp_callbacks);
+
+LIBXMP_EXPORT int xmp_test_module (const char *, struct xmp_test_info *);
+LIBXMP_EXPORT int xmp_test_module_from_memory (const void *, long, struct xmp_test_info *);
+LIBXMP_EXPORT int xmp_test_module_from_file (void *, struct xmp_test_info *);
+LIBXMP_EXPORT int xmp_test_module_from_callbacks (void *, struct xmp_callbacks, struct xmp_test_info *);
+
+LIBXMP_EXPORT void xmp_scan_module (xmp_context);
+LIBXMP_EXPORT void xmp_release_module (xmp_context);
+
+LIBXMP_EXPORT int xmp_start_player (xmp_context, int, int);
+LIBXMP_EXPORT int xmp_play_frame (xmp_context);
+LIBXMP_EXPORT int xmp_play_buffer (xmp_context, void *, int, int);
+LIBXMP_EXPORT void xmp_get_frame_info (xmp_context, struct xmp_frame_info *);
+LIBXMP_EXPORT void xmp_end_player (xmp_context);
+LIBXMP_EXPORT void xmp_inject_event (xmp_context, int, struct xmp_event *);
+LIBXMP_EXPORT void xmp_get_module_info (xmp_context, struct xmp_module_info *);
+LIBXMP_EXPORT const char *const *xmp_get_format_list (void);
+LIBXMP_EXPORT int xmp_next_position (xmp_context);
+LIBXMP_EXPORT int xmp_prev_position (xmp_context);
+LIBXMP_EXPORT int xmp_set_position (xmp_context, int);
+LIBXMP_EXPORT int xmp_set_row (xmp_context, int);
+LIBXMP_EXPORT int xmp_set_tempo_factor(xmp_context, double);
+LIBXMP_EXPORT int xmp_set_tempo_factor_relative(xmp_context, double);
+LIBXMP_EXPORT double xmp_get_tempo_factor(xmp_context);
+LIBXMP_EXPORT double xmp_get_tempo_factor_relative(xmp_context);
+LIBXMP_EXPORT void xmp_stop_module (xmp_context);
+LIBXMP_EXPORT void xmp_restart_module (xmp_context);
+LIBXMP_EXPORT int xmp_seek_time (xmp_context, int);
+LIBXMP_EXPORT int xmp_seek_time_frame (xmp_context, int);
+LIBXMP_EXPORT int xmp_channel_mute (xmp_context, int, int);
+LIBXMP_EXPORT int xmp_channel_vol (xmp_context, int, int);
+LIBXMP_EXPORT int xmp_set_player (xmp_context, int, int);
+LIBXMP_EXPORT int xmp_get_player (xmp_context, int);
+LIBXMP_EXPORT int xmp_set_instrument_path (xmp_context, const char *);
+
+/* External sample mixer API */
+LIBXMP_EXPORT int xmp_start_smix (xmp_context, int, int);
+LIBXMP_EXPORT void xmp_end_smix (xmp_context);
+LIBXMP_EXPORT int xmp_smix_play_instrument(xmp_context, int, int, int, int);
+LIBXMP_EXPORT int xmp_smix_play_sample (xmp_context, int, int, int, int);
+LIBXMP_EXPORT int xmp_smix_channel_pan (xmp_context, int, int);
+LIBXMP_EXPORT int xmp_smix_load_sample (xmp_context, int, const char *);
+LIBXMP_EXPORT int xmp_smix_release_sample (xmp_context, int);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* XMP_H */
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/callbackio.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/callbackio.h
new file mode 100644
index 00000000..de46eb49
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/callbackio.h
@@ -0,0 +1,187 @@
+#ifndef LIBXMP_CALLBACKIO_H
+#define LIBXMP_CALLBACKIO_H
+
+#include
+#include "common.h"
+
+typedef struct {
+ void *priv;
+ struct xmp_callbacks callbacks;
+ int eof;
+} CBFILE;
+
+LIBXMP_BEGIN_DECLS
+
+static inline uint8 cbread8(CBFILE *f, int *err)
+{
+ uint8 x = 0xff;
+ size_t r = f->callbacks.read_func(&x, 1, 1, f->priv);
+ f->eof = (r == 1) ? 0 : EOF;
+
+ if (err) *err = f->eof;
+
+ return x;
+}
+
+static inline int8 cbread8s(CBFILE *f, int *err)
+{
+ return (int8)cbread8(f, err);
+}
+
+static inline uint16 cbread16l(CBFILE *f, int *err)
+{
+ uint8 buf[2];
+ uint16 x = 0xffff;
+ size_t r = f->callbacks.read_func(buf, 2, 1, f->priv);
+ f->eof = (r == 1) ? 0 : EOF;
+
+ if (r) x = readmem16l(buf);
+ if (err) *err = f->eof;
+
+ return x;
+}
+
+static inline uint16 cbread16b(CBFILE *f, int *err)
+{
+ uint8 buf[2];
+ uint16 x = 0xffff;
+ size_t r = f->callbacks.read_func(buf, 2, 1, f->priv);
+ f->eof = (r == 1) ? 0 : EOF;
+
+ if (r) x = readmem16b(buf);
+ if (err) *err = f->eof;
+
+ return x;
+}
+
+static inline uint32 cbread24l(CBFILE *f, int *err)
+{
+ uint8 buf[3];
+ uint32 x = 0xffffffff;
+ size_t r = f->callbacks.read_func(buf, 3, 1, f->priv);
+ f->eof = (r == 1) ? 0 : EOF;
+
+ if (r) x = readmem24l(buf);
+ if (err) *err = f->eof;
+
+ return x;
+}
+
+static inline uint32 cbread24b(CBFILE *f, int *err)
+{
+ uint8 buf[3];
+ uint32 x = 0xffffffff;
+ size_t r = f->callbacks.read_func(buf, 3, 1, f->priv);
+ f->eof = (r == 1) ? 0 : EOF;
+
+ if (r) x = readmem24b(buf);
+ if (err) *err = f->eof;
+
+ return x;
+}
+
+static inline uint32 cbread32l(CBFILE *f, int *err)
+{
+ uint8 buf[4];
+ uint32 x = 0xffffffff;
+ size_t r = f->callbacks.read_func(buf, 4, 1, f->priv);
+ f->eof = (r == 1) ? 0 : EOF;
+
+ if (r) x = readmem32l(buf);
+ if (err) *err = f->eof;
+
+ return x;
+}
+
+static inline uint32 cbread32b(CBFILE *f, int *err)
+{
+ uint8 buf[4];
+ uint32 x = 0xffffffff;
+ size_t r = f->callbacks.read_func(buf, 4, 1, f->priv);
+ f->eof = (r == 1) ? 0 : EOF;
+
+ if (r) x = readmem32b(buf);
+ if (err) *err = f->eof;
+
+ return x;
+}
+
+static inline size_t cbread(void *dest, size_t len, size_t nmemb, CBFILE *f)
+{
+ size_t r = f->callbacks.read_func(dest, (unsigned long)len,
+ (unsigned long)nmemb, f->priv);
+ f->eof = (r < nmemb) ? EOF : 0;
+ return r;
+}
+
+static inline int cbseek(CBFILE *f, long offset, int whence)
+{
+ f->eof = 0;
+ return f->callbacks.seek_func(f->priv, offset, whence);
+}
+
+static inline long cbtell(CBFILE *f)
+{
+ return f->callbacks.tell_func(f->priv);
+}
+
+static inline int cbeof(CBFILE *f)
+{
+ return f->eof;
+}
+
+static inline long cbfilelength(CBFILE *f)
+{
+ long pos = f->callbacks.tell_func(f->priv);
+ long length;
+ int r;
+
+ if (pos < 0)
+ return EOF;
+
+ r = f->callbacks.seek_func(f->priv, 0, SEEK_END);
+ if (r < 0)
+ return EOF;
+
+ length = f->callbacks.tell_func(f->priv);
+ r = f->callbacks.seek_func(f->priv, pos, SEEK_SET);
+
+ return length;
+}
+
+static inline CBFILE *cbopen(void *priv, struct xmp_callbacks callbacks)
+{
+ CBFILE *f;
+ if (priv == NULL || callbacks.read_func == NULL ||
+ callbacks.seek_func == NULL || callbacks.tell_func == NULL)
+ goto err;
+
+ f = (CBFILE *)calloc(1, sizeof(CBFILE));
+ if (f == NULL)
+ goto err;
+
+ f->priv = priv;
+ f->callbacks = callbacks;
+ f->eof = 0;
+ return f;
+
+ err:
+ if (priv && callbacks.close_func)
+ callbacks.close_func(priv);
+
+ return NULL;
+}
+
+static inline int cbclose(CBFILE *f)
+{
+ int r = 0;
+ if (f->callbacks.close_func != NULL)
+ r = f->callbacks.close_func(f->priv);
+
+ free(f);
+ return r;
+}
+
+LIBXMP_END_DECLS
+
+#endif /* LIBXMP_CALLBACKIO_H */
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
new file mode 100644
index 00000000..324ed65e
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
@@ -0,0 +1,735 @@
+#ifndef LIBXMP_COMMON_H
+#define LIBXMP_COMMON_H
+
+/* band-aid for autotools: we aren't using autoheader.
+ * See: https://github.com/libxmp/libxmp/issues/373 . */
+#ifdef AC_APPLE_UNIVERSAL_BUILD
+# undef WORDS_BIGENDIAN
+# if defined __BIG_ENDIAN__
+# define WORDS_BIGENDIAN 1
+# endif
+#endif
+
+#ifdef LIBXMP_CORE_PLAYER
+#ifndef LIBXMP_NO_PROWIZARD
+#define LIBXMP_NO_PROWIZARD
+#endif
+#ifndef LIBXMP_NO_DEPACKERS
+#define LIBXMP_NO_DEPACKERS
+#endif
+#else
+#undef LIBXMP_CORE_DISABLE_IT
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include "xmp.h"
+
+#undef LIBXMP_EXPORT_VAR
+#if defined(EMSCRIPTEN)
+#include
+#define LIBXMP_EXPORT_VAR EMSCRIPTEN_KEEPALIVE
+#else
+#define LIBXMP_EXPORT_VAR
+#endif
+
+#ifndef __cplusplus
+#define LIBXMP_BEGIN_DECLS
+#define LIBXMP_END_DECLS
+#else
+#define LIBXMP_BEGIN_DECLS extern "C" {
+#define LIBXMP_END_DECLS }
+#endif
+
+#if defined(_MSC_VER) && !defined(__cplusplus)
+#define inline __inline
+#endif
+
+#if (defined(__GNUC__) && (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))) ||\
+ (defined(_MSC_VER) && (_MSC_VER >= 1400)) || \
+ (defined(__WATCOMC__) && (__WATCOMC__ >= 1250) && !defined(__cplusplus))
+#define LIBXMP_RESTRICT __restrict
+#else
+#define LIBXMP_RESTRICT
+#endif
+
+#if defined(__MORPHOS__) || defined(__AROS__) || defined(__AMIGA__) \
+ || defined(__amigaos__) || defined(__amigaos4__) || defined(AMIGA)
+#define LIBXMP_AMIGA 1
+#endif
+
+#if defined(_MSC_VER) && defined(__has_include)
+#if __has_include()
+#define HAVE_WINAPIFAMILY_H 1
+#else
+#define HAVE_WINAPIFAMILY_H 0
+#endif
+#endif
+
+#if defined(HAVE_WINAPIFAMILY_H) && HAVE_WINAPIFAMILY_H
+#include
+#define LIBXMP_UWP (!WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) && WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP))
+#else
+#define LIBXMP_UWP 0
+#endif
+
+#ifdef HAVE_EXTERNAL_VISIBILITY
+#define LIBXMP_EXPORT_VERSIONED __attribute__((visibility("default"),externally_visible))
+#else
+#define LIBXMP_EXPORT_VERSIONED __attribute__((visibility("default")))
+#endif
+#ifdef HAVE_ATTRIBUTE_SYMVER
+#define LIBXMP_ATTRIB_SYMVER(_sym) __attribute__((__symver__(_sym)))
+#else
+#define LIBXMP_ATTRIB_SYMVER(_sym)
+#endif
+
+/* AmigaOS fixes by Chris Young , Nov 25, 2007
+ */
+#if defined B_BEOS_VERSION
+# include
+#elif defined __amigaos4__
+# include
+#elif defined _arch_dreamcast /* KallistiOS */
+# include
+#else
+typedef signed char int8;
+typedef signed short int int16;
+typedef signed int int32;
+typedef unsigned char uint8;
+typedef unsigned short int uint16;
+typedef unsigned int uint32;
+#ifdef _MSC_VER /* MSVC6 has no long long */
+typedef signed __int64 int64;
+typedef unsigned __int64 uint64;
+#elif defined(_LP64) || defined(__LP64__)
+typedef unsigned long uint64;
+typedef signed long int64;
+#else
+typedef unsigned long long uint64;
+typedef signed long long int64;
+#endif /* [u]int64 */
+#endif
+/* just in case : */
+typedef int tst_uint32[2 * (4 == sizeof(uint32)) - 1];
+typedef int tst_uint64[2 * (8 == sizeof(uint64)) - 1];
+
+#ifdef _MSC_VER
+#pragma warning(disable:4100) /* unreferenced formal parameter */
+#pragma warning(disable:4389) /* signed/unsigned mismatch ( <, <=, >, >= ) */
+#pragma warning(disable:4018) /* signed/unsigned mismatch ( ==, != ) */
+#pragma warning(disable:4127) /* conditional expression is constant. */
+#pragma warning(disable:4761) /* integral size mismatch in argument; conversion supplied (for MSVC6 and older.) */
+#pragma warning(disable:4244) /* conversion from 'type' to 'int', possible loss of data */
+#pragma warning(disable:4267) /* conversion from 'size_t' to 'type', possible loss of data */
+#endif
+
+#ifndef LIBXMP_CORE_PLAYER
+#define LIBXMP_PAULA_SIMULATOR
+#endif
+
+/* Constants */
+#define PAL_RATE 250.0 /* 1 / (50Hz * 80us) */
+#define NTSC_RATE 208.0 /* 1 / (60Hz * 80us) */
+#define C4_PAL_RATE 8287 /* 7093789.2 / period (C4) * 2 */
+#define C4_NTSC_RATE 8363 /* 7159090.5 / period (C4) * 2 */
+
+/* [Amiga] PAL color carrier frequency (PCCF) = 4.43361825 MHz */
+/* [Amiga] CPU clock = 1.6 * PCCF = 7.0937892 MHz */
+
+#define DEFAULT_AMPLIFY 1
+#define DEFAULT_MIX 100
+
+#define MSN(x) (((x)&0xf0)>>4)
+#define LSN(x) ((x)&0x0f)
+#define SET_FLAG(a,b) ((a)|=(b))
+#define RESET_FLAG(a,b) ((a)&=~(b))
+#define TEST_FLAG(a,b) !!((a)&(b))
+
+/* libxmp_get_filetype() return values */
+#define XMP_FILETYPE_NONE 0
+#define XMP_FILETYPE_DIR (1 << 0)
+#define XMP_FILETYPE_FILE (1 << 1)
+
+#define CLAMP(x,a,b) do { \
+ if ((x) < (a)) (x) = (a); \
+ else if ((x) > (b)) (x) = (b); \
+} while (0)
+#define MIN(x,y) ((x) < (y) ? (x) : (y))
+#define MAX(x,y) ((x) > (y) ? (x) : (y))
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
+
+/* Standards-compliant sign-extending arithmetic left shift.
+ * This is required for potentially negative input values. */
+#define XMP_ASL(x,e) ((int)((unsigned)(int)(x) << (e)))
+#define XMP_ASL64(x,e) ((int64)((uint64)(int64)(x) << (e)))
+
+#define TRACK_NUM(a,c) m->mod.xxp[a]->index[c]
+#define EVENT(a,c,r) m->mod.xxt[TRACK_NUM((a),(c))]->event[r]
+
+#ifdef _MSC_VER
+
+#define D_CRIT " Error: "
+#define D_WARN "Warning: "
+#define D_INFO " Info: "
+void libxmp_msvc_dbgprint(const char *text, ...);
+#ifdef DEBUG
+#define D_ libxmp_msvc_dbgprint /* in win32.c */
+#else
+/* VS prior to VC7.1 does not support variadic macros.
+ * VC8.0 does not optimize unused parameters passing. */
+#if _MSC_VER < 1400
+static void __inline D_(const char *text, ...) {
+ do { } while (0);
+}
+#else
+#define D_(...) do {} while (0)
+#endif
+#endif
+
+#elif defined __ANDROID__
+
+#ifdef DEBUG
+#include
+#define D_CRIT " Error: "
+#define D_WARN "Warning: "
+#define D_INFO " Info: "
+#define D_(...) do { \
+ __android_log_print(ANDROID_LOG_DEBUG, "libxmp", __VA_ARGS__); \
+ } while (0)
+#else
+#define D_(...) do {} while (0)
+#endif
+
+#else
+
+#ifdef DEBUG
+#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
+#define LIBXMP_FUNC __func__
+#else
+#define LIBXMP_FUNC __FUNCTION__
+#endif
+#define D_INFO "\x1b[33m"
+#define D_CRIT "\x1b[31m"
+#define D_WARN "\x1b[36m"
+#if defined(__GNUC__) && (__GNUC__ < 3)
+#define D_(fmt, args...) do { \
+ printf("\x1b[33m%s \x1b[37m[%s:%d] " D_INFO, LIBXMP_FUNC, \
+ __FILE__, __LINE__); printf (fmt, ##args); printf ("\x1b[0m\n"); \
+ } while (0)
+#else /* assume C99 compatibility: */
+#define D_(...) do { \
+ printf("\x1b[33m%s \x1b[37m[%s:%d] " D_INFO, LIBXMP_FUNC, \
+ __FILE__, __LINE__); printf (__VA_ARGS__); printf ("\x1b[0m\n"); \
+ } while (0)
+#endif
+#else
+#if defined(__GNUC__) && (__GNUC__ < 3)
+#define D_(fmt, args...) \
+ do {} while (0)
+#else
+/* assume C99 compatibility: */
+#define D_(...) do {} while (0)
+#endif
+#endif
+
+#endif /* !_MSC_VER */
+
+#if defined(__GNUC__) || defined(__clang__)
+#define LIBXMP_ATTRIB_PRINTF(x,y) __attribute__((__format__(__printf__,x,y)))
+#else
+#define LIBXMP_ATTRIB_PRINTF(x,y)
+#endif
+
+#if defined(_WIN32) || defined(__WATCOMC__) /* in win32.c */
+#define USE_LIBXMP_SNPRINTF
+/* MSVC 2015+ has C99 compliant snprintf and vsnprintf implementations.
+ * If __USE_MINGW_ANSI_STDIO is defined for MinGW (which it is by default),
+ * compliant implementations will be used instead of the broken MSVCRT
+ * functions. Additionally, GCC may optimize some calls to those functions. */
+#if defined(_MSC_VER) && _MSC_VER >= 1900
+#undef USE_LIBXMP_SNPRINTF
+#endif
+#if defined(__MINGW32__) && !defined(__MINGW_FEATURES__)
+#define __MINGW_FEATURES__ 0 /* to avoid -Wundef from old mingw.org headers */
+#endif
+#if defined(__MINGW32__) && defined(__USE_MINGW_ANSI_STDIO) && (__USE_MINGW_ANSI_STDIO != 0)
+#undef USE_LIBXMP_SNPRINTF
+#endif
+#ifdef USE_LIBXMP_SNPRINTF
+int libxmp_vsnprintf(char *, size_t, const char *, va_list) LIBXMP_ATTRIB_PRINTF(3,0);
+int libxmp_snprintf (char *, size_t, const char *, ...) LIBXMP_ATTRIB_PRINTF(3,4);
+#define snprintf libxmp_snprintf
+#define vsnprintf libxmp_vsnprintf
+#endif
+#endif
+
+/* Output file size limit for files unpacked from unarchivers into RAM. Most
+ * general archive compression formats can't nicely bound the output size
+ * from their input filesize, and a cap is needed for a few reasons:
+ *
+ * - Linux is too dumb for its own good and its malloc/realloc will return
+ * pointers to RAM that doesn't exist instead of NULL. When these are used,
+ * it will kill the application instead of allowing it to fail gracefully.
+ * - libFuzzer and the clang sanitizers have malloc/realloc interceptors that
+ * terminate with an error instead of returning NULL.
+ *
+ * Depackers that have better ways of bounding the output size can ignore this.
+ * This value is fairly arbitrary and can be changed if needed.
+ */
+#define LIBXMP_DEPACK_LIMIT (512 << 20)
+
+/* Quirks */
+#define QUIRK_S3MLOOP (1 << 0) /* S3M loop mode */
+#define QUIRK_ENVFADE (1 << 1) /* Fade at end of envelope */
+#define QUIRK_PROTRACK (1 << 2) /* Use Protracker-specific quirks */
+#define QUIRK_RTONCE (1 << 3) /* Retrigger one time only */
+#define QUIRK_ST3BUGS (1 << 4) /* Scream Tracker 3 bug compatibility */
+#define QUIRK_FINEFX (1 << 5) /* Enable 0xf/0xe for fine effects */
+#define QUIRK_VSALL (1 << 6) /* Volume slides in all frames */
+#define QUIRK_PBALL (1 << 7) /* Pitch bending in all frames */
+#define QUIRK_PERPAT (1 << 8) /* Cancel persistent fx at pat start */
+#define QUIRK_VOLPDN (1 << 9) /* Set priority to volume slide down */
+#define QUIRK_UNISLD (1 << 10) /* Unified pitch slide/portamento */
+#define QUIRK_ITVPOR (1 << 11) /* Disable fine bends in IT vol fx */
+/*#define QUIRK_FTMOD (1 << 12)*/ /* Flag for multichannel mods */
+#define QUIRK_INVLOOP (1 << 13) /* Enable invert loop */
+/*#define QUIRK_MODRNG (1 << 13)*/ /* Limit periods to MOD range */
+#define QUIRK_INSVOL (1 << 14) /* Use instrument volume */
+#define QUIRK_VIRTUAL (1 << 15) /* Enable virtual channels */
+#define QUIRK_FILTER (1 << 16) /* Enable filter */
+#define QUIRK_IGSTPOR (1 << 17) /* Ignore stray tone portamento */
+#define QUIRK_KEYOFF (1 << 18) /* Keyoff doesn't reset fadeout */
+#define QUIRK_VIBHALF (1 << 19) /* Vibrato is half as deep */
+#define QUIRK_VIBALL (1 << 20) /* Vibrato in all frames */
+#define QUIRK_VIBINV (1 << 21) /* Vibrato has inverse waveform */
+#define QUIRK_PRENV (1 << 22) /* Portamento resets envelope & fade */
+#define QUIRK_ITOLDFX (1 << 23) /* IT old effects mode */
+#define QUIRK_S3MRTG (1 << 24) /* S3M-style retrig when count == 0 */
+#define QUIRK_FT2ENV (1 << 25) /* Use FT2-style envelope handling */
+#define QUIRK_FT2BUGS (1 << 26) /* FT2 bug compatibility */
+#define QUIRK_MARKER (1 << 27) /* Patterns 0xfe and 0xff reserved */
+#define QUIRK_NOBPM (1 << 28) /* Adjust speed only, no BPM */
+#define QUIRK_ARPMEM (1 << 29) /* Arpeggio has memory (S3M_ARPEGGIO) */
+#define QUIRK_RSTCHN (1 << 30) /* Reset channel on sample end */
+
+#define HAS_QUIRK(x) (m->quirk & (x))
+
+
+/* Format quirks */
+#define QUIRKS_ST3 (QUIRK_S3MLOOP | QUIRK_VOLPDN | QUIRK_FINEFX | \
+ QUIRK_S3MRTG | QUIRK_MARKER | QUIRK_RSTCHN )
+#define QUIRKS_FT2 (0)
+#define QUIRKS_IT (QUIRK_S3MLOOP | QUIRK_FINEFX | QUIRK_VIBALL | \
+ QUIRK_ENVFADE | QUIRK_ITVPOR | QUIRK_KEYOFF | \
+ QUIRK_VIRTUAL | QUIRK_FILTER | QUIRK_RSTCHN | \
+ QUIRK_IGSTPOR | QUIRK_S3MRTG | QUIRK_MARKER )
+
+/* Quirks specific to flow effects, especially Pattern Loop. */
+#define FLOW_LOOP_GLOBAL_TARGET (1 << 0) /* Global target for all tracks */
+#define FLOW_LOOP_GLOBAL_COUNT (1 << 1) /* Global count for all tracks */
+#define FLOW_LOOP_END_ADVANCES (1 << 2) /* Loop end advances target (S3M) */
+#define FLOW_LOOP_END_CANCELS (1 << 3) /* Loop end cancels prev jumps on row (LIQ) */
+#define FLOW_LOOP_PATTERN_RESET (1 << 4) /* Target/count reset on pattern change */
+#define FLOW_LOOP_INIT_SAMEROW (1 << 5) /* SBx sets target if it isn't set (ST 3.01) */
+#define FLOW_LOOP_FIRST_EFFECT (1 << 6) /* Only execute the first E60/E6x in a row */
+#define FLOW_LOOP_ONE_AT_A_TIME (1 << 7) /* Init E6x if no other channel is looping (MPT) */
+#define FLOW_LOOP_IGNORE_TARGET (1 << 8) /* Ignore E60 if count is >=1 (LIQ) */
+#define FLOW_LOOP_DELAY_BREAK (1 << 9) /* E6x jump prevents later Dxx on same row (S3M, IT) */
+#define FLOW_LOOP_DELAY_JUMP (1 << 10) /* E6x jump prevents later Bxx on same row (S3M) */
+#define FLOW_LOOP_UNSET_BREAK (1 << 11) /* E6x jump cancels prior Dxx on same row (S3M, IMF) */
+#define FLOW_LOOP_UNSET_JUMP (1 << 12) /* E6x jump cancels prior Bxx on same row (S3M) */
+#define FLOW_LOOP_SHARED_BREAK (1 << 13) /* E6x overrides prior Dxx dest on same row (LIQ) */
+/*#define FLOW_LOOP_TICK_0_JUMP */ /* Loop jump shortens row to one tick (DTM) */
+#define FLOW_JUMP_THEN_BREAK (1 << 28) /* TODO: Bxx Dxx jumps, then breaks (IMF, TT) */
+#define FLOW_JUMP_QUEUED (1 << 29) /* TODO: Jump queues next position (ST2) */
+#define FLOW_JUMP_NO_ROW_SET (1 << 30) /* Jump doesn't set break row to 0 (ST3/IT) */
+
+#define HAS_FLOW_MODE(x) (m->flow_mode & (x))
+
+#define FLOW_MODE_GENERIC 0
+#define FLOW_LOOP_GLOBAL (FLOW_LOOP_GLOBAL_TARGET | FLOW_LOOP_GLOBAL_COUNT)
+/* Simulate players where all breaks and jumps are ignored during a loop jump. */
+#define FLOW_LOOP_NO_BREAK_JUMP (FLOW_LOOP_DELAY_BREAK | FLOW_LOOP_DELAY_JUMP | \
+ FLOW_LOOP_UNSET_BREAK | FLOW_LOOP_UNSET_JUMP)
+
+/* Scream Tracker 3. No S3Ms seem to rely on the earlier behavior mode.
+ * 3.01b has a bug where the end advancement sets the target to the same line
+ * instead of the next line; there's no way to make use of this without getting
+ * stuck, so it's not simulated.
+ * ST2 has an entirely different behavior for pattern jump where, instead of
+ * immediately jumping, it queues the next position to be loaded after a break
+ * or the end of the current pattern.
+ */
+#define FLOW_MODE_ST2 (FLOW_JUMP_QUEUED)
+#define FLOW_MODE_ST3_301 (FLOW_LOOP_GLOBAL | FLOW_LOOP_PATTERN_RESET | \
+ FLOW_LOOP_END_ADVANCES | FLOW_LOOP_INIT_SAMEROW | \
+ FLOW_JUMP_NO_ROW_SET)
+#define FLOW_MODE_ST3_321 (FLOW_LOOP_GLOBAL | FLOW_LOOP_PATTERN_RESET | \
+ FLOW_LOOP_END_ADVANCES | FLOW_LOOP_NO_BREAK_JUMP | \
+ FLOW_JUMP_NO_ROW_SET)
+
+/* Impulse Tracker. Not clear if anything relies on the old behavior types.
+ * IT loops were global pre-1.04, and loop jumps override any prior break/jump.
+ * IT 2.00+ loop jumps also delay any following break, but not pattern jumps.
+ * IT 2.10+ reintroduced ST3's loop target advancement.
+ */
+#define FLOW_MODE_IT_100 (FLOW_LOOP_GLOBAL | \
+ FLOW_LOOP_UNSET_BREAK | FLOW_LOOP_UNSET_JUMP | \
+ FLOW_JUMP_NO_ROW_SET)
+#define FLOW_MODE_IT_104 (FLOW_LOOP_UNSET_BREAK | FLOW_LOOP_UNSET_JUMP | \
+ FLOW_JUMP_NO_ROW_SET)
+#define FLOW_MODE_IT_200 (FLOW_MODE_IT_104 | FLOW_LOOP_DELAY_BREAK)
+#define FLOW_MODE_IT_210 (FLOW_MODE_IT_200 | FLOW_LOOP_END_ADVANCES)
+
+/* Modplug Tracker/early OpenMPT */
+#define FLOW_MODE_MPT_116 (FLOW_LOOP_ONE_AT_A_TIME | FLOW_LOOP_NO_BREAK_JUMP | \
+ FLOW_JUMP_NO_ROW_SET)
+
+/* Imago Orpheus. Pattern Jump actually does not reset target/count, but all
+ * other forms of pattern change do. Unclear if anything relies on it.
+ * An XAx jump will set the destination row of a prior Txx jump.
+ * An XAx jump will cancel a prior Uxx break on the same row.
+ * Txx unsets a prior Uxx (normal); Txx followed by Uyy jumps and then
+ * breaks to the next pattern after position X.
+ */
+#define FLOW_MODE_ORPHEUS (FLOW_LOOP_PATTERN_RESET | \
+ FLOW_LOOP_SHARED_BREAK | FLOW_LOOP_UNSET_BREAK | \
+ FLOW_JUMP_THEN_BREAK)
+
+/* Liquid Tracker uses generic MOD loops with an added behavior where
+ * the end of a loop will cancel any other jump in the row that preceded it.
+ * M60 is also ignored in channels that have started a loop for some reason.
+ * When M6x jumps, it overrides any prior break line set by Jxx/Cxx.
+ * There is also a "Scream Tracker" compatibility mode (only detectable in the
+ * newer format) that adds LOOP_MODE_PATTERN_RESET.
+ */
+#define FLOW_MODE_LIQUID (FLOW_LOOP_END_CANCELS | FLOW_LOOP_IGNORE_TARGET | \
+ FLOW_LOOP_SHARED_BREAK)
+#define FLOW_MODE_LIQUID_COMPAT (FLOW_MODE_LIQUID | FLOW_LOOP_PATTERN_RESET)
+
+/* Octalyser (Atari). Looping jumps to the original position E60 was used in,
+ * which libxmp doesn't simulate for now since it mostly gets the player stuck.
+ * Octalyser ignores E60 if a loop is currently active; it's not clear if it's
+ * possible for a module to actually rely on this behavior. Loop jumps
+ * interrupt all breaks/jumps on the same row.
+ *
+ * LOOP_MODE_END_CANCELS is inaccurate but needed to fix "Dammed Illusion",
+ * which has multiple E6x on one line that don't trigger because the module
+ * expects to play in 4 channel mode. This quirk only works for this module
+ * because it uses even loop counts, and doesn't break any other modules
+ * because multiple E6x on a row otherwise traps the player.
+ */
+#define FLOW_MODE_OCTALYSER (FLOW_LOOP_GLOBAL | FLOW_LOOP_IGNORE_TARGET | \
+ FLOW_LOOP_END_CANCELS | FLOW_LOOP_NO_BREAK_JUMP)
+
+/* Digital Tracker prior to shareware 1.02 doesn't use LOOP_MODE_FIRST_EFFECT,
+ * but any MOD that would rely on it is impossible to fingerprint.
+ * Early versions had fully working loop jump precedence over jump/break;
+ * later versions gradually break this in ways libxmp only partially implements.
+ * Commercial version 1.9(?) added per-track counters.
+ * Digital Home Studio added a bizarre tick-0 jump bug.
+ */
+#define FLOW_MODE_DTM_2015 (FLOW_LOOP_GLOBAL | FLOW_LOOP_FIRST_EFFECT | \
+ FLOW_LOOP_NO_BREAK_JUMP)
+#define FLOW_MODE_DTM_2015_6CH (FLOW_LOOP_GLOBAL | FLOW_LOOP_FIRST_EFFECT | \
+ FLOW_LOOP_DELAY_BREAK | FLOW_LOOP_UNSET_BREAK | \
+ FLOW_LOOP_SHARED_BREAK)
+#define FLOW_MODE_DTM_19 (FLOW_LOOP_GLOBAL_TARGET | FLOW_LOOP_UNSET_BREAK | \
+ FLOW_LOOP_SHARED_BREAK)
+#define FLOW_MODE_DTM_DHS (FLOW_LOOP_GLOBAL_TARGET | FLOW_LOOP_TICK_0_JUMP)
+
+
+/* DSP effects */
+#define DSP_EFFECT_CUTOFF 0x02
+#define DSP_EFFECT_RESONANCE 0x03
+#define DSP_EFFECT_FILTER_A0 0xb0
+#define DSP_EFFECT_FILTER_B0 0xb1
+#define DSP_EFFECT_FILTER_B1 0xb2
+
+/* Time factor */
+#define DEFAULT_TIME_FACTOR 10.0
+#define MED_TIME_FACTOR 2.64
+#define FAR_TIME_FACTOR 4.01373 /* See far_extras.c */
+
+#define NO_SEQUENCE MAX_SEQUENCES
+#define MAX_SEQUENCES 255
+#define MAX_SAMPLE_SIZE 0x10000000
+#define MAX_SAMPLES 1024
+#define MAX_INSTRUMENTS 255
+#define MAX_PATTERNS 256
+
+#define IS_PLAYER_MODE_MOD() (m->read_event_type == READ_EVENT_MOD)
+#define IS_PLAYER_MODE_FT2() (m->read_event_type == READ_EVENT_FT2)
+#define IS_PLAYER_MODE_ST3() (m->read_event_type == READ_EVENT_ST3)
+#define IS_PLAYER_MODE_IT() (m->read_event_type == READ_EVENT_IT)
+#define IS_PLAYER_MODE_MED() (m->read_event_type == READ_EVENT_MED)
+#define IS_PERIOD_MODRNG() (m->period_type == PERIOD_MODRNG)
+#define IS_PERIOD_LINEAR() (m->period_type == PERIOD_LINEAR)
+#define IS_PERIOD_CSPD() (m->period_type == PERIOD_CSPD)
+
+#define IS_AMIGA_MOD() (IS_PLAYER_MODE_MOD() && IS_PERIOD_MODRNG())
+
+struct ord_data {
+ double time; /* replay time (ms) at start of ord */
+ int speed;
+ int bpm;
+ int gvl;
+ int start_row;
+#ifndef LIBXMP_CORE_PLAYER
+ int st26_speed;
+#endif
+};
+
+
+/* Context */
+
+struct smix_data {
+ int chn;
+ int ins;
+ int smp;
+ struct xmp_instrument *xxi;
+ struct xmp_sample *xxs;
+};
+
+/* This will be added to the sample structure in the next API revision */
+struct extra_sample_data {
+ double c5spd;
+ int sus;
+ int sue;
+};
+
+struct midi_macro {
+ char data[32];
+};
+
+struct midi_macro_data {
+ struct midi_macro param[16];
+ struct midi_macro fixed[128];
+};
+
+struct module_data {
+ struct xmp_module mod;
+
+ char *dirname; /* file dirname */
+ char *basename; /* file basename */
+ const char *filename; /* Module file name */
+ char *comment; /* Comments, if any */
+ uint8 md5[16]; /* MD5 message digest */
+ int size; /* File size */
+ double rrate; /* Replay rate */
+ double time_factor; /* Time conversion constant */
+ int c4rate; /* C4 replay rate */
+ int volbase; /* Volume base */
+ int gvolbase; /* Global volume base */
+ int gvol; /* Global volume */
+ int mvolbase; /* Mix volume base (S3M/IT) */
+ int mvol; /* Mix volume (S3M/IT) */
+ const int *vol_table; /* Volume translation table */
+ int quirk; /* player quirks */
+ int flow_mode; /* Flow quirks, esp. Pattern Loop */
+#define READ_EVENT_MOD 0
+#define READ_EVENT_FT2 1
+#define READ_EVENT_ST3 2
+#define READ_EVENT_IT 3
+#define READ_EVENT_MED 4
+ int read_event_type;
+#define PERIOD_AMIGA 0
+#define PERIOD_MODRNG 1
+#define PERIOD_LINEAR 2
+#define PERIOD_CSPD 3
+ int period_type;
+ int smpctl; /* sample control flags */
+ int defpan; /* default pan setting */
+ struct ord_data xxo_info[XMP_MAX_MOD_LENGTH];
+ int num_sequences;
+ struct xmp_sequence seq_data[MAX_SEQUENCES];
+ char *instrument_path;
+ void *extra; /* format-specific extra fields */
+ uint8 **scan_cnt; /* scan counters */
+ struct extra_sample_data *xtra;
+ struct midi_macro_data *midi;
+ int compare_vblank;
+};
+
+struct pattern_loop {
+ int start;
+ int count;
+};
+
+struct flow_control {
+ int pbreak;
+ int jump;
+ int delay;
+ int jumpline;
+ int loop_dest; /* Pattern loop destination, -1 for none */
+ int loop_param; /* Last loop param for Digital Tracker */
+ int loop_start; /* Global loop target for S3M et al. */
+ int loop_count; /* Global loop count for S3M et al. */
+ int loop_active_num; /* Number of active loops for scan */
+
+ struct pattern_loop *loop;
+
+ int num_rows;
+ int end_point;
+#define ROWDELAY_ON (1 << 0)
+#define ROWDELAY_FIRST_FRAME (1 << 1)
+ int rowdelay; /* For IT pattern row delay */
+ int rowdelay_set;
+ int force_reposition;
+};
+
+struct virt_channel {
+ int count;
+ int map;
+};
+
+struct scan_data {
+ double time; /* replay time in ms */
+ int row;
+ int ord;
+ int num;
+};
+
+struct player_data {
+ int ord;
+ int pos;
+ int row;
+ int frame;
+ int speed;
+ int bpm;
+ int mode;
+ int player_flags;
+ int flags;
+
+ double time_factor_relative; /* User time factor adjustment */
+ double scan_time_factor; /* m->time_factor for most recent scan */
+ double current_time; /* current time based on scan time factor */
+
+ int loop_count;
+ int sequence;
+ int bad_sequence; /* 1 = can't find next order, need reposition */
+ unsigned char sequence_control[XMP_MAX_MOD_LENGTH];
+
+ int smix_vol; /* SFX volume */
+ int master_vol; /* Music volume */
+ int gvol;
+
+ struct flow_control flow;
+
+ struct scan_data *scan;
+
+ struct channel_data *xc_data;
+
+ int channel_vol[XMP_MAX_CHANNELS];
+ char channel_mute[XMP_MAX_CHANNELS];
+
+ struct virt_control {
+ int num_tracks; /* Number of tracks */
+ int virt_channels; /* Number of virtual channels */
+ int virt_used; /* Number of voices currently in use */
+ int maxvoc; /* Number of sound card voices */
+
+ struct virt_channel *virt_channel;
+
+ struct mixer_voice *voice_array;
+ } virt;
+
+ struct xmp_event inject_event[XMP_MAX_CHANNELS];
+
+ struct {
+ int consumed;
+ int in_size;
+ char *in_buffer;
+ } buffer_data;
+
+#ifndef LIBXMP_CORE_PLAYER
+ int st26_speed; /* For IceTracker speed effect */
+#endif
+ int filter; /* Amiga led filter */
+};
+
+struct mixer_data {
+ int freq; /* sampling rate */
+ int format; /* sample format */
+ int amplify; /* amplification multiplier */
+ int mix; /* percentage of channel separation */
+ int interp; /* interpolation type */
+ int dsp; /* dsp effect flags */
+ char *buffer; /* output buffer */
+ int32 *buf32; /* temporary buffer for 32 bit samples */
+ int total_size; /* allocated samples (not frames) in buffers */
+ int sample_size; /* individual output sample (not frame) size */
+ int output_chn; /* output channels (1 or 2) */
+ int numvoc; /* default softmixer voices number */
+ int ticksize;
+ int dtright; /* anticlick control, right channel */
+ int dtleft; /* anticlick control, left channel */
+ int bidir_adjust; /* adjustment for IT bidirectional loops */
+ double pbase; /* period base */
+};
+
+struct rng_state {
+ unsigned state;
+};
+
+struct context_data {
+ struct player_data p;
+ struct mixer_data s;
+ struct module_data m;
+ struct smix_data smix;
+ struct rng_state rng;
+ int state;
+};
+
+
+/* Prototypes */
+
+LIBXMP_BEGIN_DECLS
+
+char *libxmp_adjust_string (char *);
+void libxmp_load_prologue (struct context_data *); /* use in load only */
+void libxmp_load_epilogue (struct context_data *); /* use in load only */
+int libxmp_prepare_scan (struct context_data *);
+void libxmp_free_scan (struct context_data *);
+int libxmp_scan_sequences (struct context_data *);
+int libxmp_get_sequence (struct context_data *, int);
+int libxmp_set_player_mode (struct context_data *);
+double libxmp_get_frame_time (struct context_data *);
+void libxmp_reset_flow (struct context_data *);
+
+int8 read8s (FILE *, int *err);
+uint8 read8 (FILE *, int *err);
+uint16 read16l (FILE *, int *err);
+uint16 read16b (FILE *, int *err);
+uint32 read24l (FILE *, int *err);
+uint32 read24b (FILE *, int *err);
+uint32 read32l (FILE *, int *err);
+uint32 read32b (FILE *, int *err);
+static inline void write8 (FILE *f, uint8 b) {
+ fputc(b, f);
+}
+void write16l (FILE *, uint16);
+void write16b (FILE *, uint16);
+void write32l (FILE *, uint32);
+void write32b (FILE *, uint32);
+
+uint16 readmem16l (const uint8 *);
+uint16 readmem16b (const uint8 *);
+uint32 readmem24l (const uint8 *);
+uint32 readmem24b (const uint8 *);
+uint32 readmem32l (const uint8 *);
+uint32 readmem32b (const uint8 *);
+
+struct xmp_instrument *libxmp_get_instrument(struct context_data *, int);
+struct xmp_sample *libxmp_get_sample(struct context_data *, int);
+
+char *libxmp_strdup(const char *);
+int libxmp_get_filetype (const char *);
+
+LIBXMP_END_DECLS
+
+#endif /* LIBXMP_COMMON_H */
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
new file mode 100644
index 00000000..bcbaf370
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
@@ -0,0 +1,743 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "format.h"
+#include "virtual.h"
+#include "mixer.h"
+#include "rng.h"
+
+/* TODO: Change this to const char *const in a future ABI change */
+const char *xmp_version LIBXMP_EXPORT_VAR = XMP_VERSION;
+const unsigned int xmp_vercode LIBXMP_EXPORT_VAR = XMP_VERCODE;
+
+xmp_context xmp_create_context(void)
+{
+ struct context_data *ctx;
+
+ ctx = (struct context_data *) calloc(1, sizeof(struct context_data));
+ if (ctx == NULL) {
+ return NULL;
+ }
+
+ ctx->state = XMP_STATE_UNLOADED;
+ ctx->m.defpan = 100;
+ ctx->s.numvoc = SMIX_NUMVOC;
+ libxmp_init_random(&ctx->rng);
+
+ return (xmp_context)ctx;
+}
+
+void xmp_free_context(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+
+ if (ctx->state > XMP_STATE_UNLOADED)
+ xmp_release_module(opaque);
+
+ xmp_end_smix(opaque);
+
+ free(m->instrument_path);
+ free(opaque);
+}
+
+static void set_position(struct context_data *ctx, int pos, int dir)
+{
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ struct flow_control *f = &p->flow;
+ int seq;
+ int has_marker;
+
+ /* If dir is 0, we can jump to a different sequence */
+ if (dir == 0) {
+ seq = libxmp_get_sequence(ctx, pos);
+ } else {
+ seq = p->sequence;
+ }
+
+ if (seq < 0 || seq >= m->num_sequences) {
+ return;
+ }
+
+ has_marker = HAS_QUIRK(QUIRK_MARKER);
+
+ p->sequence = seq;
+
+ if (pos >= 0) {
+ int pat;
+
+ while (has_marker && pos > 0 && pos < mod->len - 1 &&
+ mod->xxo[pos] == XMP_MARK_SKIP) {
+ if (dir < 0) {
+ pos--;
+ } else {
+ pos++;
+ }
+ }
+ if (pos >= mod->len) {
+ return;
+ }
+ pat = mod->xxo[pos];
+
+ if (pat < mod->pat) {
+ if (has_marker && pat == XMP_MARK_END) {
+ return;
+ }
+
+ if (pos > p->scan[seq].ord) {
+ f->end_point = 0;
+ } else {
+ f->num_rows = mod->xxp[pat]->rows;
+ f->end_point = p->scan[seq].num;
+ f->jumpline = 0;
+ }
+ }
+ }
+
+ if (pos < mod->len) {
+ if (pos == 0) {
+ p->pos = -1;
+ } else {
+ p->pos = pos;
+ }
+ /* Clear flow vars to prevent old pattern jumps and
+ * other junk from executing in the new position. */
+ libxmp_reset_flow(ctx);
+ }
+}
+
+int xmp_next_position(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return -XMP_ERROR_STATE;
+
+ if (p->pos < 0) {
+ /* Restart a stopped or restarting module.
+ * This was previously done implicitly. */
+ set_position(ctx, -1, 1);
+ } else if (p->pos < m->mod.len) {
+ set_position(ctx, p->pos + 1, 1);
+ }
+
+ return p->pos;
+}
+
+int xmp_prev_position(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return -XMP_ERROR_STATE;
+
+ if (p->pos == m->seq_data[p->sequence].entry_point) {
+ set_position(ctx, -1, -1);
+ } else if (p->pos > m->seq_data[p->sequence].entry_point) {
+ set_position(ctx, p->pos - 1, -1);
+ }
+ return p->pos < 0 ? 0 : p->pos;
+}
+
+int xmp_set_position(xmp_context opaque, int pos)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return -XMP_ERROR_STATE;
+
+ if (pos < 0 || pos >= m->mod.len)
+ return -XMP_ERROR_INVALID;
+
+ set_position(ctx, pos, 0);
+
+ return p->pos;
+}
+
+int xmp_set_row(xmp_context opaque, int row)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ struct flow_control *f = &p->flow;
+ int pos = p->pos;
+ int pattern;
+
+ if (pos < 0 || pos >= mod->len) {
+ pos = 0;
+ }
+ pattern = mod->xxo[pos];
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return -XMP_ERROR_STATE;
+
+ if (pattern >= mod->pat || row < 0 || row >= mod->xxp[pattern]->rows)
+ return -XMP_ERROR_INVALID;
+
+ /* See set_position. */
+ if (p->pos < 0)
+ p->pos = 0;
+ p->ord = p->pos;
+ p->row = row;
+ p->frame = -1;
+ p->bad_sequence = 0;
+ f->num_rows = mod->xxp[mod->xxo[p->ord]]->rows;
+
+ return row;
+}
+
+void xmp_stop_module(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return;
+
+ p->pos = -2;
+ libxmp_reset_flow(ctx);
+}
+
+void xmp_restart_module(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return;
+
+ p->loop_count = 0;
+ p->pos = -1;
+ libxmp_reset_flow(ctx);
+}
+
+int xmp_seek_time(xmp_context opaque, int time)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct ord_data *oinfo;
+ double t;
+ int pos;
+ int i;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return -XMP_ERROR_STATE;
+
+ for (i = m->mod.len - 1; i >= 0; i--) {
+ int pat = m->mod.xxo[i];
+ if (pat >= m->mod.pat) {
+ continue;
+ }
+ if (libxmp_get_sequence(ctx, i) != p->sequence) {
+ continue;
+ }
+ /* TODO: using rounding to preserve compatibility with
+ * the old (bad) int conversion here until this API
+ * function can be fixed or replaced. */
+ t = m->xxo_info[i].time;
+ CLAMP(t, 0.0, (double)INT_MAX);
+ if (time >= (int)t) {
+ set_position(ctx, i, 1);
+ break;
+ }
+ }
+ if (i < 0) {
+ xmp_set_position(opaque, 0);
+ }
+
+ /* Get the correct start row + force the next xmp_play_frame call to
+ * do a reposition, which updates properties such as BPM and global
+ * volume and normally doesn't happen within the same position: */
+ pos = p->pos >= 0 ? p->pos : m->seq_data[p->sequence].entry_point;
+
+ oinfo = &m->xxo_info[pos];
+ p->flow.jumpline = oinfo->start_row;
+ p->flow.force_reposition = 1;
+
+ /* For the first p->current_time + libxmp_get_frame_time check
+ * in xmp_seek_time_frame: */
+ p->current_time = oinfo->time;
+ p->bpm = oinfo->bpm;
+
+ return p->pos < 0 ? 0 : p->pos;
+}
+
+int xmp_seek_time_frame(xmp_context opaque, int time)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ double max_time, t;
+ int ret, i;
+
+ if ((ret = xmp_seek_time(opaque, time)) < 0) {
+ return ret;
+ }
+
+ /* set_position/xmp_set_position doesn't actually complete the seek;
+ * may need to play frames from the start of the new position until
+ * the player is at the closest frame to the requested time.
+ * TODO: it may be possible to get closer times for xmp_play_buffer
+ * users.
+ */
+#if 0
+ D_(D_INFO "%d %d init: %.06f, %.06f >=? %.06f", p->pos, p->flow.jumpline,
+ p->current_time, p->current_time + libxmp_get_frame_time(ctx),
+ (double)time);
+#endif
+
+ max_time = m->seq_data[p->sequence].duration - 0.1;
+ t = MIN((double)time, max_time);
+
+ /* Try to find the correct frame (this may take a while).
+ * TODO: temporarily put the mixer in a lower rate? */
+ for (i = 0; i < (1 << 13); i++) {
+ double prev = p->current_time;
+
+ /* TODO: the actual BPM isn't known until mid-frame. */
+ if (p->current_time + libxmp_get_frame_time(ctx) > t) {
+ break;
+ }
+ if (xmp_play_frame(opaque) < 0 || p->current_time < prev) {
+ break;
+ }
+#if 0
+ D_(D_INFO "%d %d %d: %.06f >=? %.06f", p->pos, p->row, p->frame,
+ p->current_time + libxmp_get_frame_time(ctx), t);
+#endif
+ }
+
+ /* Force an xmp_play_buffer refresh so the new (wrong) frame data
+ * doesn't confuse the caller: */
+ xmp_play_buffer(opaque, NULL, 0, 0);
+
+ return p->pos < 0 ? 0 : p->pos;
+}
+
+int xmp_channel_mute(xmp_context opaque, int chn, int status)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ int ret;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return -XMP_ERROR_STATE;
+
+ if (chn < 0 || chn >= XMP_MAX_CHANNELS) {
+ return -XMP_ERROR_INVALID;
+ }
+
+ ret = p->channel_mute[chn];
+
+ if (status >= 2) {
+ p->channel_mute[chn] = !p->channel_mute[chn];
+ } else if (status >= 0) {
+ p->channel_mute[chn] = status;
+ }
+
+ return ret;
+}
+
+int xmp_channel_vol(xmp_context opaque, int chn, int vol)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ int ret;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return -XMP_ERROR_STATE;
+
+ if (chn < 0 || chn >= XMP_MAX_CHANNELS) {
+ return -XMP_ERROR_INVALID;
+ }
+
+ ret = p->channel_vol[chn];
+
+ if (vol >= 0 && vol <= 100) {
+ p->channel_vol[chn] = vol;
+ }
+
+ return ret;
+}
+
+#ifdef USE_VERSIONED_SYMBOLS
+LIBXMP_BEGIN_DECLS /* no name-mangling */
+LIBXMP_EXPORT_VERSIONED extern int xmp_set_player_v40__(xmp_context, int, int) LIBXMP_ATTRIB_SYMVER("xmp_set_player@XMP_4.0");
+LIBXMP_EXPORT_VERSIONED extern int xmp_set_player_v41__(xmp_context, int, int)
+ __attribute__((alias("xmp_set_player_v40__"))) LIBXMP_ATTRIB_SYMVER("xmp_set_player@XMP_4.1");
+LIBXMP_EXPORT_VERSIONED extern int xmp_set_player_v43__(xmp_context, int, int)
+ __attribute__((alias("xmp_set_player_v40__"))) LIBXMP_ATTRIB_SYMVER("xmp_set_player@XMP_4.3");
+LIBXMP_EXPORT_VERSIONED extern int xmp_set_player_v44__(xmp_context, int, int)
+ __attribute__((alias("xmp_set_player_v40__"))) LIBXMP_ATTRIB_SYMVER("xmp_set_player@@XMP_4.4");
+
+#ifndef HAVE_ATTRIBUTE_SYMVER
+asm(".symver xmp_set_player_v40__, xmp_set_player@XMP_4.0");
+asm(".symver xmp_set_player_v41__, xmp_set_player@XMP_4.1");
+asm(".symver xmp_set_player_v43__, xmp_set_player@XMP_4.3");
+asm(".symver xmp_set_player_v44__, xmp_set_player@@XMP_4.4");
+#endif
+LIBXMP_END_DECLS
+
+#define xmp_set_player__ xmp_set_player_v40__
+#else
+#define xmp_set_player__ xmp_set_player
+#endif
+
+int xmp_set_player__(xmp_context opaque, int parm, int val)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct mixer_data *s = &ctx->s;
+ int ret = -XMP_ERROR_INVALID;
+
+
+ if (parm == XMP_PLAYER_SMPCTL || parm == XMP_PLAYER_DEFPAN) {
+ /* these should be set before loading the module */
+ if (ctx->state >= XMP_STATE_LOADED) {
+ return -XMP_ERROR_STATE;
+ }
+ } else if (parm == XMP_PLAYER_VOICES) {
+ /* these should be set before start playing */
+ if (ctx->state >= XMP_STATE_PLAYING) {
+ return -XMP_ERROR_STATE;
+ }
+ } else if (ctx->state < XMP_STATE_PLAYING) {
+ return -XMP_ERROR_STATE;
+ }
+
+ switch (parm) {
+ case XMP_PLAYER_AMP:
+ if (val >= 0 && val <= 3) {
+ s->amplify = val;
+ ret = 0;
+ }
+ break;
+ case XMP_PLAYER_MIX:
+ if (val >= -100 && val <= 100) {
+ s->mix = val;
+ ret = 0;
+ }
+ break;
+ case XMP_PLAYER_INTERP:
+ if (val >= XMP_INTERP_NEAREST && val <= XMP_INTERP_SPLINE) {
+ s->interp = val;
+ ret = 0;
+ }
+ break;
+ case XMP_PLAYER_DSP:
+ s->dsp = val;
+ ret = 0;
+ break;
+ case XMP_PLAYER_FLAGS: {
+ p->player_flags = val;
+ ret = 0;
+ break; }
+
+ /* 4.1 */
+ case XMP_PLAYER_CFLAGS: {
+ int vblank = p->flags & XMP_FLAGS_VBLANK;
+ p->flags = val;
+ if (vblank != (p->flags & XMP_FLAGS_VBLANK))
+ libxmp_scan_sequences(ctx);
+ ret = 0;
+ break; }
+ case XMP_PLAYER_SMPCTL:
+ m->smpctl = val;
+ ret = 0;
+ break;
+ case XMP_PLAYER_VOLUME:
+ if (val >= 0 && val <= 200) {
+ p->master_vol = val;
+ ret = 0;
+ }
+ break;
+ case XMP_PLAYER_SMIX_VOLUME:
+ if (val >= 0 && val <= 200) {
+ p->smix_vol = val;
+ ret = 0;
+ }
+ break;
+
+ /* 4.3 */
+ case XMP_PLAYER_DEFPAN:
+ if (val >= 0 && val <= 100) {
+ m->defpan = val;
+ ret = 0;
+ }
+ break;
+
+ /* 4.4 */
+ case XMP_PLAYER_MODE:
+ p->mode = val;
+ libxmp_set_player_mode(ctx);
+ libxmp_scan_sequences(ctx);
+ ret = 0;
+ break;
+ case XMP_PLAYER_VOICES:
+ s->numvoc = val;
+ break;
+ }
+
+ return ret;
+}
+
+#ifdef USE_VERSIONED_SYMBOLS
+LIBXMP_BEGIN_DECLS /* no name-mangling */
+LIBXMP_EXPORT_VERSIONED extern int xmp_get_player_v40__(xmp_context, int) LIBXMP_ATTRIB_SYMVER("xmp_get_player@XMP_4.0");
+LIBXMP_EXPORT_VERSIONED extern int xmp_get_player_v41__(xmp_context, int)
+ __attribute__((alias("xmp_get_player_v40__"))) LIBXMP_ATTRIB_SYMVER("xmp_get_player@XMP_4.1");
+LIBXMP_EXPORT_VERSIONED extern int xmp_get_player_v42__(xmp_context, int)
+ __attribute__((alias("xmp_get_player_v40__"))) LIBXMP_ATTRIB_SYMVER("xmp_get_player@XMP_4.2");
+LIBXMP_EXPORT_VERSIONED extern int xmp_get_player_v43__(xmp_context, int)
+ __attribute__((alias("xmp_get_player_v40__"))) LIBXMP_ATTRIB_SYMVER("xmp_get_player@XMP_4.3");
+LIBXMP_EXPORT_VERSIONED extern int xmp_get_player_v44__(xmp_context, int)
+ __attribute__((alias("xmp_get_player_v40__"))) LIBXMP_ATTRIB_SYMVER("xmp_get_player@@XMP_4.4");
+
+#ifndef HAVE_ATTRIBUTE_SYMVER
+asm(".symver xmp_get_player_v40__, xmp_get_player@XMP_4.0");
+asm(".symver xmp_get_player_v41__, xmp_get_player@XMP_4.1");
+asm(".symver xmp_get_player_v42__, xmp_get_player@XMP_4.2");
+asm(".symver xmp_get_player_v43__, xmp_get_player@XMP_4.3");
+asm(".symver xmp_get_player_v44__, xmp_get_player@@XMP_4.4");
+#endif
+LIBXMP_END_DECLS
+
+#define xmp_get_player__ xmp_get_player_v40__
+#else
+#define xmp_get_player__ xmp_get_player
+#endif
+
+int xmp_get_player__(xmp_context opaque, int parm)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct mixer_data *s = &ctx->s;
+ int ret = -XMP_ERROR_INVALID;
+
+ if (parm == XMP_PLAYER_SMPCTL || parm == XMP_PLAYER_DEFPAN) {
+ // can read these at any time
+ } else if (parm != XMP_PLAYER_STATE && ctx->state < XMP_STATE_PLAYING) {
+ return -XMP_ERROR_STATE;
+ }
+
+ switch (parm) {
+ case XMP_PLAYER_AMP:
+ ret = s->amplify;
+ break;
+ case XMP_PLAYER_MIX:
+ ret = s->mix;
+ break;
+ case XMP_PLAYER_INTERP:
+ ret = s->interp;
+ break;
+ case XMP_PLAYER_DSP:
+ ret = s->dsp;
+ break;
+ case XMP_PLAYER_FLAGS:
+ ret = p->player_flags;
+ break;
+
+ /* 4.1 */
+ case XMP_PLAYER_CFLAGS:
+ ret = p->flags;
+ break;
+ case XMP_PLAYER_SMPCTL:
+ ret = m->smpctl;
+ break;
+ case XMP_PLAYER_VOLUME:
+ ret = p->master_vol;
+ break;
+ case XMP_PLAYER_SMIX_VOLUME:
+ ret = p->smix_vol;
+ break;
+
+ /* 4.2 */
+ case XMP_PLAYER_STATE:
+ ret = ctx->state;
+ break;
+
+ /* 4.3 */
+ case XMP_PLAYER_DEFPAN:
+ ret = m->defpan;
+ break;
+
+ /* 4.4 */
+ case XMP_PLAYER_MODE:
+ ret = p->mode;
+ break;
+ case XMP_PLAYER_MIXER_TYPE:
+ ret = XMP_MIXER_STANDARD;
+ if (p->flags & XMP_FLAGS_A500) {
+ if (IS_AMIGA_MOD()) {
+#ifdef LIBXMP_PAULA_SIMULATOR
+ if (p->filter) {
+ ret = XMP_MIXER_A500F;
+ } else {
+ ret = XMP_MIXER_A500;
+ }
+#endif
+ }
+ }
+ break;
+ case XMP_PLAYER_VOICES:
+ ret = s->numvoc;
+ break;
+ }
+
+ return ret;
+}
+
+const char *const *xmp_get_format_list(void)
+{
+ return format_list();
+}
+
+void xmp_inject_event(xmp_context opaque, int channel, struct xmp_event *e)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+
+ if (ctx->state < XMP_STATE_PLAYING)
+ return;
+
+ memcpy(&p->inject_event[channel], e, sizeof(struct xmp_event));
+ p->inject_event[channel]._flag = 1;
+}
+
+int xmp_set_instrument_path(xmp_context opaque, const char *path)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+
+ if (m->instrument_path != NULL) {
+ free(m->instrument_path);
+ m->instrument_path = NULL;
+ }
+ if (path == NULL) {
+ return 0;
+ }
+
+ m->instrument_path = libxmp_strdup(path);
+ if (m->instrument_path == NULL) {
+ return -XMP_ERROR_SYSTEM;
+ }
+
+ return 0;
+}
+
+int xmp_set_tempo_factor(xmp_context opaque, double val)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct mixer_data *s = &ctx->s;
+ int ticksize;
+
+ /* This function relies on values initialized by xmp_start_player
+ * and will behave in an undefined manner if called prior. */
+ if (ctx->state < XMP_STATE_PLAYING) {
+ return -XMP_ERROR_STATE;
+ }
+
+ if (val <= 0.0 || val != val /* NaN */) {
+ return -1;
+ }
+
+ val *= 10;
+
+ /* s->freq can change between xmp_start_player calls and p->bpm can
+ * change during playback, so repeat these checks in the mixer. */
+ ticksize = libxmp_mixer_get_ticksize(s->freq,
+ val * p->time_factor_relative, m->rrate, p->bpm);
+
+ /* ticksize is in frames, s->total_size is in frames * 2. */
+ if (ticksize < 0 || ticksize > (s->total_size / 2)) {
+ return -1;
+ }
+ m->time_factor = val;
+
+ return 0;
+}
+
+int xmp_set_tempo_factor_relative(xmp_context opaque, double val)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct mixer_data *s = &ctx->s;
+ int ticksize;
+
+ /* This function relies on values initialized by xmp_start_player
+ * and will behave in an undefined manner if called prior. */
+ if (ctx->state < XMP_STATE_PLAYING) {
+ return -XMP_ERROR_STATE;
+ }
+
+ if (val <= 0.0 || val != val /* NaN */) {
+ return -1;
+ }
+
+ ticksize = libxmp_mixer_get_ticksize(s->freq,
+ m->time_factor * val, m->rrate, p->bpm);
+
+ /* ticksize is in frames, s->total_size is in frames * 2. */
+ if (ticksize < 0 || ticksize > (s->total_size / 2)) {
+ return -1;
+ }
+ p->time_factor_relative = val;
+
+ return 0;
+}
+
+double xmp_get_tempo_factor(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+
+ if (ctx->state < XMP_STATE_LOADED) {
+ return -1.0 * XMP_ERROR_STATE;
+ }
+
+ return ctx->m.time_factor * 0.1;
+}
+
+double xmp_get_tempo_factor_relative(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+
+ if (ctx->state < XMP_STATE_PLAYING) {
+ return -1.0 * XMP_ERROR_STATE;
+ }
+
+ return ctx->p.time_factor_relative;
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/dataio.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/dataio.c
new file mode 100644
index 00000000..a7f69d87
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/dataio.c
@@ -0,0 +1,254 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include
+#include "common.h"
+
+
+#define read_byte(x) do { \
+ (x) = fgetc(f); \
+ if ((x) < 0) goto error; \
+} while (0)
+
+#define set_error(x) do { \
+ if (err != NULL) *err = (x); \
+} while (0)
+
+uint8 read8(FILE *f, int *err)
+{
+ int a;
+
+ read_byte(a);
+ set_error(0);
+ return a;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0xff;
+}
+
+int8 read8s(FILE *f, int *err)
+{
+ int a;
+
+ read_byte(a);
+ set_error(0);
+ return (int8)a;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0;
+}
+
+uint16 read16l(FILE *f, int *err)
+{
+ int32 a, b;
+
+ read_byte(a);
+ read_byte(b);
+
+ set_error(0);
+ return ((uint32)b << 8) | a;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0xffff;
+}
+
+uint16 read16b(FILE *f, int *err)
+{
+ int32 a, b;
+
+ read_byte(a);
+ read_byte(b);
+
+ set_error(0);
+ return (a << 8) | b;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0xffff;
+}
+
+uint32 read24l(FILE *f, int *err)
+{
+ int32 a, b, c;
+
+ read_byte(a);
+ read_byte(b);
+ read_byte(c);
+
+ set_error(0);
+ return (c << 16) | (b << 8) | a;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0xffffffff;
+}
+
+uint32 read24b(FILE *f, int *err)
+{
+ int32 a, b, c;
+
+ read_byte(a);
+ read_byte(b);
+ read_byte(c);
+
+ set_error(0);
+ return (a << 16) | (b << 8) | c;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0xffffffff;
+}
+
+uint32 read32l(FILE *f, int *err)
+{
+ int32 a, b, c, d;
+
+ read_byte(a);
+ read_byte(b);
+ read_byte(c);
+ read_byte(d);
+
+ set_error(0);
+ return ((uint32)d << 24) | (c << 16) | (b << 8) | a;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0xffffffff;
+}
+
+uint32 read32b(FILE *f, int *err)
+{
+ int32 a, b, c, d;
+
+ read_byte(a);
+ read_byte(b);
+ read_byte(c);
+ read_byte(d);
+
+ set_error(0);
+ return ((uint32)a << 24) | (b << 16) | (c << 8) | d;
+
+ error:
+ set_error(ferror(f) ? errno : EOF);
+ return 0xffffffff;
+}
+
+uint16 readmem16l(const uint8 *m)
+{
+ uint32 a, b;
+
+ a = m[0];
+ b = m[1];
+
+ return (b << 8) | a;
+}
+
+uint16 readmem16b(const uint8 *m)
+{
+ uint32 a, b;
+
+ a = m[0];
+ b = m[1];
+
+ return (a << 8) | b;
+}
+
+uint32 readmem24l(const uint8 *m)
+{
+ uint32 a, b, c;
+
+ a = m[0];
+ b = m[1];
+ c = m[2];
+
+ return (c << 16) | (b << 8) | a;
+}
+
+uint32 readmem24b(const uint8 *m)
+{
+ uint32 a, b, c;
+
+ a = m[0];
+ b = m[1];
+ c = m[2];
+
+ return (a << 16) | (b << 8) | c;
+}
+
+uint32 readmem32l(const uint8 *m)
+{
+ uint32 a, b, c, d;
+
+ a = m[0];
+ b = m[1];
+ c = m[2];
+ d = m[3];
+
+ return (d << 24) | (c << 16) | (b << 8) | a;
+}
+
+uint32 readmem32b(const uint8 *m)
+{
+ uint32 a, b, c, d;
+
+ a = m[0];
+ b = m[1];
+ c = m[2];
+ d = m[3];
+
+ return (a << 24) | (b << 16) | (c << 8) | d;
+}
+
+#ifndef LIBXMP_CORE_PLAYER
+
+void write16l(FILE *f, uint16 w)
+{
+ write8(f, w & 0x00ff);
+ write8(f, (w & 0xff00) >> 8);
+}
+
+void write16b(FILE *f, uint16 w)
+{
+ write8(f, (w & 0xff00) >> 8);
+ write8(f, w & 0x00ff);
+}
+
+void write32l(FILE *f, uint32 w)
+{
+ write8(f, w & 0x000000ff);
+ write8(f, (w & 0x0000ff00) >> 8);
+ write8(f, (w & 0x00ff0000) >> 16);
+ write8(f, (w & 0xff000000) >> 24);
+}
+
+void write32b(FILE *f, uint32 w)
+{
+ write8(f, (w & 0xff000000) >> 24);
+ write8(f, (w & 0x00ff0000) >> 16);
+ write8(f, (w & 0x0000ff00) >> 8);
+ write8(f, w & 0x000000ff);
+}
+
+#endif
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/effects.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/effects.c
new file mode 100644
index 00000000..506927ac
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/effects.c
@@ -0,0 +1,1149 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "common.h"
+#include "player.h"
+#include "effects.h"
+#include "period.h"
+#include "virtual.h"
+#include "mixer.h"
+#ifndef LIBXMP_CORE_PLAYER
+#include "extras.h"
+#endif
+
+#define NOT_IMPLEMENTED
+#define HAS_QUIRK(x) (m->quirk & (x))
+
+#define SET_LFO_NOTZERO(lfo, depth, rate) do { \
+ if ((depth) != 0) libxmp_lfo_set_depth(lfo, depth); \
+ if ((rate) != 0) libxmp_lfo_set_rate(lfo, rate); \
+} while (0)
+
+#define EFFECT_MEMORY__(p, m) do { \
+ if ((p) == 0) { (p) = (m); } else { (m) = (p); } \
+} while (0)
+
+/* ST3 effect memory is not a bug, but it's a weird implementation and it's
+ * unlikely to be supported in anything other than ST3 (or OpenMPT).
+ */
+#define EFFECT_MEMORY(p, m) do { \
+ if (HAS_QUIRK(QUIRK_ST3BUGS)) { \
+ EFFECT_MEMORY__((p), xc->vol.memory); \
+ } else { \
+ EFFECT_MEMORY__((p), (m)); \
+ } \
+} while (0)
+
+#define EFFECT_MEMORY_GET(p, m) do { \
+ if (HAS_QUIRK(QUIRK_ST3BUGS)) { \
+ (p) = xc->vol.memory; \
+ } else { \
+ (p) = (m); \
+ } \
+} while(0)
+
+#define EFFECT_MEMORY_SETONLY(p, m) do { \
+ EFFECT_MEMORY__((p), (m)); \
+ if (HAS_QUIRK(QUIRK_ST3BUGS)) { \
+ if ((p) != 0) { xc->vol.memory = (p); } \
+ } \
+} while (0)
+
+#define EFFECT_MEMORY_S3M(p) do { \
+ if (HAS_QUIRK(QUIRK_ST3BUGS)) { \
+ EFFECT_MEMORY__((p), xc->vol.memory); \
+ } \
+} while (0)
+
+
+static void do_toneporta(struct context_data *ctx,
+ struct channel_data *xc, int note)
+{
+ struct module_data *m = &ctx->m;
+ struct xmp_instrument *instrument = &m->mod.xxi[xc->ins];
+ struct xmp_subinstrument *sub;
+ int mapped_xpo = 0;
+ int mapped = 0;
+
+ if (IS_VALID_NOTE(xc->key)) {
+ mapped = instrument->map[xc->key].ins;
+ }
+
+ if (mapped >= instrument->nsm) {
+ mapped = 0;
+ }
+
+ sub = &instrument->sub[mapped];
+
+ if (IS_VALID_NOTE(note - 1) && (uint32)xc->ins < m->mod.ins) {
+ note--;
+ if (IS_VALID_NOTE(xc->key_porta)) {
+ mapped_xpo = instrument->map[xc->key_porta].xpo;
+ }
+ xc->porta.target = libxmp_note_to_period(ctx, note + sub->xpo +
+ mapped_xpo, xc->finetune, xc->per_adj);
+ }
+ xc->porta.dir = xc->period < xc->porta.target ? 1 : -1;
+}
+
+void libxmp_process_fx(struct context_data *ctx, struct channel_data *xc, int chn,
+ const struct xmp_event *e, int fnum)
+{
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ struct flow_control *f = &p->flow;
+ uint8 note, fxp, fxt;
+ int h, l;
+
+ /* key_porta is IT only */
+ if (m->read_event_type != READ_EVENT_IT) {
+ xc->key_porta = xc->key;
+ }
+
+ note = e->note;
+ if (fnum == 0) {
+ fxt = e->fxt;
+ fxp = e->fxp;
+ } else {
+ fxt = e->f2t;
+ fxp = e->f2p;
+ }
+
+ switch (fxt) {
+ case FX_ARPEGGIO:
+ fx_arpeggio:
+ if (!HAS_QUIRK(QUIRK_ARPMEM) || fxp != 0) {
+ xc->arpeggio.val[0] = 0;
+ xc->arpeggio.val[1] = MSN(fxp);
+ xc->arpeggio.val[2] = LSN(fxp);
+ xc->arpeggio.size = 3;
+ }
+ break;
+ case FX_S3M_ARPEGGIO:
+ EFFECT_MEMORY(fxp, xc->arpeggio.memory);
+ goto fx_arpeggio;
+
+#ifndef LIBXMP_CORE_PLAYER
+ case FX_OKT_ARP3:
+ if (fxp != 0) {
+ xc->arpeggio.val[0] = -MSN(fxp);
+ xc->arpeggio.val[1] = 0;
+ xc->arpeggio.val[2] = LSN(fxp);
+ xc->arpeggio.size = 3;
+ }
+ break;
+ case FX_OKT_ARP4:
+ if (fxp != 0) {
+ xc->arpeggio.val[0] = 0;
+ xc->arpeggio.val[1] = LSN(fxp);
+ xc->arpeggio.val[2] = 0;
+ xc->arpeggio.val[3] = -MSN(fxp);
+ xc->arpeggio.size = 4;
+ }
+ break;
+ case FX_OKT_ARP5:
+ if (fxp != 0) {
+ xc->arpeggio.val[0] = LSN(fxp);
+ xc->arpeggio.val[1] = LSN(fxp);
+ xc->arpeggio.val[2] = 0;
+ xc->arpeggio.size = 3;
+ }
+ break;
+#endif
+ case FX_PORTA_UP: /* Portamento up */
+ EFFECT_MEMORY(fxp, xc->freq.memory);
+
+ if (HAS_QUIRK(QUIRK_FINEFX)
+ && (fnum == 0 || !HAS_QUIRK(QUIRK_ITVPOR))) {
+ switch (MSN(fxp)) {
+ case 0xf:
+ fxp &= 0x0f;
+ goto fx_f_porta_up;
+ case 0xe:
+ fxp &= 0x0f;
+ goto fx_xf_porta_up;
+ }
+ }
+
+ if (fxp != 0) {
+ SET(PITCHBEND);
+ xc->freq.slide = -fxp;
+ if (HAS_QUIRK(QUIRK_UNISLD))
+ xc->porta.memory = fxp;
+ }
+ break;
+ case FX_PORTA_DN: /* Portamento down */
+ /* FT2 has separate up and down memory. */
+ if (HAS_QUIRK(QUIRK_FT2BUGS)) {
+ EFFECT_MEMORY(fxp, xc->freq.down_memory);
+ } else {
+ EFFECT_MEMORY(fxp, xc->freq.memory);
+ }
+
+ if (HAS_QUIRK(QUIRK_FINEFX)
+ && (fnum == 0 || !HAS_QUIRK(QUIRK_ITVPOR))) {
+ switch (MSN(fxp)) {
+ case 0xf:
+ fxp &= 0x0f;
+ goto fx_f_porta_dn;
+ case 0xe:
+ fxp &= 0x0f;
+ goto fx_xf_porta_dn;
+ }
+ }
+
+ if (fxp != 0) {
+ SET(PITCHBEND);
+ xc->freq.slide = fxp;
+ if (HAS_QUIRK(QUIRK_UNISLD))
+ xc->porta.memory = fxp;
+ }
+ break;
+ case FX_TONEPORTA: /* Tone portamento */
+ EFFECT_MEMORY_SETONLY(fxp, xc->porta.memory);
+
+ if (fxp != 0) {
+ if (HAS_QUIRK(QUIRK_UNISLD)) /* IT compatible Gxx off */
+ xc->freq.memory = fxp;
+ xc->porta.slide += fxp;
+ }
+
+ if (HAS_QUIRK(QUIRK_IGSTPOR)) {
+ if (note == 0 && xc->porta.dir == 0)
+ break;
+ }
+
+ if (!IS_VALID_INSTRUMENT(xc->ins))
+ break;
+
+ do_toneporta(ctx, xc, note);
+
+ SET(TONEPORTA);
+ break;
+
+ case FX_VIBRATO: /* Vibrato */
+ EFFECT_MEMORY_SETONLY(fxp, xc->vibrato.memory);
+
+ SET(VIBRATO);
+ SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp) << 2, MSN(fxp));
+ break;
+ case FX_FINE_VIBRATO: /* Fine vibrato */
+ EFFECT_MEMORY_SETONLY(fxp, xc->vibrato.memory);
+
+ SET(VIBRATO);
+ SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp), MSN(fxp));
+ break;
+
+ case FX_TONE_VSLIDE: /* Toneporta + vol slide */
+ EFFECT_MEMORY_GET(l, xc->porta.memory);
+ xc->porta.slide += l;
+ if (!IS_VALID_INSTRUMENT(xc->ins))
+ break;
+ do_toneporta(ctx, xc, note);
+ SET(TONEPORTA);
+ goto fx_volslide;
+ case FX_VIBRA_VSLIDE: /* Vibrato + vol slide */
+ SET(VIBRATO);
+ goto fx_volslide;
+
+ case FX_TREMOLO: /* Tremolo */
+ EFFECT_MEMORY(fxp, xc->tremolo.memory);
+ SET(TREMOLO);
+ SET_LFO_NOTZERO(&xc->tremolo.lfo, LSN(fxp), MSN(fxp));
+ break;
+
+ case FX_SETPAN: /* Set pan */
+ if (HAS_QUIRK(QUIRK_PROTRACK)) {
+ break;
+ }
+ fx_setpan:
+ xc->pan.val = fxp;
+ xc->pan.surround = 0;
+ xc->rpv = 0; /* storlek_20: set pan overrides random pan */
+ break;
+ case FX_OFFSET: /* Set sample offset */
+ if (HAS_QUIRK(QUIRK_FT2BUGS)) {
+ /* FT2: only set memory when offset activates, see
+ * read_event_ft2 (ft2_offset_memory.xm). */
+ fxp = fxp ? fxp : xc->offset.memory;
+ } else {
+ EFFECT_MEMORY(fxp, xc->offset.memory);
+ }
+ SET(OFFSET);
+ if (note) {
+ xc->offset.val &= xc->offset.val & ~0xffff;
+ xc->offset.val |= fxp << 8;
+ xc->offset.val2 = fxp << 8;
+ }
+ if (e->ins) {
+ xc->offset.val2 = fxp << 8;
+ }
+ break;
+ case FX_VOLSLIDE: /* Volume slide */
+ fx_volslide:
+ /* S3M file volume slide note:
+ * DFy Fine volume down by y (...) If y is 0, the command will
+ * be treated as a volume slide up with a value of f (15).
+ * If a DFF command is specified, the volume will be slid
+ * up.
+ */
+ if (HAS_QUIRK(QUIRK_FINEFX)) {
+ h = MSN(fxp);
+ l = LSN(fxp);
+ if (l == 0xf && h != 0) {
+ xc->vol.memory = fxp;
+ fxp >>= 4;
+ goto fx_f_vslide_up;
+ } else if (h == 0xf && l != 0) {
+ xc->vol.memory = fxp;
+ fxp &= 0x0f;
+ goto fx_f_vslide_dn;
+ }
+ }
+
+ /* recover memory */
+ if (fxp == 0x00) {
+ if ((fxp = xc->vol.memory) != 0)
+ goto fx_volslide;
+ }
+
+ /* Skaven's 2nd reality (S3M) has volslide parameter D7 => pri
+ * down. Other trackers only compute volumes if the other
+ * parameter is 0, Fall from sky.xm has 2C => do nothing.
+ * Also don't assign xc->vol.memory if fxp is 0, see Guild
+ * of Sounds.xm
+ */
+ if (fxp) {
+ SET(VOL_SLIDE);
+ xc->vol.memory = fxp;
+ h = MSN(fxp);
+ l = LSN(fxp);
+ if (fxp) {
+ /* Imago Orpheus uses (x - y).
+ * TODO: a less hacky way of detecting this
+ * (READ_EVENT_ORPHEUS or quirk) would be nice.
+ * (test_effect_finefx_imf) */
+ if (m->flow_mode == FLOW_MODE_ORPHEUS) {
+ xc->vol.slide = h - l;
+ } else if (HAS_QUIRK(QUIRK_VOLPDN)) {
+ xc->vol.slide = l ? -l : h;
+ } else {
+ xc->vol.slide = h ? h : -l;
+ }
+ }
+ }
+
+ /* Mirko reports that a S3M with D0F effects created with ST321
+ * should process volume slides in all frames like ST300. I
+ * suspect ST3/IT could be handling D0F effects like this.
+ */
+ if (HAS_QUIRK(QUIRK_FINEFX)) {
+ if (MSN(xc->vol.memory) == 0xf
+ || LSN(xc->vol.memory) == 0xf) {
+ SET(FINE_VOLS);
+ xc->vol.fslide = xc->vol.slide;
+ }
+ }
+ break;
+ case FX_VOLSLIDE_2: /* Secondary volume slide (no memory) */
+ if (fxp) {
+ SET(VOL_SLIDE_2);
+ h = MSN(fxp);
+ l = LSN(fxp);
+ xc->vol.slide2 = h ? h : -l;
+ }
+ break;
+ case FX_JUMP: /* Order jump */
+ libxmp_process_pattern_jump(ctx, f, fxp);
+ break;
+ case FX_VOLSET: /* Volume set */
+ SET(NEW_VOL);
+ xc->volume = fxp;
+ if (xc->split) {
+ p->xc_data[xc->pair].volume = xc->volume;
+ }
+ break;
+ case FX_BREAK: /* Pattern break */
+ libxmp_process_pattern_break(ctx, f, 10 * MSN(fxp) + LSN(fxp));
+ break;
+ case FX_EXTENDED: /* Extended effect */
+ EFFECT_MEMORY_S3M(fxp);
+ fxt = fxp >> 4;
+ fxp &= 0x0f;
+ switch (fxt) {
+ case EX_FILTER: /* Amiga led filter */
+ if (IS_AMIGA_MOD()) {
+ p->filter = !(fxp & 1);
+ }
+ break;
+ case EX_F_PORTA_UP: /* Fine portamento up */
+ EFFECT_MEMORY(fxp, xc->fine_porta.up_memory);
+ goto fx_f_porta_up;
+ case EX_F_PORTA_DN: /* Fine portamento down */
+ EFFECT_MEMORY(fxp, xc->fine_porta.down_memory);
+ goto fx_f_porta_dn;
+ case EX_GLISS: /* Glissando toggle */
+ if (fxp) {
+ SET_NOTE(NOTE_GLISSANDO);
+ } else {
+ RESET_NOTE(NOTE_GLISSANDO);
+ }
+ break;
+ case EX_VIBRATO_WF: /* Set vibrato waveform */
+ fxp &= 3;
+ libxmp_lfo_set_waveform(&xc->vibrato.lfo, fxp);
+ break;
+ case EX_FINETUNE: /* Set finetune */
+ if (!HAS_QUIRK(QUIRK_FT2BUGS) || note > 0) {
+ xc->finetune = (int8)(fxp << 4);
+ }
+ break;
+ case EX_PATTERN_LOOP: /* Loop pattern */
+ libxmp_process_pattern_loop(ctx, f, chn, p->row, fxp);
+ break;
+ case EX_TREMOLO_WF: /* Set tremolo waveform */
+ libxmp_lfo_set_waveform(&xc->tremolo.lfo, fxp & 3);
+ break;
+ case EX_SETPAN:
+ fxp <<= 4;
+ goto fx_setpan;
+ case EX_RETRIG: /* Retrig note */
+#ifndef LIBXMP_CORE_PLAYER
+ fx_retrig:
+#endif
+ SET(RETRIG);
+ xc->retrig.val = fxp;
+ xc->retrig.count = fxp + 1;
+ xc->retrig.type = 0;
+ xc->retrig.limit = HAS_QUIRK(QUIRK_RTONCE) ? 1 : 0;
+ break;
+ case EX_F_VSLIDE_UP: /* Fine volume slide up */
+ EFFECT_MEMORY(fxp, xc->fine_vol.up_memory);
+ goto fx_f_vslide_up;
+ case EX_F_VSLIDE_DN: /* Fine volume slide down */
+ EFFECT_MEMORY(fxp, xc->fine_vol.down_memory);
+ goto fx_f_vslide_dn;
+ case EX_CUT: /* Cut note */
+ SET(RETRIG);
+ SET_NOTE(NOTE_CUT); /* for IT cut-carry */
+ xc->retrig.val = fxp + 1;
+ xc->retrig.count = xc->retrig.val;
+ xc->retrig.type = 0x10;
+ break;
+ case EX_DELAY: /* Note delay */
+ /* computed at frame loop */
+ break;
+ case EX_PATT_DELAY: /* Pattern delay */
+ goto fx_patt_delay;
+ case EX_INVLOOP: /* Invert loop / funk repeat */
+ xc->invloop.speed = fxp;
+ break;
+ }
+ break;
+ case FX_SPEED: /* Set speed */
+ if (HAS_QUIRK(QUIRK_NOBPM) || p->flags & XMP_FLAGS_VBLANK) {
+ goto fx_s3m_speed;
+ }
+
+ /* speedup.xm needs BPM = 20 */
+ if (fxp < 0x20) {
+ goto fx_s3m_speed;
+ }
+ goto fx_s3m_bpm;
+
+ case FX_FINETUNE:
+ xc->finetune = (int16) (fxp - 0x80);
+ break;
+
+ case FX_F_VSLIDE_UP: /* Fine volume slide up */
+ EFFECT_MEMORY(fxp, xc->fine_vol.up_memory);
+ fx_f_vslide_up:
+ SET(FINE_VOLS);
+ xc->vol.fslide = fxp;
+ break;
+ case FX_F_VSLIDE_DN: /* Fine volume slide down */
+ EFFECT_MEMORY(fxp, xc->fine_vol.up_memory);
+ fx_f_vslide_dn:
+ SET(FINE_VOLS);
+ xc->vol.fslide = -fxp;
+ break;
+
+ case FX_F_PORTA_UP: /* Fine portamento up */
+ fx_f_porta_up:
+ if (fxp) {
+ SET(FINE_BEND);
+ xc->freq.fslide = -fxp;
+ }
+ break;
+ case FX_F_PORTA_DN: /* Fine portamento down */
+ fx_f_porta_dn:
+ if (fxp) {
+ SET(FINE_BEND);
+ xc->freq.fslide = fxp;
+ }
+ break;
+ case FX_PATT_DELAY:
+ fx_patt_delay:
+ if (m->read_event_type != READ_EVENT_ST3 || !p->flow.delay) {
+ p->flow.delay = fxp;
+ }
+ break;
+
+ case FX_S3M_SPEED: /* Set S3M speed */
+ EFFECT_MEMORY_S3M(fxp);
+ fx_s3m_speed:
+ if (fxp) {
+ p->speed = fxp;
+#ifndef LIBXMP_CORE_PLAYER
+ p->st26_speed = 0;
+#endif
+ }
+ break;
+ case FX_S3M_BPM: /* Set S3M BPM */
+ fx_s3m_bpm: {
+ /* Lower time factor in MED allows lower BPM values */
+ int min_bpm = (int)(0.5 + m->time_factor * XMP_MIN_BPM / 10);
+ if (fxp < min_bpm)
+ fxp = min_bpm;
+ p->bpm = fxp;
+ break;
+ }
+
+#ifndef LIBXMP_CORE_DISABLE_IT
+ case FX_IT_BPM: /* Set IT BPM */
+ if (MSN(fxp) == 0) {
+ SET(TEMPO_SLIDE);
+ if (LSN(fxp)) /* T0x - Tempo slide down by x */
+ xc->tempo.slide = -LSN(fxp);
+ /* T00 - Repeat previous slide */
+ } else if (MSN(fxp) == 1) { /* T1x - Tempo slide up by x */
+ SET(TEMPO_SLIDE);
+ xc->tempo.slide = LSN(fxp);
+ } else {
+ if (fxp < XMP_MIN_BPM)
+ fxp = XMP_MIN_BPM;
+ p->bpm = fxp;
+ }
+ break;
+ case FX_IT_ROWDELAY:
+ if (!f->rowdelay_set) {
+ f->rowdelay = fxp;
+ f->rowdelay_set = ROWDELAY_ON | ROWDELAY_FIRST_FRAME;
+ }
+ break;
+
+ /* From the OpenMPT VolColMemory.it test case:
+ * "Volume column commands a, b, c and d (volume slide) share one
+ * effect memory, but it should not be shared with Dxy in the effect
+ * column.
+ */
+ case FX_VSLIDE_UP_2: /* Fine volume slide up */
+ EFFECT_MEMORY(fxp, xc->vol.memory2);
+ SET(VOL_SLIDE_2);
+ xc->vol.slide2 = fxp;
+ break;
+ case FX_VSLIDE_DN_2: /* Fine volume slide down */
+ EFFECT_MEMORY(fxp, xc->vol.memory2);
+ SET(VOL_SLIDE_2);
+ xc->vol.slide2 = -fxp;
+ break;
+ case FX_F_VSLIDE_UP_2: /* Fine volume slide up */
+ EFFECT_MEMORY(fxp, xc->vol.memory2);
+ SET(FINE_VOLS_2);
+ xc->vol.fslide2 = fxp;
+ break;
+ case FX_F_VSLIDE_DN_2: /* Fine volume slide down */
+ EFFECT_MEMORY(fxp, xc->vol.memory2);
+ SET(FINE_VOLS_2);
+ xc->vol.fslide2 = -fxp;
+ break;
+ case FX_IT_BREAK: /* Pattern break with hex parameter */
+ libxmp_process_pattern_break(ctx, f, fxp);
+ break;
+
+#endif
+
+ case FX_GLOBALVOL: /* Set global volume */
+ if (fxp > m->gvolbase) {
+ p->gvol = m->gvolbase;
+ } else {
+ p->gvol = fxp;
+ }
+ break;
+ case FX_GVOL_SLIDE: /* Global volume slide */
+ fx_gvolslide:
+ if (fxp) {
+ SET(GVOL_SLIDE);
+ xc->gvol.memory = fxp;
+ h = MSN(fxp);
+ l = LSN(fxp);
+
+ if (HAS_QUIRK(QUIRK_FINEFX)) {
+ if (l == 0xf && h != 0) {
+ xc->gvol.slide = 0;
+ xc->gvol.fslide = h;
+ } else if (h == 0xf && l != 0) {
+ xc->gvol.slide = 0;
+ xc->gvol.fslide = -l;
+ } else {
+ xc->gvol.slide = h ? h : -l;
+ xc->gvol.fslide = 0;
+ }
+ } else {
+ xc->gvol.slide = h ? h : -l;
+ xc->gvol.fslide = 0;
+ }
+ } else {
+ if ((fxp = xc->gvol.memory) != 0) {
+ goto fx_gvolslide;
+ }
+ }
+ break;
+ case FX_KEYOFF: /* Key off */
+ xc->keyoff = fxp + 1;
+ break;
+ case FX_ENVPOS: /* Set envelope position */
+ /* From OpenMPT SetEnvPos.xm:
+ * "When using the Lxx effect, Fasttracker 2 only sets the
+ * panning envelope position if the volume envelope’s sustain
+ * flag is set.
+ */
+ if (HAS_QUIRK(QUIRK_FT2BUGS)) {
+ struct xmp_instrument *instrument;
+ instrument = libxmp_get_instrument(ctx, xc->ins);
+ if (instrument != NULL) {
+ if (instrument->aei.flg & XMP_ENVELOPE_SUS) {
+ xc->p_idx = fxp;
+ }
+ }
+ } else {
+ xc->p_idx = fxp;
+ }
+ xc->v_idx = fxp;
+ xc->f_idx = fxp;
+ break;
+ case FX_PANSLIDE: /* Pan slide (XM) */
+ EFFECT_MEMORY(fxp, xc->pan.memory);
+ SET(PAN_SLIDE);
+ xc->pan.slide = LSN(fxp) - MSN(fxp);
+ break;
+ case FX_PANSL_NOMEM: /* Pan slide (XM volume column) */
+ SET(PAN_SLIDE);
+ xc->pan.slide = LSN(fxp) - MSN(fxp);
+ break;
+
+#ifndef LIBXMP_CORE_DISABLE_IT
+ case FX_IT_PANSLIDE: /* Pan slide w/ fine pan (IT) */
+ SET(PAN_SLIDE);
+ if (fxp) {
+ if (MSN(fxp) == 0xf) {
+ xc->pan.slide = 0;
+ xc->pan.fslide = LSN(fxp);
+ } else if (LSN(fxp) == 0xf) {
+ xc->pan.slide = 0;
+ xc->pan.fslide = -MSN(fxp);
+ } else {
+ SET(PAN_SLIDE);
+ xc->pan.slide = LSN(fxp) - MSN(fxp);
+ xc->pan.fslide = 0;
+ }
+ }
+ break;
+#endif
+
+ case FX_MULTI_RETRIG: /* Multi retrig */
+ EFFECT_MEMORY_S3M(fxp);
+ if (fxp) {
+ xc->retrig.val = LSN(fxp);
+ xc->retrig.type = MSN(fxp);
+ }
+ if (note) {
+ xc->retrig.count = xc->retrig.val + 1;
+ }
+ xc->retrig.limit = 0;
+ SET(RETRIG);
+ break;
+ case FX_TREMOR: /* Tremor */
+ EFFECT_MEMORY(fxp, xc->tremor.memory);
+ xc->tremor.up = MSN(fxp);
+ xc->tremor.down = LSN(fxp);
+ if (!IS_PLAYER_MODE_FT2()) {
+ if (xc->tremor.up == 0) {
+ xc->tremor.up++;
+ }
+ if (xc->tremor.down == 0) {
+ xc->tremor.down++;
+ }
+ }
+ SET(TREMOR);
+ break;
+ case FX_XF_PORTA: /* Extra fine portamento */
+ h = MSN(fxp);
+ fxp &= 0x0f;
+ switch (h) {
+ case XX_XF_PORTA_UP: /* Extra fine portamento up */
+ EFFECT_MEMORY(fxp, xc->fine_porta.xf_up_memory);
+ fx_xf_porta_up:
+ SET(FINE_BEND);
+ xc->freq.fslide = -0.25 * fxp;
+ break;
+ case XX_XF_PORTA_DN: /* Extra fine portamento down */
+ EFFECT_MEMORY(fxp, xc->fine_porta.xf_down_memory);
+ fx_xf_porta_dn:
+ SET(FINE_BEND);
+ xc->freq.fslide = 0.25 * fxp;
+ break;
+ }
+ break;
+ case FX_SURROUND:
+ xc->pan.surround = fxp;
+ break;
+ case FX_REVERSE: /* Play forward/backward */
+ libxmp_virt_reverse(ctx, chn, fxp);
+ break;
+
+#ifndef LIBXMP_CORE_DISABLE_IT
+ case FX_TRK_VOL: /* Track volume setting */
+ if (fxp <= m->volbase) {
+ xc->mastervol = fxp;
+ }
+ break;
+ case FX_TRK_VSLIDE: /* Track volume slide */
+ if (fxp == 0) {
+ if ((fxp = xc->trackvol.memory) == 0)
+ break;
+ }
+
+ if (HAS_QUIRK(QUIRK_FINEFX)) {
+ h = MSN(fxp);
+ l = LSN(fxp);
+ if (h == 0xf && l != 0) {
+ xc->trackvol.memory = fxp;
+ fxp &= 0x0f;
+ goto fx_trk_fvslide;
+ } else if (l == 0xf && h != 0) {
+ xc->trackvol.memory = fxp;
+ fxp &= 0xf0;
+ goto fx_trk_fvslide;
+ }
+ }
+
+ SET(TRK_VSLIDE);
+ if (fxp) {
+ h = MSN(fxp);
+ l = LSN(fxp);
+
+ xc->trackvol.memory = fxp;
+ if (HAS_QUIRK(QUIRK_VOLPDN)) {
+ xc->trackvol.slide = l ? -l : h;
+ } else {
+ xc->trackvol.slide = h ? h : -l;
+ }
+ }
+
+ break;
+ case FX_TRK_FVSLIDE: /* Track fine volume slide */
+ fx_trk_fvslide:
+ SET(TRK_FVSLIDE);
+ if (fxp) {
+ xc->trackvol.fslide = MSN(fxp) - LSN(fxp);
+ }
+ break;
+
+ case FX_IT_INSTFUNC:
+ switch (fxp) {
+ case 0: /* Past note cut */
+ libxmp_virt_pastnote(ctx, chn, VIRT_ACTION_CUT);
+ break;
+ case 1: /* Past note off */
+ libxmp_virt_pastnote(ctx, chn, VIRT_ACTION_OFF);
+ break;
+ case 2: /* Past note fade */
+ libxmp_virt_pastnote(ctx, chn, VIRT_ACTION_FADE);
+ break;
+ case 3: /* Set NNA to note cut */
+ libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_CUT);
+ break;
+ case 4: /* Set NNA to continue */
+ libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_CONT);
+ break;
+ case 5: /* Set NNA to note off */
+ libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_OFF);
+ break;
+ case 6: /* Set NNA to note fade */
+ libxmp_virt_setnna(ctx, chn, XMP_INST_NNA_FADE);
+ break;
+ case 7: /* Turn off volume envelope */
+ SET_PER(VENV_PAUSE);
+ break;
+ case 8: /* Turn on volume envelope */
+ RESET_PER(VENV_PAUSE);
+ break;
+ case 9: /* Turn off pan envelope */
+ SET_PER(PENV_PAUSE);
+ break;
+ case 0xa: /* Turn on pan envelope */
+ RESET_PER(PENV_PAUSE);
+ break;
+ case 0xb: /* Turn off pitch envelope */
+ SET_PER(FENV_PAUSE);
+ break;
+ case 0xc: /* Turn on pitch envelope */
+ RESET_PER(FENV_PAUSE);
+ break;
+ }
+ break;
+ case FX_FLT_CUTOFF:
+ xc->filter.cutoff = fxp;
+ break;
+ case FX_FLT_RESN:
+ xc->filter.resonance = fxp;
+ break;
+ case FX_MACRO_SET:
+ xc->macro.active = LSN(fxp);
+ break;
+ case FX_MACRO:
+ SET(MIDI_MACRO);
+ xc->macro.val = fxp;
+ xc->macro.slide = 0;
+ break;
+ case FX_MACROSMOOTH:
+ if (ctx->p.speed && xc->macro.val < 0x80) {
+ SET(MIDI_MACRO);
+ xc->macro.target = fxp;
+ xc->macro.slide = ((float)fxp - xc->macro.val) / ctx->p.speed;
+ }
+ break;
+ case FX_PANBRELLO: /* Panbrello */
+ SET(PANBRELLO);
+ SET_LFO_NOTZERO(&xc->panbrello.lfo, LSN(fxp) << 4, MSN(fxp));
+ break;
+ case FX_PANBRELLO_WF: /* Panbrello waveform */
+ libxmp_lfo_set_waveform(&xc->panbrello.lfo, fxp & 3);
+ break;
+ case FX_HIOFFSET: /* High offset */
+ xc->offset.val &= 0xffff;
+ xc->offset.val |= fxp << 16;
+ break;
+#endif
+
+#ifndef LIBXMP_CORE_PLAYER
+
+ /* SFX effects */
+ case FX_VOL_ADD:
+ if (!IS_VALID_INSTRUMENT(xc->ins)) {
+ break;
+ }
+ SET(NEW_VOL);
+ xc->volume = m->mod.xxi[xc->ins].sub[0].vol + fxp;
+ if (xc->volume > m->volbase) {
+ xc->volume = m->volbase;
+ }
+ break;
+ case FX_VOL_SUB:
+ if (!IS_VALID_INSTRUMENT(xc->ins)) {
+ break;
+ }
+ SET(NEW_VOL);
+ xc->volume = m->mod.xxi[xc->ins].sub[0].vol - fxp;
+ if (xc->volume < 0) {
+ xc->volume =0;
+ }
+ break;
+ case FX_PITCH_ADD:
+ SET_PER(TONEPORTA);
+ xc->porta.target = libxmp_note_to_period(ctx, note - 1, xc->finetune, 0)
+ + fxp;
+ xc->porta.slide = 2;
+ xc->porta.dir = 1;
+ break;
+ case FX_PITCH_SUB:
+ SET_PER(TONEPORTA);
+ xc->porta.target = libxmp_note_to_period(ctx, note - 1, xc->finetune, 0)
+ - fxp;
+ xc->porta.slide = 2;
+ xc->porta.dir = -1;
+ break;
+
+ /* Saga Musix says:
+ *
+ * "When both nibbles of an Fxx command are set, SoundTracker 2.6
+ * applies the both values alternatingly, first the high nibble,
+ * then the low nibble on the next row, then the high nibble again...
+ * If only the high nibble is set, it should act like if only the low
+ * nibble is set (i.e. F30 is the same as F03).
+ */
+ case FX_ICE_SPEED:
+ if (fxp) {
+ if (LSN(fxp)) {
+ p->st26_speed = (MSN(fxp) << 8) | LSN(fxp);
+ } else {
+ p->st26_speed = MSN(fxp);
+ }
+ }
+ break;
+
+ case FX_VOLSLIDE_UP: /* Vol slide with uint8 arg */
+ if (HAS_QUIRK(QUIRK_FINEFX)) {
+ h = MSN(fxp);
+ l = LSN(fxp);
+ if (h == 0xf && l != 0) {
+ fxp &= 0x0f;
+ goto fx_f_vslide_up;
+ }
+ }
+
+ if (fxp)
+ xc->vol.slide = fxp;
+ SET(VOL_SLIDE);
+ break;
+ case FX_VOLSLIDE_DN: /* Vol slide with uint8 arg */
+ if (HAS_QUIRK(QUIRK_FINEFX)) {
+ h = MSN(fxp);
+ l = LSN(fxp);
+ if (h == 0xf && l != 0) {
+ fxp &= 0x0f;
+ goto fx_f_vslide_dn;
+ }
+ }
+
+ if (fxp)
+ xc->vol.slide = -fxp;
+ SET(VOL_SLIDE);
+ break;
+ case FX_F_VSLIDE: /* Fine volume slide */
+ SET(FINE_VOLS);
+ if (fxp) {
+ h = MSN(fxp);
+ l = LSN(fxp);
+ xc->vol.fslide = h ? h : -l;
+ }
+ break;
+ case FX_IMF_FPORTA_DN: /* IMF 1/16th precision fine slide down */
+ EFFECT_MEMORY(fxp, xc->freq.memory);
+ if (fxp != 0) {
+ SET(FINE_BEND);
+ xc->freq.fslide = 0.0625 * fxp;
+ }
+ break;
+ case FX_IMF_FPORTA_UP: /* IMF 1/16th precision fine slide up */
+ EFFECT_MEMORY(fxp, xc->freq.memory);
+ if (fxp != 0) {
+ SET(FINE_BEND);
+ xc->freq.fslide = -0.0625 * fxp;
+ }
+ break;
+ case FX_NSLIDE_DN:
+ case FX_NSLIDE_UP:
+ case FX_NSLIDE_R_DN:
+ case FX_NSLIDE_R_UP:
+ if (fxp != 0) {
+ if (fxt == FX_NSLIDE_R_DN || fxt == FX_NSLIDE_R_UP) {
+ xc->retrig.val = MSN(fxp);
+ xc->retrig.count = MSN(fxp) + 1;
+ xc->retrig.type = 0;
+ xc->retrig.limit = 0;
+ }
+
+ if (fxt == FX_NSLIDE_UP || fxt == FX_NSLIDE_R_UP)
+ xc->noteslide.slide = LSN(fxp);
+ else
+ xc->noteslide.slide = -LSN(fxp);
+
+ xc->noteslide.count = xc->noteslide.speed = MSN(fxp);
+ }
+ if (fxt == FX_NSLIDE_R_DN || fxt == FX_NSLIDE_R_UP)
+ SET(RETRIG);
+ SET(NOTE_SLIDE);
+ break;
+ case FX_NSLIDE2_DN:
+ SET(NOTE_SLIDE);
+ xc->noteslide.slide = -fxp;
+ xc->noteslide.count = xc->noteslide.speed = 1;
+ break;
+ case FX_NSLIDE2_UP:
+ SET(NOTE_SLIDE);
+ xc->noteslide.slide = fxp;
+ xc->noteslide.count = xc->noteslide.speed = 1;
+ break;
+ case FX_F_NSLIDE_DN:
+ SET(FINE_NSLIDE);
+ xc->noteslide.fslide = -fxp;
+ break;
+ case FX_F_NSLIDE_UP:
+ SET(FINE_NSLIDE);
+ xc->noteslide.fslide = fxp;
+ break;
+
+ case FX_PER_VIBRATO: /* Persistent vibrato */
+ if (LSN(fxp) != 0) {
+ SET_PER(VIBRATO);
+ } else {
+ RESET_PER(VIBRATO);
+ }
+ SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp) << 2, MSN(fxp));
+ break;
+ case FX_PER_PORTA_UP: /* Persistent portamento up */
+ SET_PER(PITCHBEND);
+ xc->freq.slide = -fxp;
+ if ((xc->freq.memory = fxp) == 0)
+ RESET_PER(PITCHBEND);
+ break;
+ case FX_PER_PORTA_DN: /* Persistent portamento down */
+ SET_PER(PITCHBEND);
+ xc->freq.slide = fxp;
+ if ((xc->freq.memory = fxp) == 0)
+ RESET_PER(PITCHBEND);
+ break;
+ case FX_PER_TPORTA: /* Persistent tone portamento */
+ if (!IS_VALID_INSTRUMENT(xc->ins))
+ break;
+ SET_PER(TONEPORTA);
+ do_toneporta(ctx, xc, note);
+ xc->porta.slide = fxp;
+ if (fxp == 0)
+ RESET_PER(TONEPORTA);
+ break;
+ case FX_PER_VSLD_UP: /* Persistent volslide up */
+ SET_PER(VOL_SLIDE);
+ xc->vol.slide = fxp;
+ if (fxp == 0)
+ RESET_PER(VOL_SLIDE);
+ break;
+ case FX_PER_VSLD_DN: /* Persistent volslide down */
+ SET_PER(VOL_SLIDE);
+ xc->vol.slide = -fxp;
+ if (fxp == 0)
+ RESET_PER(VOL_SLIDE);
+ break;
+ case FX_VIBRATO2: /* Deep vibrato (2x) */
+ SET(VIBRATO);
+ SET_LFO_NOTZERO(&xc->vibrato.lfo, LSN(fxp) << 3, MSN(fxp));
+ break;
+ case FX_MED_RETRIG: /* MED 1Fxy delay x, then retrig every y */
+ /* initial delay is computed at frame loop */
+ SET(RETRIG);
+ xc->retrig.val = LSN(fxp);
+ xc->retrig.count = LSN(fxp) + 1;
+ xc->retrig.type = 0;
+ xc->retrig.limit = 0;
+ break;
+ case FX_SPEED_CP: /* Set speed and ... */
+ if (fxp) {
+ p->speed = fxp;
+ p->st26_speed = 0;
+ }
+ /* fall through */
+ case FX_PER_CANCEL: /* Cancel persistent effects */
+ xc->per_flags = 0;
+ break;
+
+ /* 669 effects */
+
+ case FX_669_PORTA_UP: /* 669 portamento up */
+ SET_PER(PITCHBEND);
+ xc->freq.slide = 80 * fxp;
+ if ((xc->freq.memory = fxp) == 0)
+ RESET_PER(PITCHBEND);
+ break;
+ case FX_669_PORTA_DN: /* 669 portamento down */
+ SET_PER(PITCHBEND);
+ xc->freq.slide = -80 * fxp;
+ if ((xc->freq.memory = fxp) == 0)
+ RESET_PER(PITCHBEND);
+ break;
+ case FX_669_TPORTA: /* 669 tone portamento */
+ if (!IS_VALID_INSTRUMENT(xc->ins))
+ break;
+ SET_PER(TONEPORTA);
+ do_toneporta(ctx, xc, note);
+ xc->porta.slide = 40 * fxp;
+ if (fxp == 0)
+ RESET_PER(TONEPORTA);
+ break;
+ case FX_669_FINETUNE: /* 669 finetune */
+ xc->finetune = 80 * (int8)fxp;
+ break;
+ case FX_669_VIBRATO: /* 669 vibrato */
+ if (LSN(fxp) != 0) {
+ libxmp_lfo_set_waveform(&xc->vibrato.lfo, 669);
+ SET_PER(VIBRATO);
+ } else {
+ RESET_PER(VIBRATO);
+ }
+ SET_LFO_NOTZERO(&xc->vibrato.lfo, 669, 1);
+ break;
+
+ /* ULT effects */
+
+ case FX_ULT_TEMPO: /* ULT tempo */
+ /* Has unusual semantics and is hard to split into multiple
+ * effects, due to ULT's two effects lanes per channel:
+ *
+ * 00: reset both speed and BPM to the default 6/125.
+ * 01-2f: set speed
+ * 30-ff: set BPM (CIA compatible)
+ */
+ if (fxp == 0) {
+ p->speed = 6;
+ p->st26_speed = 0;
+ fxp = 125;
+ } else if (fxp < 0x30) {
+ goto fx_s3m_speed;
+ }
+ goto fx_s3m_bpm;
+ case FX_ULT_TPORTA: /* ULT tone portamento */
+ /* Like normal persistent tone portamento, except:
+ *
+ * 1) Despite the documentation claiming 300 cancels tone
+ * portamento, it actually reuses the last parameter.
+ *
+ * 2) A 3xx without a note will reuse the last target note.
+ */
+ if (!IS_VALID_INSTRUMENT(xc->ins))
+ break;
+ SET_PER(TONEPORTA);
+ EFFECT_MEMORY(fxp, xc->porta.memory);
+ EFFECT_MEMORY(note, xc->porta.note_memory);
+ do_toneporta(ctx, xc, note);
+ xc->porta.slide = fxp;
+ if (fxp == 0)
+ RESET_PER(TONEPORTA);
+ break;
+
+ /* Archimedes (!Tracker, Digital Symphony, et al.) effects */
+
+ case FX_LINE_JUMP: /* !Tracker and Digital Symphony "Line Jump" */
+ libxmp_process_line_jump(ctx, f, p->ord, fxp);
+ break;
+ case FX_RETRIG: /* Retrigger with extended range */
+ goto fx_retrig;
+#endif
+
+ default:
+#ifndef LIBXMP_CORE_PLAYER
+ libxmp_extras_process_fx(ctx, xc, chn, note, e->ins, fxt, fxp, fnum);
+#endif
+ break;
+ }
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/effects.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/effects.h
new file mode 100644
index 00000000..ad6c10c2
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/effects.h
@@ -0,0 +1,174 @@
+#ifndef LIBXMP_EFFECTS_H
+#define LIBXMP_EFFECTS_H
+
+/* Protracker effects */
+#define FX_ARPEGGIO 0x00
+#define FX_PORTA_UP 0x01
+#define FX_PORTA_DN 0x02
+#define FX_TONEPORTA 0x03
+#define FX_VIBRATO 0x04
+#define FX_TONE_VSLIDE 0x05
+#define FX_VIBRA_VSLIDE 0x06
+#define FX_TREMOLO 0x07
+#define FX_OFFSET 0x09
+#define FX_VOLSLIDE 0x0a
+#define FX_JUMP 0x0b
+#define FX_VOLSET 0x0c
+#define FX_BREAK 0x0d
+#define FX_EXTENDED 0x0e
+#define FX_SPEED 0x0f
+
+/* Fast tracker effects */
+#define FX_SETPAN 0x08
+
+/* Fast Tracker II effects */
+#define FX_GLOBALVOL 0x10
+#define FX_GVOL_SLIDE 0x11
+#define FX_KEYOFF 0x14
+#define FX_ENVPOS 0x15
+#define FX_PANSLIDE 0x19
+#define FX_MULTI_RETRIG 0x1b
+#define FX_TREMOR 0x1d
+#define FX_XF_PORTA 0x21
+
+/* Protracker extended effects */
+#define EX_FILTER 0x00
+#define EX_F_PORTA_UP 0x01
+#define EX_F_PORTA_DN 0x02
+#define EX_GLISS 0x03
+#define EX_VIBRATO_WF 0x04
+#define EX_FINETUNE 0x05
+#define EX_PATTERN_LOOP 0x06
+#define EX_TREMOLO_WF 0x07
+#define EX_SETPAN 0x08
+#define EX_RETRIG 0x09
+#define EX_F_VSLIDE_UP 0x0a
+#define EX_F_VSLIDE_DN 0x0b
+#define EX_CUT 0x0c
+#define EX_DELAY 0x0d
+#define EX_PATT_DELAY 0x0e
+#define EX_INVLOOP 0x0f
+
+/* XM extended effects 2 */
+#define XX_XF_PORTA_UP 0x01
+#define XX_XF_PORTA_DN 0x02
+
+#ifndef LIBXMP_CORE_PLAYER
+/* Oktalyzer effects */
+#define FX_OKT_ARP3 0x70
+#define FX_OKT_ARP4 0x71
+#define FX_OKT_ARP5 0x72
+#define FX_NSLIDE2_DN 0x73
+#define FX_NSLIDE2_UP 0x74
+#define FX_F_NSLIDE_DN 0x75
+#define FX_F_NSLIDE_UP 0x76
+
+/* Persistent effects -- for FNK */
+#define FX_PER_PORTA_DN 0x78
+#define FX_PER_PORTA_UP 0x79
+#define FX_PER_TPORTA 0x7a
+#define FX_PER_VIBRATO 0x7b
+#define FX_PER_VSLD_UP 0x7c
+#define FX_PER_VSLD_DN 0x7d
+#define FX_SPEED_CP 0x7e
+#define FX_PER_CANCEL 0x7f
+
+/* 669 frequency based effects */
+#define FX_669_PORTA_UP 0x60
+#define FX_669_PORTA_DN 0x61
+#define FX_669_TPORTA 0x62
+#define FX_669_FINETUNE 0x63
+#define FX_669_VIBRATO 0x64
+
+/* FAR effects */
+#define FX_FAR_PORTA_UP 0x65 /* FAR pitch offset up */
+#define FX_FAR_PORTA_DN 0x66 /* FAR pitch offset down */
+#define FX_FAR_TPORTA 0x67 /* FAR persistent tone portamento */
+#define FX_FAR_TEMPO 0x68 /* FAR coarse tempo and tempo mode */
+#define FX_FAR_F_TEMPO 0x69 /* FAR fine tempo slide up/down */
+#define FX_FAR_VIBDEPTH 0x6a /* FAR set vibrato depth */
+#define FX_FAR_VIBRATO 0x6b /* FAR persistent vibrato */
+#define FX_FAR_SLIDEVOL 0x6c /* FAR persistent slide-to-volume */
+#define FX_FAR_RETRIG 0x6d /* FAR retrigger */
+#define FX_FAR_DELAY 0x6e /* FAR note offset */
+
+/* ULT effects */
+#define FX_ULT_TEMPO 0x5f
+#define FX_ULT_TPORTA 0x6f
+#endif
+
+#ifndef LIBXMP_CORE_DISABLE_IT
+/* IT effects */
+#define FX_TRK_VOL 0x80
+#define FX_TRK_VSLIDE 0x81
+#define FX_TRK_FVSLIDE 0x82
+#define FX_IT_INSTFUNC 0x83
+#define FX_FLT_CUTOFF 0x84
+#define FX_FLT_RESN 0x85
+#define FX_IT_BPM 0x87
+#define FX_IT_ROWDELAY 0x88
+#define FX_IT_PANSLIDE 0x89
+#define FX_PANBRELLO 0x8a
+#define FX_PANBRELLO_WF 0x8b
+#define FX_HIOFFSET 0x8c
+#define FX_IT_BREAK 0x8e /* like FX_BREAK with hex parameter */
+#define FX_MACRO_SET 0xbd /* Set active IT parametered MIDI macro */
+#define FX_MACRO 0xbe /* Execute IT MIDI macro */
+#define FX_MACROSMOOTH 0xbf /* Execute IT MIDI macro slide */
+#endif
+
+#ifndef LIBXMP_CORE_PLAYER
+/* MED effects */
+#define FX_HOLD_DECAY 0x90
+#define FX_SETPITCH 0x91
+#define FX_VIBRATO2 0x92
+#define FX_MED_RETRIG 0x93
+
+/* IMF effects */
+#define FX_IMF_FPORTA_DN 0x9a /* IMF 1/16th fine porta down, 1xx/2xx mem */
+#define FX_IMF_FPORTA_UP 0x9b /* IMF 1/16th fine porta up, 1xx/2xx mem */
+
+/* PTM effects */
+#define FX_NSLIDE_DN 0x9c /* IMF/PTM note slide down */
+#define FX_NSLIDE_UP 0x9d /* IMF/PTM note slide up */
+#define FX_NSLIDE_R_UP 0x9e /* PTM note slide down with retrigger */
+#define FX_NSLIDE_R_DN 0x9f /* PTM note slide up with retrigger */
+
+/* Extra effects */
+#define FX_VOLSLIDE_UP 0xa0 /* SFX, MDL */
+#define FX_VOLSLIDE_DN 0xa1
+#define FX_F_VSLIDE 0xa5 /* IMF/MDL */
+#define FX_CHORUS 0xa9 /* IMF */
+#define FX_ICE_SPEED 0xa2
+#define FX_REVERB 0xaa /* IMF */
+#define FX_MED_HOLD 0xb1 /* MMD hold/decay */
+#define FX_MEGAARP 0xb2 /* Smaksak effect 7: MegaArp */
+#define FX_VOL_ADD 0xb6 /* SFX change volume up */
+#define FX_VOL_SUB 0xb7 /* SFX change volume down */
+#define FX_PITCH_ADD 0xb8 /* SFX add steps to current note */
+#define FX_PITCH_SUB 0xb9 /* SFX add steps to current note */
+#define FX_LINE_JUMP 0xba /* Archimedes jump to line in current order */
+#define FX_RETRIG 0xbb /* Retrigger with extended range (LIQ, DSym) */
+#endif
+
+#define FX_SURROUND 0x8d /* S3M/IT */
+#define FX_REVERSE 0x8f /* XM/IT/others: play forward/reverse */
+#define FX_S3M_SPEED 0xa3 /* S3M */
+#define FX_VOLSLIDE_2 0xa4
+#define FX_FINETUNE 0xa6
+#define FX_S3M_BPM 0xab /* S3M */
+#define FX_FINE_VIBRATO 0xac /* S3M/PTM/IMF/LIQ */
+#define FX_F_VSLIDE_UP 0xad /* MMD */
+#define FX_F_VSLIDE_DN 0xae /* MMD */
+#define FX_F_PORTA_UP 0xaf /* MMD */
+#define FX_F_PORTA_DN 0xb0 /* MMD */
+#define FX_PATT_DELAY 0xb3 /* MMD */
+#define FX_S3M_ARPEGGIO 0xb4
+#define FX_PANSL_NOMEM 0xb5 /* XM volume column */
+
+#define FX_VSLIDE_UP_2 0xc0 /* IT volume column volume slide */
+#define FX_VSLIDE_DN_2 0xc1
+#define FX_F_VSLIDE_UP_2 0xc2
+#define FX_F_VSLIDE_DN_2 0xc3
+
+#endif /* LIBXMP_EFFECTS_H */
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/filetype.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/filetype.c
new file mode 100644
index 00000000..72773722
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/filetype.c
@@ -0,0 +1,145 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "common.h"
+#include
+
+#if defined(_WIN32)
+
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include
+
+int libxmp_get_filetype (const char *path)
+{
+ DWORD result = GetFileAttributesA(path);
+ if (result == (DWORD)(-1)) {
+ errno = ENOENT;
+ return XMP_FILETYPE_NONE;
+ }
+ return (result & FILE_ATTRIBUTE_DIRECTORY) ? XMP_FILETYPE_DIR : XMP_FILETYPE_FILE;
+}
+
+#elif defined(__OS2__) || defined(__EMX__)
+
+#define INCL_DOSFILEMGR
+#include
+
+int libxmp_get_filetype (const char *path)
+{
+ FILESTATUS3 fs;
+ if (DosQueryPathInfo((PCSZ)path, FIL_STANDARD, &fs, sizeof(fs)) != 0) {
+ errno = ENOENT;
+ return XMP_FILETYPE_NONE;
+ }
+ return (fs.attrFile & FILE_DIRECTORY) ? XMP_FILETYPE_DIR : XMP_FILETYPE_FILE;
+}
+
+#elif defined(__DJGPP__)
+
+#include
+#include
+
+int libxmp_get_filetype (const char *path)
+{
+ int attr = _chmod(path, 0);
+ /* Root directories on some non-local drives (e.g. CD-ROM), as well as
+ * devices may fail _chmod, but we are not interested in such cases. */
+ if (attr < 0) return XMP_FILETYPE_NONE;
+ /* we shouldn't hit _A_VOLID ! */
+ return (attr & (_A_SUBDIR|_A_VOLID)) ? XMP_FILETYPE_DIR : XMP_FILETYPE_FILE;
+}
+
+#elif defined(__WATCOMC__) && defined(_DOS)
+
+#include
+#include
+
+int libxmp_get_filetype (const char *path)
+{
+ unsigned int attr;
+ if (_dos_getfileattr(path, &attr)) return XMP_FILETYPE_NONE;
+ return (attr & (_A_SUBDIR|_A_VOLID)) ? XMP_FILETYPE_DIR : XMP_FILETYPE_FILE;
+}
+
+#elif defined(__amigaos4__)
+
+#define __USE_INLINE__
+#include
+
+int libxmp_get_filetype (const char *path)
+{
+ int typ = XMP_FILETYPE_NONE;
+ struct ExamineData *data = ExamineObjectTags(EX_StringNameInput, path, TAG_END);
+ if (data) {
+ if (EXD_IS_FILE(data)) {
+ typ = XMP_FILETYPE_FILE;
+ } else
+ if (EXD_IS_DIRECTORY(data)) {
+ typ = XMP_FILETYPE_DIR;
+ }
+ FreeDosObject(DOS_EXAMINEDATA, data);
+ }
+ if (typ == XMP_FILETYPE_NONE) errno = ENOENT;
+ return typ;
+}
+
+#elif defined(LIBXMP_AMIGA)
+
+#include
+
+int libxmp_get_filetype (const char *path)
+{
+ int typ = XMP_FILETYPE_NONE;
+ BPTR lock = Lock((const STRPTR)path, ACCESS_READ);
+ if (lock) {
+ struct FileInfoBlock *fib = (struct FileInfoBlock *) AllocDosObject(DOS_FIB,NULL);
+ if (fib) {
+ if (Examine(lock, fib)) {
+ typ = (fib->fib_DirEntryType < 0) ? XMP_FILETYPE_FILE : XMP_FILETYPE_DIR;
+ }
+ FreeDosObject(DOS_FIB, fib);
+ }
+ UnLock(lock);
+ }
+ if (typ == XMP_FILETYPE_NONE) errno = ENOENT;
+ return typ;
+}
+
+#else /* unix (ish): */
+
+#include
+#include
+
+int libxmp_get_filetype (const char *path)
+{
+ struct stat st;
+ memset(&st, 0, sizeof(st)); /* silence sanitizers.. */
+ if (stat(path, &st) < 0) return XMP_FILETYPE_NONE;
+ if (S_ISDIR(st.st_mode)) return XMP_FILETYPE_DIR;
+ if (S_ISREG(st.st_mode)) return XMP_FILETYPE_FILE;
+ return XMP_FILETYPE_NONE;
+}
+
+#endif
+
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/filter.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/filter.c
new file mode 100644
index 00000000..ccc24769
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/filter.c
@@ -0,0 +1,118 @@
+/*
+ * Based on the public domain version by Olivier Lapicque
+ * Rewritten for libxmp by Claudio Matsuoka
+ *
+ * Copyright (C) 2012-2025 Claudio Matsuoka
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "common.h"
+
+#ifndef LIBXMP_CORE_DISABLE_IT
+#include
+#include "xmp.h"
+#include "player.h"
+#include "mixer.h"
+
+/*
+ * LUT for 2 * damping factor
+ *
+ * Formula for the table:
+ *
+ * resonance_table[i] = pow(10.0, -((24.0 / 128.0) * i) / 20.0);
+ * or
+ * resonance_table[i] = pow(10.0, -3.0 * i / 320.0);
+ *
+ */
+static const float resonance_table[128] = {
+ 1.0000000000000000f, 0.9786446094512940f, 0.9577452540397644f, 0.9372922182083130f,
+ 0.9172759056091309f, 0.8976871371269226f, 0.8785166740417481f, 0.8597555756568909f,
+ 0.8413951396942139f, 0.8234267830848694f, 0.8058421611785889f, 0.7886331081390381f,
+ 0.7717915177345276f, 0.7553095817565918f, 0.7391796708106995f, 0.7233941555023193f,
+ 0.7079457640647888f, 0.6928272843360901f, 0.6780316829681397f, 0.6635520458221436f,
+ 0.6493816375732422f, 0.6355138421058655f, 0.6219421625137329f, 0.6086603403091431f,
+ 0.5956621170043945f, 0.5829415321350098f, 0.5704925656318665f, 0.5583094954490662f,
+ 0.5463865399360657f, 0.5347182154655457f, 0.5232990980148315f, 0.5121238231658936f,
+ 0.5011872053146362f, 0.4904841780662537f, 0.4800096750259399f, 0.4697588682174683f,
+ 0.4597269892692566f, 0.4499093294143677f, 0.4403013288974762f, 0.4308985173702240f,
+ 0.4216965138912201f, 0.4126909971237183f, 0.4038778245449066f, 0.3952528536319733f,
+ 0.3868120610713959f, 0.3785515129566193f, 0.3704673945903778f, 0.3625559210777283f,
+ 0.3548133969306946f, 0.3472362160682678f, 0.3398208320140839f, 0.3325638175010681f,
+ 0.3254617750644684f, 0.3185114264488220f, 0.3117094635963440f, 0.3050527870655060f,
+ 0.2985382676124573f, 0.2921628654003143f, 0.2859236001968384f, 0.2798175811767578f,
+ 0.2738419771194458f, 0.2679939568042755f, 0.2622708380222321f, 0.2566699385643005f,
+ 0.2511886358261108f, 0.2458244115114212f, 0.2405747324228287f, 0.2354371547698975f,
+ 0.2304092943668366f, 0.2254888117313385f, 0.2206734120845795f, 0.2159608304500580f,
+ 0.2113489061594009f, 0.2068354636430740f, 0.2024184018373489f, 0.1980956792831421f,
+ 0.1938652694225311f, 0.1897251904010773f, 0.1856735348701477f, 0.1817083954811096f,
+ 0.1778279393911362f, 0.1740303486585617f, 0.1703138649463654f, 0.1666767448186874f,
+ 0.1631172895431519f, 0.1596338599920273f, 0.1562248021364212f, 0.1528885662555695f,
+ 0.1496235728263855f, 0.1464282870292664f, 0.1433012634515762f, 0.1402409970760346f,
+ 0.1372461020946503f, 0.1343151479959488f, 0.1314467936754227f, 0.1286396980285645f,
+ 0.1258925348520279f, 0.1232040524482727f, 0.1205729842185974f, 0.1179980933666229f,
+ 0.1154781952500343f, 0.1130121126770973f, 0.1105986908078194f, 0.1082368120551109f,
+ 0.1059253737330437f, 0.1036632955074310f, 0.1014495193958283f, 0.0992830246686935f,
+ 0.0971627980470657f, 0.0950878411531448f, 0.0930572077631950f, 0.0910699293017387f,
+ 0.0891250967979431f, 0.0872217938303947f, 0.0853591337800026f, 0.0835362523794174f,
+ 0.0817523002624512f, 0.0800064504146576f, 0.0782978758215904f, 0.0766257941722870f,
+ 0.0749894231557846f, 0.0733879879117012f, 0.0718207582831383f, 0.0702869966626167f,
+ 0.0687859877943993f, 0.0673170387744904f, 0.0658794566988945f, 0.0644725710153580f,
+};
+
+
+#if !defined(HAVE_POWF) || defined(__DJGPP__) || defined(__WATCOMC__)
+/* Watcom doesn't have powf. DJGPP have a C-only implementation in libm. */
+#undef powf
+#define powf(f1_,f2_) (float)pow((f1_),(f2_))
+#endif
+
+/*
+ * Simple 2-poles resonant filter
+ */
+#define FREQ_PARAM_MULT (128.0f / (24.0f * 256.0f))
+void libxmp_filter_setup(int srate, int cutoff, int res, int *a0, int *b0, int *b1)
+{
+ float fc, fs = (float)srate;
+ float fg, fb0, fb1;
+ float r, d, e;
+
+ /* [0-255] => [100Hz-8000Hz] */
+ CLAMP(cutoff, 0, 255);
+ CLAMP(res, 0, 255);
+
+ fc = 110.0f * powf(2.0f, (float)cutoff * FREQ_PARAM_MULT + 0.25f);
+ if (fc > fs / 2.0f) {
+ fc = fs / 2.0f;
+ }
+
+ r = fs / (2.0 * 3.14159265358979f * fc);
+ d = resonance_table[res >> 1] * (r + 1.0) - 1.0;
+ e = r * r;
+
+ fg = 1.0 / (1.0 + d + e);
+ fb0 = (d + e + e) / (1.0 + d + e);
+ fb1 = -e / (1.0 + d + e);
+
+ *a0 = (int)(fg * (1 << FILTER_SHIFT));
+ *b0 = (int)(fb0 * (1 << FILTER_SHIFT));
+ *b1 = (int)(fb1 * (1 << FILTER_SHIFT));
+}
+
+#endif
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/flow.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/flow.c
new file mode 100644
index 00000000..8da53a15
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/flow.c
@@ -0,0 +1,195 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "common.h"
+#include "player.h"
+
+/* Process a pattern loop effect with the parameter fxp. A parameter of 0
+ * will set the loop target, and a parameter of 1-15 (most formats) or
+ * 1-255 (OctaMED) will perform a loop.
+ *
+ * The compatibility logic for Pattern Loop is complex, so a flow_control
+ * argument is taken such that the scan can use this function directly.
+ *
+ * If the development tests ever start building against effects.c, this
+ * can be moved back to effects.c.
+ */
+void libxmp_process_pattern_loop(struct context_data *ctx,
+ struct flow_control *f, int chn, int row, int fxp)
+{
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ int *start = &f->loop[chn].start;
+ int *count = &f->loop[chn].count;
+ int looped = 0;
+ int i;
+
+ /* Digital Tracker: only the first E60 or E6x is handled per row. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_FIRST_EFFECT) && f->loop_param >= 0) {
+ return;
+ }
+ f->loop_param = fxp;
+
+ /* Scream Tracker 3, Digital Tracker, Octalyser, and probably others
+ * use global loop targets and counts. Later versions of Digital
+ * Tracker use a global target but per-track counts. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_GLOBAL_TARGET)) {
+ start = &f->loop_start;
+ }
+ if (HAS_FLOW_MODE(FLOW_LOOP_GLOBAL_COUNT)) {
+ count = &f->loop_count;
+ }
+
+ if (fxp == 0) {
+ /* mark start of loop */
+ /* Liquid Tracker: M60 is ignored for channels with count >= 1 */
+ if (HAS_FLOW_MODE(FLOW_LOOP_IGNORE_TARGET) && *count >= 1) {
+ return;
+ }
+ *start = row;
+ if (HAS_QUIRK(QUIRK_FT2BUGS))
+ f->jumpline = row;
+ } else {
+ /* end of loop */
+ if (*start < 0) {
+ /* Scream Tracker 3.01b: if SB0 wasn't used, the first
+ * SBx used will set the loop target to its row. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_INIT_SAMEROW)) {
+ *start = row;
+ } else {
+ *start = 0;
+ }
+ }
+
+ if (*count) {
+ if (--(*count)) {
+ f->loop_dest = *start;
+ looped = 1;
+ } else {
+ /* S3M and IT: loop termination advances the
+ * loop target past SBx. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_END_ADVANCES)) {
+ *start = row + 1;
+ }
+ /* Liquid Tracker cancels any other loop jumps
+ * this row started on loop termination. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_END_CANCELS)) {
+ f->loop_dest = -1;
+ }
+ f->loop_active_num--;
+ }
+ } else {
+ /* Modplug Tracker: only begin a loop if no
+ * other channel is currently looping. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_ONE_AT_A_TIME)) {
+ for (i = 0; i < mod->chn; i++) {
+ if (i != chn && f->loop[i].count != 0)
+ return;
+ }
+ }
+ *count = fxp;
+ f->loop_dest = *start;
+ f->loop_active_num++;
+ looped = 1;
+ }
+ }
+
+ /* Hacks for loop jumps altering prior position jumps/breaks. */
+ if (looped && f->pbreak != 0) {
+ /* Many implementations use the same variable for both the
+ * jump/break destination row and the loop destination row. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_SHARED_BREAK)) {
+ f->jumpline = f->loop_dest;
+ }
+ /* Various players e.g. ST3, IT will block prior breaks. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_UNSET_BREAK) && f->jump < 0) {
+ f->pbreak = 0;
+ }
+ /* Various players e.g. ST3, IT will block prior jumps. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_UNSET_JUMP) && f->jump >= 0) {
+ f->pbreak = 0;
+ f->jump = -1;
+ }
+ }
+}
+
+/* Process a pattern jump effect with the parameter fxp.
+ * This function will not actually perform the jump, but it will
+ * prepare the flow variables to perform a jump (unless prevented).
+ */
+void libxmp_process_pattern_jump(struct context_data *ctx,
+ struct flow_control *f, int fxp)
+{
+ struct module_data *m = &ctx->m;
+
+ /* Some formats and trackers e.g. S3M, Modplug Tracker 1.16 will
+ * prevent jumps from being executed when a loop jump occurs. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_DELAY_JUMP) && f->loop_dest >= 0) {
+ return;
+ }
+
+ f->pbreak = 1;
+ f->jump = fxp;
+ if (!HAS_FLOW_MODE(FLOW_JUMP_NO_ROW_SET)) {
+ /* Effect B resets effect D in lower channels.
+ * This does not apply to ST3, IT, ModPlug. */
+ f->jumpline = 0;
+ }
+}
+
+/* Process a pattern break effect with the parameter fxp.
+ * This function will not actually perform the jump, but it will
+ * prepare the flow variables to perform a jump (unless prevented).
+ */
+void libxmp_process_pattern_break(struct context_data *ctx,
+ struct flow_control *f, int fxp)
+{
+ struct module_data *m = &ctx->m;
+
+ /* Some formats and trackers e.g. S3M, IT 2.00+, Modplug Tracker 1.16
+ * will prevent breaks from being executed when a loop jump occurs. */
+ if (HAS_FLOW_MODE(FLOW_LOOP_DELAY_BREAK) && f->loop_dest >= 0) {
+ return;
+ }
+
+ f->pbreak = 1;
+ f->jumpline = fxp;
+}
+
+/* Process a line jump within current position `ord` to row `fxp`.
+ * This function will not actually perform the jump, but it will
+ * prepare the flow variables to perform a jump (unless prevented).
+ */
+void libxmp_process_line_jump(struct context_data *ctx,
+ struct flow_control *f, int ord, int fxp)
+{
+#ifndef LIBXMP_CORE_PLAYER
+ /* In Digital Symphony, this can be combined with position jump
+ * (like pattern break) and overrides the pattern break line in
+ * lower channels. */
+ if (f->pbreak == 0) {
+ f->pbreak = 1;
+ f->jump = ord;
+ }
+ f->jumpline = fxp;
+#endif
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/format.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/format.c
new file mode 100644
index 00000000..a6553236
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/format.c
@@ -0,0 +1,127 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2024 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "format.h"
+#ifndef LIBXMP_NO_PROWIZARD
+#include "loaders/prowizard/prowiz.h"
+#endif
+
+const struct format_loader *const format_loaders[NUM_FORMATS + 2] = {
+ &libxmp_loader_xm,
+ &libxmp_loader_mod,
+#ifndef LIBXMP_CORE_DISABLE_IT
+ &libxmp_loader_it,
+#endif
+ &libxmp_loader_s3m,
+#ifndef LIBXMP_CORE_PLAYER
+ &libxmp_loader_flt,
+ &libxmp_loader_st,
+ &libxmp_loader_stm,
+ &libxmp_loader_stx,
+ &libxmp_loader_mtm,
+ &libxmp_loader_ice,
+ &libxmp_loader_imf,
+ &libxmp_loader_ptm,
+ &libxmp_loader_mdl,
+ &libxmp_loader_ult,
+ &libxmp_loader_liq,
+ &libxmp_loader_no,
+ &libxmp_loader_masi,
+ &libxmp_loader_masi16,
+ &libxmp_loader_muse,
+ &libxmp_loader_gal5,
+ &libxmp_loader_gal4,
+ &libxmp_loader_amf,
+ &libxmp_loader_asylum,
+ &libxmp_loader_gdm,
+ &libxmp_loader_mmd1,
+ &libxmp_loader_mmd3,
+ &libxmp_loader_med2,
+ &libxmp_loader_med3,
+ &libxmp_loader_med4,
+ /* &libxmp_loader_dmf, */
+ &libxmp_loader_chip,
+ &libxmp_loader_rtm,
+ &libxmp_loader_pt3,
+ /* &libxmp_loader_tcb, */
+ &libxmp_loader_dt,
+ /* &libxmp_loader_gtk, */
+ /* &libxmp_loader_dtt, */
+ &libxmp_loader_mgt,
+ &libxmp_loader_arch,
+ &libxmp_loader_sym,
+ &libxmp_loader_digi,
+ &libxmp_loader_dbm,
+ &libxmp_loader_emod,
+ &libxmp_loader_okt,
+ &libxmp_loader_sfx,
+ &libxmp_loader_far,
+ &libxmp_loader_umx,
+ &libxmp_loader_hmn,
+ &libxmp_loader_stim,
+ &libxmp_loader_coco,
+ /* &libxmp_loader_mtp, */
+ &libxmp_loader_ims,
+ &libxmp_loader_669,
+ &libxmp_loader_fnk,
+ /* &libxmp_loader_amd, */
+ /* &libxmp_loader_rad, */
+ /* &libxmp_loader_hsc, */
+ &libxmp_loader_mfp,
+ &libxmp_loader_abk,
+ /* &libxmp_loader_alm, */
+ /* &libxmp_loader_polly, */
+ /* &libxmp_loader_stc, */
+ &libxmp_loader_xmf,
+#ifndef LIBXMP_NO_PROWIZARD
+ &libxmp_loader_pw,
+#endif
+#endif /* LIBXMP_CORE_PLAYER */
+ NULL /* list terminator */
+};
+
+static const char *_farray[NUM_FORMATS + NUM_PW_FORMATS + 1] = { NULL };
+
+const char *const *format_list(void)
+{
+ int count, i;
+
+ if (_farray[0] == NULL) {
+ for (count = i = 0; format_loaders[i] != NULL; i++) {
+#ifndef LIBXMP_NO_PROWIZARD
+ if (strcmp(format_loaders[i]->name, "prowizard") == 0) {
+ int j;
+
+ for (j = 0; pw_formats[j] != NULL; j++) {
+ _farray[count++] = pw_formats[j]->name;
+ }
+ continue;
+ }
+#endif
+ _farray[count++] = format_loaders[i]->name;
+ }
+
+ _farray[count] = NULL;
+ }
+
+ return _farray;
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/format.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/format.h
new file mode 100644
index 00000000..af3a620f
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/format.h
@@ -0,0 +1,107 @@
+#ifndef LIBXMP_FORMAT_H
+#define LIBXMP_FORMAT_H
+
+#include "common.h"
+#include "hio.h"
+
+LIBXMP_BEGIN_DECLS
+
+struct format_loader {
+ const char *name;
+ int (*test)(HIO_HANDLE *, char *, const int);
+ int (*loader)(struct module_data *, HIO_HANDLE *, const int);
+};
+
+extern const struct format_loader *const format_loaders[];
+
+const char *const *format_list(void);
+
+extern const struct format_loader libxmp_loader_xm;
+extern const struct format_loader libxmp_loader_mod;
+extern const struct format_loader libxmp_loader_it;
+extern const struct format_loader libxmp_loader_s3m;
+
+#ifndef LIBXMP_CORE_PLAYER
+extern const struct format_loader libxmp_loader_flt;
+extern const struct format_loader libxmp_loader_st;
+extern const struct format_loader libxmp_loader_stm;
+extern const struct format_loader libxmp_loader_stx;
+extern const struct format_loader libxmp_loader_mtm;
+extern const struct format_loader libxmp_loader_ice;
+extern const struct format_loader libxmp_loader_imf;
+extern const struct format_loader libxmp_loader_ptm;
+extern const struct format_loader libxmp_loader_mdl;
+extern const struct format_loader libxmp_loader_ult;
+extern const struct format_loader libxmp_loader_liq;
+extern const struct format_loader libxmp_loader_no;
+extern const struct format_loader libxmp_loader_masi;
+extern const struct format_loader libxmp_loader_masi16;
+extern const struct format_loader libxmp_loader_muse;
+extern const struct format_loader libxmp_loader_gal5;
+extern const struct format_loader libxmp_loader_gal4;
+extern const struct format_loader libxmp_loader_amf;
+extern const struct format_loader libxmp_loader_asylum;
+extern const struct format_loader libxmp_loader_gdm;
+extern const struct format_loader libxmp_loader_mmd1;
+extern const struct format_loader libxmp_loader_mmd3;
+extern const struct format_loader libxmp_loader_med2;
+extern const struct format_loader libxmp_loader_med3;
+extern const struct format_loader libxmp_loader_med4;
+extern const struct format_loader libxmp_loader_rtm;
+extern const struct format_loader libxmp_loader_pt3;
+extern const struct format_loader libxmp_loader_dt;
+extern const struct format_loader libxmp_loader_mgt;
+extern const struct format_loader libxmp_loader_arch;
+extern const struct format_loader libxmp_loader_sym;
+extern const struct format_loader libxmp_loader_digi;
+extern const struct format_loader libxmp_loader_dbm;
+extern const struct format_loader libxmp_loader_emod;
+extern const struct format_loader libxmp_loader_okt;
+extern const struct format_loader libxmp_loader_sfx;
+extern const struct format_loader libxmp_loader_far;
+extern const struct format_loader libxmp_loader_umx;
+extern const struct format_loader libxmp_loader_stim;
+extern const struct format_loader libxmp_loader_coco;
+extern const struct format_loader libxmp_loader_ims;
+extern const struct format_loader libxmp_loader_669;
+extern const struct format_loader libxmp_loader_fnk;
+extern const struct format_loader libxmp_loader_mfp;
+extern const struct format_loader libxmp_loader_pw;
+extern const struct format_loader libxmp_loader_hmn;
+extern const struct format_loader libxmp_loader_chip;
+extern const struct format_loader libxmp_loader_abk;
+extern const struct format_loader libxmp_loader_xmf;
+#if 0 /* broken / unused, yet. */
+extern const struct format_loader libxmp_loader_dmf;
+extern const struct format_loader libxmp_loader_tcb;
+extern const struct format_loader libxmp_loader_gtk;
+extern const struct format_loader libxmp_loader_dtt;
+extern const struct format_loader libxmp_loader_mtp;
+extern const struct format_loader libxmp_loader_amd;
+extern const struct format_loader libxmp_loader_rad;
+extern const struct format_loader libxmp_loader_hsc;
+extern const struct format_loader libxmp_loader_alm;
+extern const struct format_loader libxmp_loader_polly;
+extern const struct format_loader libxmp_loader_stc;
+#endif
+#endif /* LIBXMP_CORE_PLAYER */
+
+#ifndef LIBXMP_CORE_PLAYER
+#define NUM_FORMATS 52
+#elif !defined(LIBXMP_CORE_DISABLE_IT)
+#define NUM_FORMATS 4
+#else
+#define NUM_FORMATS 3
+#endif
+
+#ifndef LIBXMP_NO_PROWIZARD
+#define NUM_PW_FORMATS 43
+extern const struct pw_format *const pw_formats[];
+int pw_test_format(HIO_HANDLE *, char *, const int, struct xmp_test_info *);
+#else
+#define NUM_PW_FORMATS 0
+#endif
+
+LIBXMP_END_DECLS
+
+#endif /* LIBXMP_FORMAT_H */
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/hio.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/hio.c
new file mode 100644
index 00000000..9f42da57
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/hio.c
@@ -0,0 +1,560 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2024 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include
+#include "common.h"
+#include "hio.h"
+#include "callbackio.h"
+#include "mdataio.h"
+
+static long get_size(FILE *f)
+{
+ long size, pos;
+
+ pos = ftell(f);
+ if (pos >= 0) {
+ if (fseek(f, 0, SEEK_END) < 0) {
+ return -1;
+ }
+ size = ftell(f);
+ if (fseek(f, pos, SEEK_SET) < 0) {
+ return -1;
+ }
+ return size;
+ } else {
+ return pos;
+ }
+}
+
+int8 hio_read8s(HIO_HANDLE *h)
+{
+ int err;
+ int8 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read8s(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread8s(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread8s(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+uint8 hio_read8(HIO_HANDLE *h)
+{
+ int err;
+ uint8 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read8(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread8(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread8(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+uint16 hio_read16l(HIO_HANDLE *h)
+{
+ int err;
+ uint16 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read16l(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread16l(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread16l(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+uint16 hio_read16b(HIO_HANDLE *h)
+{
+ int err;
+ uint16 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read16b(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread16b(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread16b(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+uint32 hio_read24l(HIO_HANDLE *h)
+{
+ int err;
+ uint32 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read24l(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread24l(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread24l(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+uint32 hio_read24b(HIO_HANDLE *h)
+{
+ int err;
+ uint32 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read24b(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread24b(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread24b(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+uint32 hio_read32l(HIO_HANDLE *h)
+{
+ int err;
+ uint32 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read32l(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread32l(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread32l(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+uint32 hio_read32b(HIO_HANDLE *h)
+{
+ int err;
+ uint32 ret;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = read32b(h->handle.file, &err);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread32b(h->handle.mem, &err);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread32b(h->handle.cbfile, &err);
+ break;
+ default:
+ return 0;
+ }
+
+ if (err != 0) {
+ h->error = err;
+ }
+ return ret;
+}
+
+size_t hio_read(void *buf, size_t size, size_t num, HIO_HANDLE *h)
+{
+ size_t ret = 0;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = fread(buf, size, num, h->handle.file);
+ if (ret != num) {
+ if (ferror(h->handle.file)) {
+ h->error = errno;
+ } else {
+ h->error = feof(h->handle.file) ? EOF : -2;
+ }
+ }
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mread(buf, size, num, h->handle.mem);
+ if (ret != num) {
+ h->error = EOF;
+ }
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbread(buf, size, num, h->handle.cbfile);
+ if (ret != num) {
+ h->error = EOF;
+ }
+ break;
+ }
+
+ return ret;
+}
+
+int hio_seek(HIO_HANDLE *h, long offset, int whence)
+{
+ int ret = -1;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = fseek(h->handle.file, offset, whence);
+ if (ret < 0) {
+ h->error = errno;
+ }
+ else if (h->error == EOF) {
+ h->error = 0;
+ }
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mseek(h->handle.mem, offset, whence);
+ if (ret < 0) {
+ h->error = EINVAL;
+ }
+ else if (h->error == EOF) {
+ h->error = 0;
+ }
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbseek(h->handle.cbfile, offset, whence);
+ if (ret < 0) {
+ h->error = EINVAL;
+ }
+ else if (h->error == EOF) {
+ h->error = 0;
+ }
+ break;
+ }
+
+ return ret;
+}
+
+long hio_tell(HIO_HANDLE *h)
+{
+ long ret = -1;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = ftell(h->handle.file);
+ if (ret < 0) {
+ h->error = errno;
+ }
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mtell(h->handle.mem);
+ if (ret < 0) {
+ /* should _not_ happen! */
+ h->error = EINVAL;
+ }
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbtell(h->handle.cbfile);
+ if (ret < 0) {
+ h->error = EINVAL;
+ }
+ break;
+ }
+
+ return ret;
+}
+
+int hio_eof(HIO_HANDLE *h)
+{
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ return feof(h->handle.file);
+ case HIO_HANDLE_TYPE_MEMORY:
+ return meof(h->handle.mem);
+ case HIO_HANDLE_TYPE_CBFILE:
+ return cbeof(h->handle.cbfile);
+ }
+ return EOF;
+}
+
+int hio_error(HIO_HANDLE *h)
+{
+ int error = h->error;
+ h->error = 0;
+ return error;
+}
+
+HIO_HANDLE *hio_open(const char *path, const char *mode)
+{
+ HIO_HANDLE *h;
+
+ h = (HIO_HANDLE *) calloc(1, sizeof(HIO_HANDLE));
+ if (h == NULL)
+ goto err;
+
+ h->type = HIO_HANDLE_TYPE_FILE;
+ h->handle.file = fopen(path, mode);
+ if (h->handle.file == NULL)
+ goto err2;
+
+ h->size = get_size(h->handle.file);
+ if (h->size < 0)
+ goto err3;
+
+ return h;
+
+ err3:
+ fclose(h->handle.file);
+ err2:
+ free(h);
+ err:
+ return NULL;
+}
+
+HIO_HANDLE *hio_open_const_mem(const void *ptr, long size)
+{
+ HIO_HANDLE *h;
+
+ if (size <= 0) return NULL;
+ h = (HIO_HANDLE *) calloc(1, sizeof(HIO_HANDLE));
+ if (h == NULL)
+ return NULL;
+
+ h->type = HIO_HANDLE_TYPE_MEMORY;
+ h->handle.mem = mcopen(ptr, size);
+ h->size = size;
+
+ if (!h->handle.mem) {
+ free(h);
+ h = NULL;
+ }
+
+ return h;
+}
+
+HIO_HANDLE *hio_open_file(FILE *f)
+{
+ HIO_HANDLE *h;
+
+ h = (HIO_HANDLE *) calloc(1, sizeof(HIO_HANDLE));
+ if (h == NULL)
+ return NULL;
+
+ h->noclose = 1;
+ h->type = HIO_HANDLE_TYPE_FILE;
+ h->handle.file = f;
+ h->size = get_size(f);
+ if (h->size < 0) {
+ free(h);
+ return NULL;
+ }
+
+ return h;
+}
+
+HIO_HANDLE *hio_open_file2(FILE *f)
+{
+ HIO_HANDLE *h = hio_open_file(f);
+ if (h != NULL) {
+ h->noclose = 0;
+ }
+ else {
+ fclose(f);
+ }
+ return h;
+}
+
+HIO_HANDLE *hio_open_callbacks(void *priv, struct xmp_callbacks callbacks)
+{
+ HIO_HANDLE *h;
+ CBFILE *f = cbopen(priv, callbacks);
+ if (!f)
+ return NULL;
+
+ h = (HIO_HANDLE *) calloc(1, sizeof(HIO_HANDLE));
+ if (h == NULL) {
+ cbclose(f);
+ return NULL;
+ }
+
+ h->type = HIO_HANDLE_TYPE_CBFILE;
+ h->handle.cbfile = f;
+ h->size = cbfilelength(f);
+ if (h->size < 0) {
+ cbclose(f);
+ free(h);
+ return NULL;
+ }
+ return h;
+}
+
+static int hio_close_internal(HIO_HANDLE *h)
+{
+ int ret = -1;
+
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ ret = (h->noclose)? 0 : fclose(h->handle.file);
+ break;
+ case HIO_HANDLE_TYPE_MEMORY:
+ ret = mclose(h->handle.mem);
+ break;
+ case HIO_HANDLE_TYPE_CBFILE:
+ ret = cbclose(h->handle.cbfile);
+ break;
+ }
+ return ret;
+}
+
+/* hio_close + hio_open_mem. Reuses the same HIO_HANDLE. */
+int hio_reopen_mem(void *ptr, long size, int free_after_use, HIO_HANDLE *h)
+{
+ MFILE *m;
+ int ret;
+ if (size <= 0) return -1;
+
+ m = mopen(ptr, size, free_after_use);
+ if (m == NULL) {
+ return -1;
+ }
+
+ ret = hio_close_internal(h);
+ if (ret < 0) {
+ m->ptr_free = NULL;
+ mclose(m);
+ return ret;
+ }
+
+ h->type = HIO_HANDLE_TYPE_MEMORY;
+ h->handle.mem = m;
+ h->size = size;
+ return 0;
+}
+
+/* hio_close + hio_open_file. Reuses the same HIO_HANDLE. */
+int hio_reopen_file(FILE *f, int close_after_use, HIO_HANDLE *h)
+{
+ long size = get_size(f);
+ int ret;
+ if (size < 0) {
+ return -1;
+ }
+
+ ret = hio_close_internal(h);
+ if (ret < 0) {
+ return -1;
+ }
+
+ h->noclose = !close_after_use;
+ h->type = HIO_HANDLE_TYPE_FILE;
+ h->handle.file = f;
+ h->size = size;
+ return 0;
+}
+
+int hio_close(HIO_HANDLE *h)
+{
+ int ret = hio_close_internal(h);
+ free(h);
+ return ret;
+}
+
+long hio_size(HIO_HANDLE *h)
+{
+ return h->size;
+}
+
+/* Returns a pointer to the underlying continuous memory buffer the entire
+ * contents of HIO_HANDLE `h` are stored at if applicable, otherwise NULL.
+ * Do not reallocate this pointer or modify its underlying data!
+ */
+const unsigned char *hio_get_underlying_memory(HIO_HANDLE *h)
+{
+ switch (HIO_HANDLE_TYPE(h)) {
+ case HIO_HANDLE_TYPE_FILE:
+ case HIO_HANDLE_TYPE_CBFILE:
+ return NULL;
+ case HIO_HANDLE_TYPE_MEMORY:
+ return h->handle.mem->start;
+ }
+ return NULL;
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/hio.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/hio.h
new file mode 100644
index 00000000..a5107439
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/hio.h
@@ -0,0 +1,55 @@
+#ifndef XMP_HIO_H
+#define XMP_HIO_H
+
+#include "callbackio.h"
+#include "memio.h"
+
+#define HIO_HANDLE_TYPE(x) ((x)->type)
+
+enum hio_type {
+ HIO_HANDLE_TYPE_FILE,
+ HIO_HANDLE_TYPE_MEMORY,
+ HIO_HANDLE_TYPE_CBFILE
+};
+
+typedef struct {
+ enum hio_type type;
+ long size;
+ union {
+ FILE *file;
+ MFILE *mem;
+ CBFILE *cbfile;
+ } handle;
+ int error;
+ int noclose;
+} HIO_HANDLE;
+
+LIBXMP_BEGIN_DECLS
+
+int8 hio_read8s (HIO_HANDLE *);
+uint8 hio_read8 (HIO_HANDLE *);
+uint16 hio_read16l (HIO_HANDLE *);
+uint16 hio_read16b (HIO_HANDLE *);
+uint32 hio_read24l (HIO_HANDLE *);
+uint32 hio_read24b (HIO_HANDLE *);
+uint32 hio_read32l (HIO_HANDLE *);
+uint32 hio_read32b (HIO_HANDLE *);
+size_t hio_read (void *, size_t, size_t, HIO_HANDLE *);
+int hio_seek (HIO_HANDLE *, long, int);
+long hio_tell (HIO_HANDLE *);
+int hio_eof (HIO_HANDLE *);
+int hio_error (HIO_HANDLE *);
+HIO_HANDLE *hio_open (const char *, const char *);
+HIO_HANDLE *hio_open_const_mem (const void *, long);
+HIO_HANDLE *hio_open_file (FILE *);
+HIO_HANDLE *hio_open_file2 (FILE *);/* allows fclose()ing the file by libxmp */
+HIO_HANDLE *hio_open_callbacks (void *, struct xmp_callbacks);
+int hio_reopen_mem (void *, long, int, HIO_HANDLE *);
+int hio_reopen_file (FILE *, int, HIO_HANDLE *);
+int hio_close (HIO_HANDLE *);
+long hio_size (HIO_HANDLE *);
+const unsigned char *hio_get_underlying_memory(HIO_HANDLE *);
+
+LIBXMP_END_DECLS
+
+#endif
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/lfo.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/lfo.c
new file mode 100644
index 00000000..009e7e74
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/lfo.c
@@ -0,0 +1,164 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2024 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include "lfo.h"
+#include "rng.h"
+
+#define WAVEFORM_SIZE 64
+
+static const int sine_wave[WAVEFORM_SIZE] = {
+ 0, 24, 49, 74, 97, 120, 141, 161, 180, 197, 212, 224,
+ 235, 244, 250, 253, 255, 253, 250, 244, 235, 224, 212, 197,
+ 180, 161, 141, 120, 97, 74, 49, 24, 0, -24, -49, -74,
+ -97,-120,-141,-161,-180,-197,-212,-224,-235,-244,-250,-253,
+ -255,-253,-250,-244,-235,-224,-212,-197,-180,-161,-141,-120,
+ -97, -74, -49, -24
+};
+
+/* LFO */
+
+static int get_lfo_mod(struct context_data *ctx, struct lfo *lfo)
+{
+ int val;
+
+ if (lfo->rate == 0)
+ return 0;
+
+ switch (lfo->type) {
+ case 0: /* sine */
+ val = sine_wave[lfo->phase];
+ break;
+ case 1: /* ramp down */
+ val = 255 - (lfo->phase << 3);
+ break;
+ case 2: /* square */
+ val = lfo->phase < WAVEFORM_SIZE / 2 ? 255 : -255;
+ break;
+ case 3: /* random */
+ val = libxmp_get_random(&ctx->rng, 512) - 256;
+ break;
+#ifndef LIBXMP_CORE_PLAYER
+ case 669: /* 669 vibrato */
+ val = lfo->phase & 1;
+ break;
+#endif
+ default:
+ return 0;
+ }
+
+ return val * lfo->depth;
+}
+
+static int get_lfo_st3(struct context_data *ctx, struct lfo *lfo)
+{
+ if (lfo->rate == 0) {
+ return 0;
+ }
+
+ /* S3M square */
+ if (lfo->type == 2) {
+ int val = lfo->phase < WAVEFORM_SIZE / 2 ? 255 : 0;
+ return val * lfo->depth;
+ }
+
+ return get_lfo_mod(ctx, lfo);
+}
+
+/* From OpenMPT VibratoWaveforms.xm:
+ * "Generally the vibrato and tremolo tables are identical to those that
+ * ProTracker uses, but the vibrato’s “ramp down” table is upside down."
+ */
+static int get_lfo_ft2(struct context_data *ctx, struct lfo *lfo)
+{
+ if (lfo->rate == 0)
+ return 0;
+
+ /* FT2 ramp */
+ if (lfo->type == 1) {
+ int phase = (lfo->phase + (WAVEFORM_SIZE >> 1)) % WAVEFORM_SIZE;
+ int val = (phase << 3) - 255;
+ return val * lfo->depth;
+ }
+
+ return get_lfo_mod(ctx, lfo);
+}
+
+#ifndef LIBXMP_CORE_DISABLE_IT
+
+static int get_lfo_it(struct context_data *ctx, struct lfo *lfo)
+{
+ if (lfo->rate == 0)
+ return 0;
+
+ return get_lfo_st3(ctx, lfo);
+}
+
+#endif
+
+int libxmp_lfo_get(struct context_data *ctx, struct lfo *lfo, int is_vibrato)
+{
+ struct module_data *m = &ctx->m;
+
+ switch (m->read_event_type) {
+ case READ_EVENT_ST3:
+ return get_lfo_st3(ctx, lfo);
+ case READ_EVENT_FT2:
+ if (is_vibrato) {
+ return get_lfo_ft2(ctx, lfo);
+ } else {
+ return get_lfo_mod(ctx, lfo);
+ }
+#ifndef LIBXMP_CORE_DISABLE_IT
+ case READ_EVENT_IT:
+ return get_lfo_it(ctx, lfo);
+#endif
+ default:
+ return get_lfo_mod(ctx, lfo);
+ }
+}
+
+
+void libxmp_lfo_update(struct lfo *lfo)
+{
+ lfo->phase += lfo->rate;
+ lfo->phase &= WAVEFORM_SIZE - 1; /* Rate may be negative, don't %= */
+}
+
+void libxmp_lfo_set_phase(struct lfo *lfo, int phase)
+{
+ lfo->phase = phase;
+}
+
+void libxmp_lfo_set_depth(struct lfo *lfo, int depth)
+{
+ lfo->depth = depth;
+}
+
+void libxmp_lfo_set_rate(struct lfo *lfo, int rate)
+{
+ lfo->rate = rate;
+}
+
+void libxmp_lfo_set_waveform(struct lfo *lfo, int type)
+{
+ lfo->type = type;
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/lfo.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/lfo.h
new file mode 100644
index 00000000..cbc78f72
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/lfo.h
@@ -0,0 +1,24 @@
+#ifndef LIBXMP_LFO_H
+#define LIBXMP_LFO_H
+
+#include "common.h"
+
+struct lfo {
+ int type;
+ int rate;
+ int depth;
+ int phase;
+};
+
+LIBXMP_BEGIN_DECLS
+
+int libxmp_lfo_get(struct context_data *, struct lfo *, int);
+void libxmp_lfo_update(struct lfo *);
+void libxmp_lfo_set_phase(struct lfo *, int);
+void libxmp_lfo_set_depth(struct lfo *, int);
+void libxmp_lfo_set_rate(struct lfo *, int);
+void libxmp_lfo_set_waveform(struct lfo *, int);
+
+LIBXMP_END_DECLS
+
+#endif
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/load.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/load.c
new file mode 100644
index 00000000..83c7f8fc
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/load.c
@@ -0,0 +1,575 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2025 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include
+
+#include "format.h"
+#include "hio.h"
+#include "loaders/loader.h"
+
+#ifndef LIBXMP_NO_DEPACKERS
+#include "tempfile.h"
+#include "depackers/depacker.h"
+#endif
+
+#ifndef LIBXMP_CORE_PLAYER
+#include "md5.h"
+#include "extras.h"
+#endif
+
+
+#ifndef LIBXMP_CORE_PLAYER
+#define BUFLEN 16384
+
+static void set_md5sum(HIO_HANDLE *f, unsigned char *digest)
+{
+ unsigned char buf[BUFLEN];
+ MD5_CTX ctx;
+ int bytes_read;
+
+ hio_seek(f, 0, SEEK_SET);
+
+ MD5Init(&ctx);
+ while ((bytes_read = hio_read(buf, 1, BUFLEN, f)) > 0) {
+ MD5Update(&ctx, buf, bytes_read);
+ }
+ MD5Final(digest, &ctx);
+}
+
+static char *get_dirname(const char *name)
+{
+ char *dirname;
+ const char *p;
+ ptrdiff_t len;
+
+ if ((p = strrchr(name, '/')) != NULL) {
+ len = p - name + 1;
+ dirname = (char *) malloc(len + 1);
+ if (dirname != NULL) {
+ memcpy(dirname, name, len);
+ dirname[len] = 0;
+ }
+ } else {
+ dirname = libxmp_strdup("");
+ }
+
+ return dirname;
+}
+
+static char *get_basename(const char *name)
+{
+ const char *p;
+ char *basename;
+
+ if ((p = strrchr(name, '/')) != NULL) {
+ basename = libxmp_strdup(p + 1);
+ } else {
+ basename = libxmp_strdup(name);
+ }
+
+ return basename;
+}
+#endif /* LIBXMP_CORE_PLAYER */
+
+static int test_module(struct xmp_test_info *info, HIO_HANDLE *h)
+{
+ char buf[XMP_NAME_SIZE];
+ int i;
+
+ if (info != NULL) {
+ *info->name = 0; /* reset name prior to testing */
+ *info->type = 0; /* reset type prior to testing */
+ }
+
+ for (i = 0; format_loaders[i] != NULL; i++) {
+ hio_seek(h, 0, SEEK_SET);
+ if (format_loaders[i]->test(h, buf, 0) == 0) {
+ int is_prowizard = 0;
+
+#ifndef LIBXMP_NO_PROWIZARD
+ if (strcmp(format_loaders[i]->name, "prowizard") == 0) {
+ hio_seek(h, 0, SEEK_SET);
+ pw_test_format(h, buf, 0, info);
+ is_prowizard = 1;
+ }
+#endif
+
+ if (info != NULL && !is_prowizard) {
+ strncpy(info->name, buf, XMP_NAME_SIZE - 1);
+ info->name[XMP_NAME_SIZE - 1] = '\0';
+
+ strncpy(info->type, format_loaders[i]->name,
+ XMP_NAME_SIZE - 1);
+ info->type[XMP_NAME_SIZE - 1] = '\0';
+ }
+ return 0;
+ }
+ }
+ return -XMP_ERROR_FORMAT;
+}
+
+int xmp_test_module(const char *path, struct xmp_test_info *info)
+{
+ HIO_HANDLE *h;
+#ifndef LIBXMP_NO_DEPACKERS
+ char *temp = NULL;
+#endif
+ int ret;
+
+ ret = libxmp_get_filetype(path);
+
+ if (ret == XMP_FILETYPE_NONE) {
+ return -XMP_ERROR_SYSTEM;
+ }
+ if (ret & XMP_FILETYPE_DIR) {
+ errno = EISDIR;
+ return -XMP_ERROR_SYSTEM;
+ }
+
+ if ((h = hio_open(path, "rb")) == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+#ifndef LIBXMP_NO_DEPACKERS
+ if (libxmp_decrunch(h, path, &temp) < 0) {
+ ret = -XMP_ERROR_DEPACK;
+ goto err;
+ }
+#endif
+
+ ret = test_module(info, h);
+
+#ifndef LIBXMP_NO_DEPACKERS
+ err:
+ hio_close(h);
+ unlink_temp_file(temp);
+#else
+ hio_close(h);
+#endif
+ return ret;
+}
+
+int xmp_test_module_from_memory(const void *mem, long size, struct xmp_test_info *info)
+{
+ HIO_HANDLE *h;
+ int ret;
+
+ if (size <= 0) {
+ return -XMP_ERROR_INVALID;
+ }
+
+ if ((h = hio_open_const_mem(mem, size)) == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+ ret = test_module(info, h);
+
+ hio_close(h);
+ return ret;
+}
+
+int xmp_test_module_from_file(void *file, struct xmp_test_info *info)
+{
+ HIO_HANDLE *h;
+ int ret;
+#ifndef LIBXMP_NO_DEPACKERS
+ char *temp = NULL;
+#endif
+
+ if ((h = hio_open_file((FILE *)file)) == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+#ifndef LIBXMP_NO_DEPACKERS
+ if (libxmp_decrunch(h, NULL, &temp) < 0) {
+ ret = -XMP_ERROR_DEPACK;
+ goto err;
+ }
+#endif
+
+ ret = test_module(info, h);
+
+#ifndef LIBXMP_NO_DEPACKERS
+ err:
+ hio_close(h);
+ unlink_temp_file(temp);
+#else
+ hio_close(h);
+#endif
+ return ret;
+}
+
+int xmp_test_module_from_callbacks(void *priv, struct xmp_callbacks callbacks,
+ struct xmp_test_info *info)
+{
+ HIO_HANDLE *h;
+ int ret;
+
+ if ((h = hio_open_callbacks(priv, callbacks)) == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+ ret = test_module(info, h);
+
+ hio_close(h);
+ return ret;
+}
+
+static int load_module(xmp_context opaque, HIO_HANDLE *h)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ int i, j, ret;
+ int test_result, load_result;
+
+ libxmp_load_prologue(ctx);
+
+ D_(D_WARN "load");
+ test_result = load_result = -1;
+ for (i = 0; format_loaders[i] != NULL; i++) {
+ hio_seek(h, 0, SEEK_SET);
+
+ D_(D_WARN "test %s", format_loaders[i]->name);
+ test_result = format_loaders[i]->test(h, NULL, 0);
+ if (test_result == 0) {
+ hio_seek(h, 0, SEEK_SET);
+ D_(D_WARN "load format: %s", format_loaders[i]->name);
+ load_result = format_loaders[i]->loader(m, h, 0);
+ break;
+ }
+ }
+
+ if (test_result < 0) {
+ xmp_release_module(opaque);
+ return -XMP_ERROR_FORMAT;
+ }
+
+ if (load_result < 0) {
+ goto err_load;
+ }
+
+ /* Sanity check: number of channels, module length */
+ if (mod->chn > XMP_MAX_CHANNELS || mod->len > XMP_MAX_MOD_LENGTH) {
+ goto err_load;
+ }
+
+ /* Sanity check: channel pan */
+ for (i = 0; i < mod->chn; i++) {
+ if (mod->xxc[i].vol < 0 || mod->xxc[i].vol > 0xff) {
+ goto err_load;
+ }
+ if (mod->xxc[i].pan < 0 || mod->xxc[i].pan > 0xff) {
+ goto err_load;
+ }
+ }
+
+ /* Sanity check: patterns */
+ if (mod->xxp == NULL) {
+ goto err_load;
+ }
+ for (i = 0; i < mod->pat; i++) {
+ if (mod->xxp[i] == NULL) {
+ goto err_load;
+ }
+ for (j = 0; j < mod->chn; j++) {
+ int t = mod->xxp[i]->index[j];
+ if (t < 0 || t >= mod->trk || mod->xxt[t] == NULL) {
+ goto err_load;
+ }
+ }
+ }
+
+ libxmp_adjust_string(mod->name);
+ for (i = 0; i < mod->ins; i++) {
+ libxmp_adjust_string(mod->xxi[i].name);
+ }
+ for (i = 0; i < mod->smp; i++) {
+ libxmp_adjust_string(mod->xxs[i].name);
+ }
+
+#ifndef LIBXMP_CORE_PLAYER
+ if (test_result == 0 && load_result == 0)
+ set_md5sum(h, m->md5);
+#endif
+
+ libxmp_load_epilogue(ctx);
+
+ ret = libxmp_prepare_scan(ctx);
+ if (ret < 0) {
+ xmp_release_module(opaque);
+ return ret;
+ }
+
+ ret = libxmp_scan_sequences(ctx);
+ if (ret < 0) {
+ xmp_release_module(opaque);
+ return -XMP_ERROR_LOAD;
+ }
+
+ ctx->state = XMP_STATE_LOADED;
+
+ return 0;
+
+ err_load:
+ xmp_release_module(opaque);
+ return -XMP_ERROR_LOAD;
+}
+
+int xmp_load_module(xmp_context opaque, const char *path)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+#ifndef LIBXMP_CORE_PLAYER
+ struct module_data *m = &ctx->m;
+#endif
+#ifndef LIBXMP_NO_DEPACKERS
+ char *temp_name;
+#endif
+ HIO_HANDLE *h;
+ int ret;
+
+ D_(D_WARN "path = %s", path);
+
+ ret = libxmp_get_filetype(path);
+
+ if (ret == XMP_FILETYPE_NONE) {
+ return -XMP_ERROR_SYSTEM;
+ }
+ if (ret & XMP_FILETYPE_DIR) {
+ errno = EISDIR;
+ return -XMP_ERROR_SYSTEM;
+ }
+
+ if ((h = hio_open(path, "rb")) == NULL) {
+ return -XMP_ERROR_SYSTEM;
+ }
+
+#ifndef LIBXMP_NO_DEPACKERS
+ D_(D_INFO "decrunch");
+ if (libxmp_decrunch(h, path, &temp_name) < 0) {
+ ret = -XMP_ERROR_DEPACK;
+ goto err;
+ }
+#endif
+
+ if (ctx->state > XMP_STATE_UNLOADED)
+ xmp_release_module(opaque);
+
+#ifndef LIBXMP_CORE_PLAYER
+ m->dirname = get_dirname(path);
+ if (m->dirname == NULL) {
+ ret = -XMP_ERROR_SYSTEM;
+ goto err;
+ }
+
+ m->basename = get_basename(path);
+ if (m->basename == NULL) {
+ ret = -XMP_ERROR_SYSTEM;
+ goto err;
+ }
+
+ m->filename = path; /* For ALM, SSMT, etc */
+ m->size = hio_size(h);
+#else
+ ctx->m.filename = NULL;
+ ctx->m.dirname = NULL;
+ ctx->m.basename = NULL;
+#endif
+
+ ret = load_module(opaque, h);
+ hio_close(h);
+
+#ifndef LIBXMP_NO_DEPACKERS
+ unlink_temp_file(temp_name);
+#endif
+
+ return ret;
+
+#ifndef LIBXMP_CORE_PLAYER
+ err:
+ hio_close(h);
+#ifndef LIBXMP_NO_DEPACKERS
+ unlink_temp_file(temp_name);
+#endif
+ return ret;
+#endif
+}
+
+int xmp_load_module_from_memory(xmp_context opaque, const void *mem, long size)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+ HIO_HANDLE *h;
+ int ret;
+
+ if (size <= 0) {
+ return -XMP_ERROR_INVALID;
+ }
+
+ if ((h = hio_open_const_mem(mem, size)) == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+ if (ctx->state > XMP_STATE_UNLOADED)
+ xmp_release_module(opaque);
+
+ m->filename = NULL;
+ m->basename = NULL;
+ m->dirname = NULL;
+ m->size = size;
+
+ ret = load_module(opaque, h);
+
+ hio_close(h);
+
+ return ret;
+}
+
+int xmp_load_module_from_file(xmp_context opaque, void *file, long size)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+ HIO_HANDLE *h;
+ int ret;
+
+ if ((h = hio_open_file((FILE *)file)) == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+ if (ctx->state > XMP_STATE_UNLOADED)
+ xmp_release_module(opaque);
+
+ m->filename = NULL;
+ m->basename = NULL;
+ m->dirname = NULL;
+ m->size = hio_size(h);
+
+ ret = load_module(opaque, h);
+
+ hio_close(h);
+
+ return ret;
+}
+
+int xmp_load_module_from_callbacks(xmp_context opaque, void *priv,
+ struct xmp_callbacks callbacks)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+ HIO_HANDLE *h;
+ int ret;
+
+ if ((h = hio_open_callbacks(priv, callbacks)) == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+ if (ctx->state > XMP_STATE_UNLOADED)
+ xmp_release_module(opaque);
+
+ m->filename = NULL;
+ m->basename = NULL;
+ m->dirname = NULL;
+ m->size = hio_size(h);
+
+ ret = load_module(opaque, h);
+
+ hio_close(h);
+
+ return ret;
+}
+
+void xmp_release_module(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ int i;
+
+ /* can't test this here, we must call release_module to clean up
+ * load errors
+ if (ctx->state < XMP_STATE_LOADED)
+ return;
+ */
+
+ if (ctx->state > XMP_STATE_LOADED)
+ xmp_end_player(opaque);
+
+ ctx->state = XMP_STATE_UNLOADED;
+
+ D_(D_INFO "Freeing memory");
+
+#ifndef LIBXMP_CORE_PLAYER
+ libxmp_release_module_extras(ctx);
+#endif
+
+ if (mod->xxt != NULL) {
+ for (i = 0; i < mod->trk; i++) {
+ free(mod->xxt[i]);
+ }
+ free(mod->xxt);
+ mod->xxt = NULL;
+ }
+
+ if (mod->xxp != NULL) {
+ for (i = 0; i < mod->pat; i++) {
+ free(mod->xxp[i]);
+ }
+ free(mod->xxp);
+ mod->xxp = NULL;
+ }
+
+ if (mod->xxi != NULL) {
+ for (i = 0; i < mod->ins; i++) {
+ free(mod->xxi[i].sub);
+ free(mod->xxi[i].extra);
+ }
+ free(mod->xxi);
+ mod->xxi = NULL;
+ }
+
+ if (mod->xxs != NULL) {
+ for (i = 0; i < mod->smp; i++) {
+ libxmp_free_sample(&mod->xxs[i]);
+ }
+ free(mod->xxs);
+ mod->xxs = NULL;
+ }
+
+ free(m->xtra);
+ free(m->midi);
+ m->xtra = NULL;
+ m->midi = NULL;
+
+ libxmp_free_scan(ctx);
+
+ free(m->comment);
+ m->comment = NULL;
+
+ D_("free dirname/basename");
+ free(m->dirname);
+ free(m->basename);
+ m->basename = NULL;
+ m->dirname = NULL;
+}
+
+void xmp_scan_module(xmp_context opaque)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+
+ if (ctx->state < XMP_STATE_LOADED)
+ return;
+
+ libxmp_scan_sequences(ctx);
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/load_helpers.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/load_helpers.c
new file mode 100644
index 00000000..a9ea2c79
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/load_helpers.c
@@ -0,0 +1,572 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2025 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include
+#include "common.h"
+#include "loaders/loader.h"
+
+
+#ifndef LIBXMP_CORE_PLAYER
+
+/*
+ * Handle special "module quirks" that can't be detected automatically
+ * such as Protracker 2.x compatibility, vblank timing, etc.
+ */
+
+struct module_quirk {
+ uint8 md5[16];
+ int flags;
+ int mode;
+};
+
+const struct module_quirk mq[] = {
+ /* "No Mercy" by Alf/VTL (added by Martin Willers) */
+ {
+ { 0x36, 0x6e, 0xc0, 0xfa, 0x96, 0x2a, 0xeb, 0xee,
+ 0x03, 0x4a, 0xa2, 0xdb, 0xaa, 0x49, 0xaa, 0xea },
+ 0, XMP_MODE_PROTRACKER
+ },
+
+ /* mod.souvenir of china */
+ {
+ { 0x93, 0xf1, 0x46, 0xae, 0xb7, 0x58, 0xc3, 0x9d,
+ 0x8b, 0x5f, 0xbc, 0x98, 0xbf, 0x23, 0x7a, 0x43 },
+ XMP_FLAGS_FIXLOOP, XMP_MODE_AUTO
+ },
+
+#if 0
+ /* "siedler ii" (added by Daniel Åkerud) */
+ /* Timing fixed by vblank scan compare. CIA: 32m10s VBlank: 12m32s */
+ {
+ { 0x70, 0xaa, 0x03, 0x4d, 0xfb, 0x2f, 0x1f, 0x73,
+ 0xd9, 0xfd, 0xba, 0xfe, 0x13, 0x1b, 0xb7, 0x01 },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+#endif
+
+ /* "Klisje paa klisje" (added by Kjetil Torgrim Homme) */
+ {
+ { 0xe9, 0x98, 0x01, 0x2c, 0x70, 0x0e, 0xb4, 0x3a,
+ 0xf0, 0x32, 0x17, 0x11, 0x30, 0x58, 0x29, 0xb2 },
+ 0, XMP_MODE_NOISETRACKER
+ },
+
+#if 0
+ /* -- Already covered by Noisetracker fingerprinting -- */
+
+ /* Another version of Klisje paa klisje sent by Steve Fernandez */
+ {
+ { 0x12, 0x19, 0x1c, 0x90, 0x41, 0xe3, 0xfd, 0x70,
+ 0xb7, 0xe6, 0xb3, 0x94, 0x8b, 0x21, 0x07, 0x63 },
+ XMP_FLAGS_VBLANK
+ },
+#endif
+
+ /* "((((( nebulos )))))" sent by Tero Auvinen (AMP version) */
+ {
+ { 0x51, 0x6e, 0x8d, 0xcc, 0x35, 0x7d, 0x50, 0xde,
+ 0xa9, 0x85, 0xbe, 0xbf, 0x90, 0x2e, 0x42, 0xdc },
+ 0, XMP_MODE_NOISETRACKER
+ },
+
+ /* Purple Motion's Sundance.mod, Music Channel BBS edit */
+ {
+ { 0x5d, 0x3e, 0x1e, 0x08, 0x28, 0x52, 0x12, 0xc7,
+ 0x17, 0x64, 0x95, 0x75, 0x98, 0xe6, 0x95, 0xc1 },
+ 0, XMP_MODE_ST3
+ },
+
+ /* Asle's Ode to Protracker */
+ {
+ { 0x97, 0xa3, 0x7d, 0x30, 0xd7, 0xae, 0x6d, 0x50,
+ 0xc9, 0x62, 0xe9, 0xd8, 0x87, 0x1b, 0x7e, 0x8a },
+ 0, XMP_MODE_PROTRACKER
+ },
+
+ /* grooving3.mod */
+ /* length 150778 crc32c 0xfdcf9aadU */
+ {
+ { 0xdb, 0x61, 0x22, 0x44, 0x39, 0x85, 0x74, 0xe9,
+ 0xfa, 0x11, 0xb8, 0xfb, 0x87, 0xe8, 0xde, 0xc5, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* mod.Rundgren */
+ /* length 195078 crc32c 0x8fa827a4U */
+ {
+ { 0x9a, 0xdb, 0xb2, 0x09, 0x07, 0x1c, 0x44, 0x82,
+ 0xc5, 0xdf, 0x83, 0x52, 0xcc, 0x73, 0x9f, 0x20, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* dance feeling by Audiomonster */
+ /* length 169734 crc32c 0x79fa2c9bU */
+ {
+ { 0x31, 0x2c, 0x3d, 0xaa, 0x5f, 0x1a, 0x54, 0x44,
+ 0x9d, 0xf7, 0xc4, 0x41, 0x8a, 0xc5, 0x01, 0x02, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* knights melody by Audiomonster */
+ /* length 77798 crc32c 0x7bf19c5bU */
+ {
+ { 0x31, 0xc3, 0x0e, 0x32, 0xfc, 0x99, 0x95, 0xd2,
+ 0x97, 0x20, 0xb3, 0x77, 0x50, 0x05, 0xfe, 0xa5, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* hcomme by Bouffon */
+ /* length 71346 crc32c 0x4ad49cb3U */
+ {
+ { 0x6e, 0xf9, 0x78, 0xc1, 0x80, 0xae, 0x51, 0x06,
+ 0x05, 0x7c, 0x6e, 0xd0, 0x26, 0x7e, 0xfe, 0x3d, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* ((((aquapool)))) by Dolphin */
+ /* length 62932 crc32c 0x05b103fcU */
+ {
+ { 0xff, 0x0b, 0xe0, 0x26, 0xc6, 0x31, 0xb5, 0x9b,
+ 0x94, 0x83, 0x94, 0x99, 0x7e, 0x24, 0x7c, 0xdd, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* 100yarddash by Dr. Awesome */
+ /* length 104666 crc32c 0xd2b0e4a6U */
+ {
+ { 0x5b, 0xff, 0x2f, 0xb8, 0xef, 0x3c, 0xbe, 0x55,
+ 0xa8, 0xe2, 0xa7, 0xcf, 0x5c, 0xbd, 0xdd, 0xb2, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* jazz-reggae-funk by Droid */
+ /* length 115564 crc32c 0x41ff635fU */
+ {
+ { 0xe5, 0x6e, 0x31, 0x2f, 0x62, 0x80, 0xc1, 0x9d,
+ 0x2f, 0x24, 0x54, 0xf3, 0x89, 0x3f, 0x94, 0x6c, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* hard and heavy by Fish */
+ /* length 69814 crc32c 0x1f09d3d5U */
+ {
+ { 0x6b, 0xce, 0x39, 0x94, 0x75, 0x42, 0x06, 0x74,
+ 0xd2, 0x83, 0xbc, 0x5e, 0x7b, 0x42, 0x1f, 0xa0, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* crazy valley by Julius and Droid */
+ /* length 97496 crc32c 0xb8eec40eU */
+ {
+ { 0x23, 0x77, 0x18, 0x1d, 0x21, 0x9b, 0x41, 0x8f,
+ 0xc1, 0xb4, 0xf4, 0xf8, 0x22, 0xdd, 0xd8, 0xb6, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* THE ILLOGICAL ONE by Rhino */
+ /* length 173432 crc32c 0xcb4e2987U */
+ {
+ { 0xd8, 0xc2, 0xbb, 0xe6, 0x11, 0xd0, 0x5c, 0x02,
+ 0x8e, 0x3b, 0xcb, 0x7c, 0x4a, 0x7d, 0x43, 0xa0, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* sounds of holiday by Spacebrain */
+ /* length 309520 crc32c 0x28804a57U */
+ {
+ { 0x36, 0x18, 0x19, 0xa4, 0x9d, 0xa2, 0xa2, 0x6f,
+ 0x58, 0x60, 0xc4, 0xd9, 0x0d, 0xa2, 0x9f, 0x49, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* sunisinus by Speed-Head */
+ /* length 175706 crc32c 0x2e56451bU */
+ {
+ { 0x7e, 0x69, 0x44, 0xb6, 0x38, 0x0d, 0x27, 0x14,
+ 0x70, 0x5d, 0x44, 0xce, 0xce, 0xdd, 0x37, 0x31, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* eat the fulcrum bop by The Assassin */
+ /* length 160286 crc32c 0x583a4683U */
+ {
+ { 0x11, 0xe9, 0x6f, 0x62, 0xe1, 0xc3, 0xc5, 0xcc,
+ 0x3b, 0xaf, 0xea, 0x69, 0x4b, 0xce, 0x5f, 0xec, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* obvious disaster by Tip */
+ /* length 221086 crc32c 0x51c6d489U */
+ {
+ { 0x06, 0x8e, 0x69, 0x01, 0x49, 0x8f, 0xbd, 0x0f,
+ 0xfc, 0xb7, 0x8f, 0x2a, 0x91, 0xe1, 0x8b, 0xe8, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* alien nation by Turtle */
+ /* length 167548 crc32c 0xc9ec1674U */
+ {
+ { 0x71, 0xdf, 0x11, 0xac, 0x5d, 0xec, 0x07, 0xf8,
+ 0x10, 0x6f, 0x28, 0x8d, 0x47, 0x59, 0x54, 0x9b, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+ /* illusions!2 by Zuhl */
+ /* length 289770 crc32c 0x6bf5fbcfU */
+ {
+ { 0xca, 0x37, 0x8c, 0x0e, 0x87, 0x4f, 0x1e, 0xcd,
+ 0xa3, 0xe9, 0x8b, 0xdd, 0x11, 0x46, 0x8d, 0x69, },
+ XMP_FLAGS_VBLANK, XMP_MODE_AUTO
+ },
+
+ {
+ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+ 0, 0
+ }
+};
+
+static void module_quirks(struct context_data *ctx)
+{
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ int i;
+
+ for (i = 0; mq[i].flags != 0 || mq[i].mode != 0; i++) {
+ if (!memcmp(m->md5, mq[i].md5, 16)) {
+ p->flags |= mq[i].flags;
+ p->mode = mq[i].mode;
+ }
+ }
+}
+
+#endif /* LIBXMP_CORE_PLAYER */
+
+char *libxmp_adjust_string(char *s)
+{
+ int i;
+
+ for (i = 0; i < strlen(s); i++) {
+ if (!isprint((unsigned char)s[i]) || ((uint8) s[i] > 127))
+ s[i] = ' ';
+ }
+
+ while (*s && (s[strlen(s) - 1] == ' ')) {
+ s[strlen(s) - 1] = 0;
+ }
+
+ return s;
+}
+
+static void check_envelope(struct xmp_envelope *env)
+{
+ /* Disable envelope if invalid number of points */
+ if (env->npt <= 0 || env->npt > XMP_MAX_ENV_POINTS) {
+ env->flg &= ~XMP_ENVELOPE_ON;
+ }
+
+ /* Disable envelope loop if invalid loop parameters */
+ if (env->lps >= env->npt || env->lpe >= env->npt) {
+ env->flg &= ~XMP_ENVELOPE_LOOP;
+ }
+
+ /* Disable envelope sustain if invalid sustain */
+ if (env->sus >= env->npt || env->sue >= env->npt) {
+ env->flg &= ~XMP_ENVELOPE_SUS;
+ }
+}
+
+static void clamp_volume_envelope(struct module_data *m, struct xmp_envelope *env)
+{
+ /* Clamp broken values in the volume envelope to the expected range. */
+ if (env->flg & XMP_ENVELOPE_ON) {
+ int i;
+ for (i = 0; i < env->npt; i++) {
+ int16 *data = &env->data[i * 2 + 1];
+ CLAMP(*data, 0, m->volbase);
+ }
+ }
+}
+
+void libxmp_load_prologue(struct context_data *ctx)
+{
+ struct module_data *m = &ctx->m;
+ int i;
+
+ /* Reset variables */
+ memset(&m->mod, 0, sizeof (struct xmp_module));
+ m->rrate = PAL_RATE;
+ m->c4rate = C4_PAL_RATE;
+ m->volbase = 0x40;
+ m->gvol = m->gvolbase = 0x40;
+ m->vol_table = NULL;
+ m->quirk = 0;
+ m->flow_mode = FLOW_MODE_GENERIC;
+ m->read_event_type = READ_EVENT_MOD;
+ m->period_type = PERIOD_AMIGA;
+ m->compare_vblank = 0;
+ m->comment = NULL;
+ m->scan_cnt = NULL;
+ m->midi = NULL;
+
+ /* Set defaults */
+ m->mod.pat = 0;
+ m->mod.trk = 0;
+ m->mod.chn = 4;
+ m->mod.ins = 0;
+ m->mod.smp = 0;
+ m->mod.spd = 6;
+ m->mod.bpm = 125;
+ m->mod.len = 0;
+ m->mod.rst = 0;
+
+#ifndef LIBXMP_CORE_PLAYER
+ m->extra = NULL;
+#endif
+
+ m->time_factor = DEFAULT_TIME_FACTOR;
+
+ for (i = 0; i < 64; i++) {
+ int pan = (((i + 1) / 2) % 2) * 0xff;
+ m->mod.xxc[i].pan = 0x80 + (pan - 0x80) * m->defpan / 100;
+ m->mod.xxc[i].vol = 0x40;
+ m->mod.xxc[i].flg = 0;
+ }
+}
+
+void libxmp_load_epilogue(struct context_data *ctx)
+{
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ int i, j;
+
+ mod->gvl = m->gvol;
+
+ /* Sanity check for module parameters */
+ CLAMP(mod->len, 0, XMP_MAX_MOD_LENGTH);
+ CLAMP(mod->pat, 0, 257); /* some formats have an extra pattern */
+ CLAMP(mod->ins, 0, 255);
+ CLAMP(mod->smp, 0, MAX_SAMPLES);
+ CLAMP(mod->chn, 0, XMP_MAX_CHANNELS);
+
+ /* Fix cases where the restart value is invalid e.g. kc_fall8.xm
+ * from http://aminet.net/mods/mvp/mvp_0002.lha (reported by
+ * Ralf Hoffmann )
+ */
+ if (mod->rst >= mod->len) {
+ mod->rst = 0;
+ }
+
+ /* Sanity check for tempo and BPM */
+ if (mod->spd <= 0 || mod->spd > 255) {
+ mod->spd = 6;
+ }
+ CLAMP(mod->bpm, XMP_MIN_BPM, 1000);
+
+ /* Set appropriate values for instrument volumes and subinstrument
+ * global volumes when QUIRK_INSVOL is not set, to keep volume values
+ * consistent if the user inspects struct xmp_module. We can later
+ * set volumes in the loaders and eliminate the quirk.
+ */
+ for (i = 0; i < mod->ins; i++) {
+ if (~m->quirk & QUIRK_INSVOL) {
+ mod->xxi[i].vol = m->volbase;
+ }
+ for (j = 0; j < mod->xxi[i].nsm; j++) {
+ if (~m->quirk & QUIRK_INSVOL) {
+ mod->xxi[i].sub[j].gvl = m->volbase;
+ }
+ }
+ }
+
+ /* Sanity check for envelopes
+ */
+ for (i = 0; i < mod->ins; i++) {
+ check_envelope(&mod->xxi[i].aei);
+ check_envelope(&mod->xxi[i].fei);
+ check_envelope(&mod->xxi[i].pei);
+ clamp_volume_envelope(m, &mod->xxi[i].aei);
+ }
+
+#ifndef LIBXMP_CORE_DISABLE_IT
+ /* TODO: there's no unintrusive and clean way to get this struct into
+ * libxmp_load_sample currently, so bound these fields here for now. */
+ for (i = 0; i < mod->smp; i++) {
+ struct xmp_sample *xxs = &mod->xxs[i];
+ struct extra_sample_data *xtra = &m->xtra[i];
+ if (xtra->sus < 0) {
+ xtra->sus = 0;
+ }
+ if (xtra->sue > xxs->len) {
+ xtra->sue = xxs->len;
+ }
+ if (xtra->sus >= xxs->len || xtra->sus >= xtra->sue) {
+ xtra->sus = xtra->sue = 0;
+ xxs->flg &= ~(XMP_SAMPLE_SLOOP | XMP_SAMPLE_SLOOP_BIDIR);
+ }
+ }
+#endif
+
+ p->filter = 0;
+ p->mode = XMP_MODE_AUTO;
+ p->flags = p->player_flags;
+ p->scan_time_factor = m->time_factor;
+#ifndef LIBXMP_CORE_PLAYER
+ module_quirks(ctx);
+#endif
+ libxmp_set_player_mode(ctx);
+}
+
+int libxmp_prepare_scan(struct context_data *ctx)
+{
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ int i, ord;
+
+ if (mod->xxp == NULL || mod->xxt == NULL)
+ return -XMP_ERROR_LOAD;
+ ord = 0;
+ while (ord < mod->len && mod->xxo[ord] >= mod->pat) {
+ ord++;
+ }
+
+ if (ord >= mod->len) {
+ mod->len = 0;
+ return 0;
+ }
+
+ m->scan_cnt = (uint8 **) calloc(mod->len, sizeof(uint8 *));
+ if (m->scan_cnt == NULL)
+ return -XMP_ERROR_SYSTEM;
+
+ for (i = 0; i < mod->len; i++) {
+ int pat_idx = mod->xxo[i];
+ struct xmp_pattern *pat;
+
+ /* Add pattern if referenced in orders */
+ if (pat_idx < mod->pat && !mod->xxp[pat_idx]) {
+ if (libxmp_alloc_pattern(mod, pat_idx) < 0) {
+ return -XMP_ERROR_SYSTEM;
+ }
+ }
+
+ pat = pat_idx >= mod->pat ? NULL : mod->xxp[pat_idx];
+ m->scan_cnt[i] = (uint8 *) calloc(1, (pat && pat->rows)? pat->rows : 1);
+ if (m->scan_cnt[i] == NULL)
+ return -XMP_ERROR_SYSTEM;
+ }
+
+ return 0;
+}
+
+void libxmp_free_scan(struct context_data *ctx)
+{
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ struct xmp_module *mod = &m->mod;
+ int i;
+
+ if (m->scan_cnt) {
+ for (i = 0; i < mod->len; i++)
+ free(m->scan_cnt[i]);
+
+ free(m->scan_cnt);
+ m->scan_cnt = NULL;
+ }
+
+ free(p->scan);
+ p->scan = NULL;
+}
+
+/* Process player personality flags */
+int libxmp_set_player_mode(struct context_data *ctx)
+{
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ int q;
+
+ switch (p->mode) {
+ case XMP_MODE_AUTO:
+ break;
+ case XMP_MODE_MOD:
+ m->c4rate = C4_PAL_RATE;
+ m->quirk = 0;
+ m->flow_mode = FLOW_MODE_GENERIC;
+ m->read_event_type = READ_EVENT_MOD;
+ m->period_type = PERIOD_AMIGA;
+ break;
+ case XMP_MODE_NOISETRACKER:
+ m->c4rate = C4_PAL_RATE;
+ m->quirk = QUIRK_NOBPM;
+ m->flow_mode = FLOW_MODE_GENERIC;
+ m->read_event_type = READ_EVENT_MOD;
+ m->period_type = PERIOD_MODRNG;
+ break;
+ case XMP_MODE_PROTRACKER:
+ m->c4rate = C4_PAL_RATE;
+ m->quirk = QUIRK_PROTRACK;
+ m->flow_mode = FLOW_MODE_GENERIC;
+ m->read_event_type = READ_EVENT_MOD;
+ m->period_type = PERIOD_MODRNG;
+ break;
+ case XMP_MODE_S3M:
+ q = m->quirk & (QUIRK_VSALL | QUIRK_ARPMEM);
+ m->c4rate = C4_NTSC_RATE;
+ m->quirk = QUIRKS_ST3 | q;
+ m->flow_mode = FLOW_MODE_ST3_321;
+ m->read_event_type = READ_EVENT_ST3;
+ break;
+ case XMP_MODE_ST3:
+ q = m->quirk & (QUIRK_VSALL | QUIRK_ARPMEM);
+ m->c4rate = C4_NTSC_RATE;
+ m->quirk = QUIRKS_ST3 | QUIRK_ST3BUGS | q;
+ m->flow_mode = FLOW_MODE_ST3_321;
+ m->read_event_type = READ_EVENT_ST3;
+ break;
+ case XMP_MODE_ST3GUS:
+ q = m->quirk & (QUIRK_VSALL | QUIRK_ARPMEM);
+ m->c4rate = C4_NTSC_RATE;
+ m->quirk = QUIRKS_ST3 | QUIRK_ST3BUGS | q;
+ m->quirk &= ~QUIRK_RSTCHN;
+ m->flow_mode = FLOW_MODE_ST3_321;
+ m->read_event_type = READ_EVENT_ST3;
+ break;
+ case XMP_MODE_XM:
+ m->c4rate = C4_NTSC_RATE;
+ m->quirk = QUIRKS_FT2;
+ m->flow_mode = FLOW_MODE_GENERIC;
+ m->read_event_type = READ_EVENT_FT2;
+ break;
+ case XMP_MODE_FT2:
+ m->c4rate = C4_NTSC_RATE;
+ m->quirk = QUIRKS_FT2 | QUIRK_FT2BUGS;
+ m->flow_mode = FLOW_MODE_GENERIC;
+ m->read_event_type = READ_EVENT_FT2;
+ break;
+ case XMP_MODE_IT:
+ m->c4rate = C4_NTSC_RATE;
+ m->quirk = QUIRKS_IT | QUIRK_VIBHALF | QUIRK_VIBINV;
+ m->flow_mode = FLOW_MODE_IT_210;
+ m->read_event_type = READ_EVENT_IT;
+ break;
+ case XMP_MODE_ITSMP:
+ m->c4rate = C4_NTSC_RATE;
+ m->quirk = QUIRKS_IT | QUIRK_VIBHALF | QUIRK_VIBINV;
+ m->quirk &= ~(QUIRK_VIRTUAL | QUIRK_RSTCHN);
+ m->flow_mode = FLOW_MODE_IT_210;
+ m->read_event_type = READ_EVENT_IT;
+ break;
+ default:
+ return -1;
+ }
+
+ if (p->mode != XMP_MODE_AUTO)
+ m->compare_vblank = 0;
+
+ return 0;
+}
+
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/common.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/common.c
new file mode 100644
index 00000000..06b5bd26
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/common.c
@@ -0,0 +1,639 @@
+/* Extended Module Player
+ * Copyright (C) 1996-2025 Claudio Matsuoka and Hipolito Carraro Jr
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+#include