New installer. Game features!
This commit is contained in:
parent
28092f2ef2
commit
bac2018538
403 changed files with 73971 additions and 1887 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -13,3 +13,4 @@ just-stuff/
|
|||
dvx.log
|
||||
claudeDocs/
|
||||
toolchains/
|
||||
dist/
|
||||
|
|
|
|||
24
Makefile
24
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
|
||||
|
|
|
|||
18
README.md
18
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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ img { max-width: 100%; }
|
|||
<li><a href="#lang.flow">Flow Control</a></li>
|
||||
<li><a href="#lang.io">Input/Output</a></li>
|
||||
<li><a href="#lang.misc">Miscellaneous Statements</a></li>
|
||||
<li><a href="#lang.sound">Sound, Joysticks and Frames</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#lang.fileio">File I/O</a></li>
|
||||
|
|
@ -170,6 +171,7 @@ img { max-width: 100%; }
|
|||
<li><a href="#ide.editor">Auto-indent</a></li>
|
||||
<li><a href="#ctrl.textarea">AutoIndent</a></li>
|
||||
<li><a href="#ctrl.form">AutoSize</a></li>
|
||||
<li><a href="#lang.sound">BEEP</a></li>
|
||||
<li><a href="#ctrl.image">BMP</a></li>
|
||||
<li><a href="#lang.datatypes">Boolean</a></li>
|
||||
<li><a href="#ide.debugger">Breakpoints</a></li>
|
||||
|
|
@ -315,10 +317,14 @@ img { max-width: 100%; }
|
|||
<li><a href="#ctrl.frm">Form File</a></li>
|
||||
<li><a href="#lang.func.string">FORMAT$</a></li>
|
||||
<li><a href="#ctrl.frame">Frame</a></li>
|
||||
<li><a href="#lang.sound">Frame rate</a></li>
|
||||
<li><a href="#lang.sound">FRAMEWAIT</a></li>
|
||||
<li><a href="#lang.func.fileio">FREEFILE</a></li>
|
||||
<li><a href="#ctrl.frm">FRM</a></li>
|
||||
<li><a href="#lang.procedures">FUNCTION</a></li>
|
||||
<li><a href="#ide.editor">Function Dropdown</a></li>
|
||||
<li><a href="#lang.runtime">game.bas</a></li>
|
||||
<li><a href="#lang.runtime">GameKeyDown</a></li>
|
||||
<li><a href="#lang.fileio">GET</a></li>
|
||||
<li><a href="#lang.filesystem">GETATTR</a></li>
|
||||
<li><a href="#lang.flow">GOSUB</a></li>
|
||||
|
|
@ -356,6 +362,14 @@ img { max-width: 100%; }
|
|||
<li><a href="#lang.func.math">INT</a></li>
|
||||
<li><a href="#lang.datatypes">Integer</a></li>
|
||||
<li><a href="#ctrl.timer">Interval</a></li>
|
||||
<li><a href="#lang.runtime">JoyAxis</a></li>
|
||||
<li><a href="#lang.runtime">JoyButton</a></li>
|
||||
<li><a href="#lang.runtime">JoyCount</a></li>
|
||||
<li><a href="#lang.runtime">JoyHat</a></li>
|
||||
<li><a href="#lang.runtime">Joystick</a></li>
|
||||
<li><a href="#lang.runtime">joystick.bas</a></li>
|
||||
<li><a href="#lang.runtime">JoyX</a></li>
|
||||
<li><a href="#lang.runtime">JoyY</a></li>
|
||||
<li><a href="#ide.shortcuts">Keyboard Shortcuts</a></li>
|
||||
<li><a href="#lang.filesystem">KILL</a></li>
|
||||
<li><a href="#ctrl.label">Label</a></li>
|
||||
|
|
@ -385,12 +399,15 @@ img { max-width: 100%; }
|
|||
<li><a href="#lang.func.string">LTRIM$</a></li>
|
||||
<li><a href="#ctrl.databinding">Master-Detail</a></li>
|
||||
<li><a href="#ctrl.hscrollbar">Max</a></li>
|
||||
<li><a href="#lang.sound">MB</a></li>
|
||||
<li><a href="#lang.forms">Me</a></li>
|
||||
<li><a href="#ctrl.menus">Menu</a></li>
|
||||
<li><a href="#ctrl.menus">Menu Bar</a></li>
|
||||
<li><a href="#ide.menu.view">Menu Editor</a></li>
|
||||
<li><a href="#ctrl.common.props">Methods</a></li>
|
||||
<li><a href="#lang.sound">MF</a></li>
|
||||
<li><a href="#lang.func.string">MID$</a></li>
|
||||
<li><a href="#lang.runtime">MIDI</a></li>
|
||||
<li><a href="#ide.changes">Migration</a></li>
|
||||
<li><a href="#ctrl.hscrollbar">Min</a></li>
|
||||
<li><a href="#lang.filesystem">MKDIR</a></li>
|
||||
|
|
@ -400,6 +417,27 @@ img { max-width: 100%; }
|
|||
<li><a href="#lang.forms">MsgBox</a></li>
|
||||
<li><a href="#lang.forms">MSGBOX</a></li>
|
||||
<li><a href="#ctrl.listview">Multi-Column List</a></li>
|
||||
<li><a href="#lang.runtime">Music</a></li>
|
||||
<li><a href="#lang.sound">Music macro language</a></li>
|
||||
<li><a href="#lang.runtime">music.bas</a></li>
|
||||
<li><a href="#lang.runtime">MusicControl</a></li>
|
||||
<li><a href="#lang.runtime">MusicError$</a></li>
|
||||
<li><a href="#lang.runtime">MusicFree</a></li>
|
||||
<li><a href="#lang.runtime">MusicLength</a></li>
|
||||
<li><a href="#lang.runtime">MusicLoad</a></li>
|
||||
<li><a href="#lang.runtime">MusicMidiOpen</a></li>
|
||||
<li><a href="#lang.runtime">MusicNoteOff</a></li>
|
||||
<li><a href="#lang.runtime">MusicNoteOn</a></li>
|
||||
<li><a href="#lang.runtime">MusicPause</a></li>
|
||||
<li><a href="#lang.runtime">MusicPitchBend</a></li>
|
||||
<li><a href="#lang.runtime">MusicPlay</a></li>
|
||||
<li><a href="#lang.runtime">MusicPlaying</a></li>
|
||||
<li><a href="#lang.runtime">MusicPosition</a></li>
|
||||
<li><a href="#lang.runtime">MusicProgram</a></li>
|
||||
<li><a href="#lang.runtime">MusicResume</a></li>
|
||||
<li><a href="#lang.runtime">MusicSeek</a></li>
|
||||
<li><a href="#lang.runtime">MusicStop</a></li>
|
||||
<li><a href="#lang.runtime">MusicVolume</a></li>
|
||||
<li><a href="#lang.filesystem">NAME</a></li>
|
||||
<li><a href="#lang.runtime">net.bas</a></li>
|
||||
<li><a href="#ide.menu.file">New Project</a></li>
|
||||
|
|
@ -426,6 +464,12 @@ img { max-width: 100%; }
|
|||
<li><a href="#ide.menu.edit">Paste</a></li>
|
||||
<li><a href="#ctrl.image">Picture</a></li>
|
||||
<li><a href="#ctrl.picturebox">PictureBox</a></li>
|
||||
<li><a href="#lang.sound">PLAY</a></li>
|
||||
<li><a href="#lang.sound">PLAY function</a></li>
|
||||
<li><a href="#lang.runtime">PlayNoise</a></li>
|
||||
<li><a href="#lang.runtime">PlayTone</a></li>
|
||||
<li><a href="#lang.runtime">PlayWave</a></li>
|
||||
<li><a href="#lang.runtime">PlayWaveLoop</a></li>
|
||||
<li><a href="#lang.operators">Precedence</a></li>
|
||||
<li><a href="#lang.constants">Predefined Constants</a></li>
|
||||
<li><a href="#ide.menu.tools">Preferences</a></li>
|
||||
|
|
@ -512,6 +556,13 @@ img { max-width: 100%; }
|
|||
<li><a href="#lang.datatypes">Single</a></li>
|
||||
<li><a href="#lang.misc">SLEEP</a></li>
|
||||
<li><a href="#ctrl.hscrollbar">Slider</a></li>
|
||||
<li><a href="#lang.sound">SOUND</a></li>
|
||||
<li><a href="#lang.runtime">Sound</a></li>
|
||||
<li><a href="#lang.runtime">sound.bas</a></li>
|
||||
<li><a href="#lang.runtime">SoundError$</a></li>
|
||||
<li><a href="#lang.runtime">SoundInfo$</a></li>
|
||||
<li><a href="#lang.runtime">SoundPlaying</a></li>
|
||||
<li><a href="#lang.runtime">SoundVolume</a></li>
|
||||
<li><a href="#ide.project">Source Map</a></li>
|
||||
<li><a href="#lang.func.string">SPACE$</a></li>
|
||||
<li><a href="#ctrl.spacer">Spacer</a></li>
|
||||
|
|
@ -548,9 +599,13 @@ img { max-width: 100%; }
|
|||
<li><a href="#ide.menu.run">Step Out</a></li>
|
||||
<li><a href="#ide.menu.run">Step Over</a></li>
|
||||
<li><a href="#ide.debugger">Stepping</a></li>
|
||||
<li><a href="#lang.sound">STICK</a></li>
|
||||
<li><a href="#ide.menu.run">Stop</a></li>
|
||||
<li><a href="#ctrl.timer">Stop</a></li>
|
||||
<li><a href="#lang.runtime">StopAllSounds</a></li>
|
||||
<li><a href="#lang.runtime">StopWave</a></li>
|
||||
<li><a href="#lang.func.string">STR$</a></li>
|
||||
<li><a href="#lang.sound">STRIG</a></li>
|
||||
<li><a href="#lang.datatypes">String</a></li>
|
||||
<li><a href="#lang.func.string">STRING$</a></li>
|
||||
<li><a href="#lang.procedures">SUB</a></li>
|
||||
|
|
@ -1255,6 +1310,16 @@ img { max-width: 100%; }
|
|||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
</ul>
|
||||
<h2>Sound, Joysticks and Frames</h2>
|
||||
<ul>
|
||||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
<li>Message boxes (MsgBox) play the desktop sound chosen for their kind on the Control Panel's Sounds tab.</li>
|
||||
</ul>
|
||||
<p><a href="#lang.sound">See: Sound, Joysticks and Frames</a></p>
|
||||
<h2>Runtime</h2>
|
||||
<ul>
|
||||
<li>Nested BYREF parameters reach the original variable or array element.</li>
|
||||
|
|
@ -1462,6 +1527,7 @@ REM This is a comment
|
|||
<p><a href="#lang.flow">Flow Control (EXIT, CALL, GOTO, GOSUB, ON)</a></p>
|
||||
<p><a href="#lang.io">Input/Output (PRINT, INPUT, DATA/READ)</a></p>
|
||||
<p><a href="#lang.misc">Miscellaneous Statements (ON ERROR, SHELL, SLEEP, END)</a></p>
|
||||
<p><a href="#lang.sound">Sound, Joysticks and Frames (BEEP, SOUND, PLAY, STICK, STRIG, FRAMEWAIT)</a></p>
|
||||
</div>
|
||||
<div class="topic" id="lang.declarations">
|
||||
<h1>Declaration Statements</h1>
|
||||
|
|
@ -1969,6 +2035,70 @@ RANDOMIZE TIMER ' Seed from system clock</code></pre>
|
|||
<p>Terminates program execution immediately. Any form that is loaded is unloaded before the program exits.</p>
|
||||
<pre><code>END</code></pre>
|
||||
</div>
|
||||
<div class="topic" id="lang.sound">
|
||||
<h1>Sound, Joysticks and Frames</h1>
|
||||
<p>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.</p>
|
||||
<h2>BEEP</h2>
|
||||
<p>Sounds a short beep (800 hertz for a quarter of a second).</p>
|
||||
<pre><code>BEEP</code></pre>
|
||||
<h2>SOUND</h2>
|
||||
<p>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).</p>
|
||||
<pre><code>SOUND frequency, duration
|
||||
SOUND 440, 18.2 ' A for one second
|
||||
SOUND 440, 0 ' stop</code></pre>
|
||||
<h2>PLAY</h2>
|
||||
<p>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.</p>
|
||||
<pre><code>PLAY commands$
|
||||
PLAY "T160 O3 L8 E D C D E E E4"</code></pre>
|
||||
<pre> 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).</pre>
|
||||
<p>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.</p>
|
||||
<pre><code>PLAY "MB T120 L4 CDEF GAB>C" ' a scale in the background
|
||||
PLAY "I74 O4 L8 GAB>C" ' on the flute</code></pre>
|
||||
<h2>PLAY Function</h2>
|
||||
<p>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:</p>
|
||||
<pre><code>If PLAY(0) < 4 Then PLAY "MB " + tune$</code></pre>
|
||||
<h2>STICK and STRIG</h2>
|
||||
<p>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.</p>
|
||||
<p>STRIG(n) returns True (-1) or False (0) for the buttons:</p>
|
||||
<pre> 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</pre>
|
||||
<p>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.</p>
|
||||
<pre><code>x = STICK(0)
|
||||
y = STICK(1)
|
||||
If STRIG(0) Then FireShot</code></pre>
|
||||
<h2>FRAMEWAIT</h2>
|
||||
<p>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.</p>
|
||||
<pre><code>FRAMEWAIT rate
|
||||
|
||||
Do
|
||||
MoveEverything
|
||||
DrawEverything
|
||||
FRAMEWAIT 30
|
||||
Loop Until quitting</code></pre>
|
||||
</div>
|
||||
<div class="topic" id="lang.fileio">
|
||||
<h1>File I/O</h1>
|
||||
<h2>OPEN</h2>
|
||||
|
|
@ -2325,7 +2455,7 @@ Next</code></pre>
|
|||
<p>Displays a message-box dialog. Can be used as a statement (discards the result) or as a function (returns the button clicked).</p>
|
||||
<pre><code>MSGBOX message$ [, flags [, title$]]
|
||||
result = MSGBOX(message$ [, flags [, title$]])</code></pre>
|
||||
<p>flags is the sum of a button-style constant and an icon constant. See Predefined Constants for the complete list.</p>
|
||||
<p>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).</p>
|
||||
<pre><code>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</pre>
|
||||
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, ...)</pre>
|
||||
<h2>Using an Include Library</h2>
|
||||
<pre><code>' With commdlg.bas added to the project
|
||||
Dim path As String
|
||||
|
|
@ -2565,6 +2699,67 @@ Sub mnuPageSetup_Click ()
|
|||
PrintPageSetup
|
||||
End Sub</code></pre>
|
||||
<p>For printer output a line at a time, see LPRINT and OPEN "LPT1:".</p>
|
||||
<h2>Held Keys for Games</h2>
|
||||
<p>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:</p>
|
||||
<pre><code>Sub Timer1_Timer ()
|
||||
If GameKeyDown(GAMEKEY_LEFT) Then shipX = shipX - 2
|
||||
If GameKeyDown(GAMEKEY_RIGHT) Then shipX = shipX + 2
|
||||
End Sub</code></pre>
|
||||
<h2>Joysticks</h2>
|
||||
<p>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.</p>
|
||||
<pre><code>Sub Timer1_Timer ()
|
||||
If JoyCount() = 0 Then Exit Sub
|
||||
|
||||
shipX = shipX + JoyX(1) \ 8192
|
||||
If JoyButton(1, 1) Then FireShot
|
||||
End Sub</code></pre>
|
||||
<p>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.</p>
|
||||
<p><a href="#lang.sound">See: STICK and STRIG</a></p>
|
||||
<h2>Sound</h2>
|
||||
<p>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.</p>
|
||||
<p>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".</p>
|
||||
<pre><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</code></pre>
|
||||
<p>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.</p>
|
||||
<p><a href="#lang.sound">See: BEEP, SOUND and PLAY</a></p>
|
||||
<h2>Music</h2>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<pre><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</code></pre>
|
||||
<p>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.</p>
|
||||
<pre><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</code></pre>
|
||||
<h2>Network Connections</h2>
|
||||
<p>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:</p>
|
||||
<pre><code>Dim conn As Long
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (<app>/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
|
||||
|
|
|
|||
|
|
@ -393,9 +393,6 @@ The `apps/kpunch/Makefile` orchestrates both C and BASIC builds:
|
|||
compiled and linked with `dxe3gen -U` into `<app>.app`, then
|
||||
resources are attached with `dvxres build` if `<app>/<app>.res`
|
||||
exists. No list needs editing.
|
||||
* **DOS batch files** an app ships in the DVX folder itself
|
||||
(`<app>/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 <app>.dbp -o <app>.app -release`,
|
||||
which handles project-file parsing, compilation, resource
|
||||
attachment, and linking into `basstub.app`.
|
||||
|
|
|
|||
746
src/apps/kpunch/cpanel/cpJoystick.c
Normal file
746
src/apps/kpunch/cpanel/cpJoystick.c
Normal file
|
|
@ -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 <dlfcn.h>
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
49
src/apps/kpunch/cpanel/cpJoystick.h
Normal file
49
src/apps/kpunch/cpanel/cpJoystick.h
Normal file
|
|
@ -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
|
||||
457
src/apps/kpunch/cpanel/cpSounds.c
Normal file
457
src/apps/kpunch/cpanel/cpSounds.c
Normal file
|
|
@ -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 <dlfcn.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
49
src/apps/kpunch/cpanel/cpSounds.h
Normal file
49
src/apps/kpunch/cpanel/cpSounds.h
Normal file
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
BIN
src/apps/kpunch/setup/icon32.bmp
(Stored with Git LFS)
BIN
src/apps/kpunch/setup/icon32.bmp
(Stored with Git LFS)
Binary file not shown.
|
|
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#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;
|
||||
}
|
||||
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
|
|
@ -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/<name>/ 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)/<name>.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
|
||||
|
|
|
|||
61
src/include/basic/game.bas
Normal file
61
src/include/basic/game.bas
Normal file
|
|
@ -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
|
||||
70
src/include/basic/joystick.bas
Normal file
70
src/include/basic/joystick.bas
Normal file
|
|
@ -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
|
||||
105
src/include/basic/music.bas
Normal file
105
src/include/basic/music.bas
Normal file
|
|
@ -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
|
||||
73
src/include/basic/sound.bas
Normal file
73
src/include/basic/sound.bas
Normal file
|
|
@ -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
|
||||
137
src/installer/Makefile
Normal file
137
src/installer/Makefile
Normal file
|
|
@ -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<Name>, 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: <folder>/<source>:<Name> (wgtRegister -> wgtRegister<Name>)
|
||||
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)
|
||||
77
src/installer/README.TXT
Normal file
77
src/installer/README.TXT
Normal file
|
|
@ -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.
|
||||
153
src/installer/mkDist.sh
Executable file
153
src/installer/mkDist.sh
Executable file
|
|
@ -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"
|
||||
378
src/installer/pkgBuild.c
Normal file
378
src/installer/pkgBuild.c
Normal file
|
|
@ -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 <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
55
src/installer/pkgBuild.h
Normal file
55
src/installer/pkgBuild.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
236
src/installer/pkgFormat.c
Normal file
236
src/installer/pkgFormat.c
Normal file
|
|
@ -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 <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
// 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));
|
||||
}
|
||||
120
src/installer/pkgFormat.h
Normal file
120
src/installer/pkgFormat.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// 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
|
||||
798
src/installer/pkgRead.c
Normal file
798
src/installer/pkgRead.c
Normal file
|
|
@ -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 <ctype.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#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;
|
||||
}
|
||||
131
src/installer/pkgRead.h
Normal file
131
src/installer/pkgRead.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
136
src/installer/pkgTool.c
Normal file
136
src/installer/pkgTool.c
Normal file
|
|
@ -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 <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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();
|
||||
}
|
||||
784
src/installer/setup.c
Normal file
784
src/installer/setup.c
Normal file
|
|
@ -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 <inttypes.h>
|
||||
#include <process.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// ============================================================
|
||||
// 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;
|
||||
}
|
||||
534
src/installer/setupCore.c
Normal file
534
src/installer/setupCore.c
Normal file
|
|
@ -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 <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#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);
|
||||
}
|
||||
119
src/installer/setupCore.h
Normal file
119
src/installer/setupCore.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
34
src/installer/thirdparty/miniz/DVX-VERSION.md
vendored
Normal file
34
src/installer/thirdparty/miniz/DVX-VERSION.md
vendored
Normal file
|
|
@ -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`.
|
||||
22
src/installer/thirdparty/miniz/LICENSE
vendored
Normal file
22
src/installer/thirdparty/miniz/LICENSE
vendored
Normal file
|
|
@ -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.
|
||||
7922
src/installer/thirdparty/miniz/miniz.c
vendored
Normal file
7922
src/installer/thirdparty/miniz/miniz.c
vendored
Normal file
File diff suppressed because it is too large
Load diff
1510
src/installer/thirdparty/miniz/miniz.h
vendored
Normal file
1510
src/installer/thirdparty/miniz/miniz.h
vendored
Normal file
File diff suppressed because it is too large
Load diff
80
src/libs/kpunch/audio/Makefile
Normal file
80
src/libs/kpunch/audio/Makefile
Normal file
|
|
@ -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/<name>/
|
||||
# directory is one driver, built from all its sources into
|
||||
# drivers/<name>.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)
|
||||
1761
src/libs/kpunch/audio/audio.c
Normal file
1761
src/libs/kpunch/audio/audio.c
Normal file
File diff suppressed because it is too large
Load diff
3
src/libs/kpunch/audio/audio.dep
Normal file
3
src/libs/kpunch/audio/audio.dep
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
libtasks
|
||||
libdvx
|
||||
dvxshell
|
||||
229
src/libs/kpunch/audio/audio.dhs
Normal file
229
src/libs/kpunch/audio/audio.dhs
Normal file
|
|
@ -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): <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.
|
||||
|
||||
.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.
|
||||
504
src/libs/kpunch/audio/audio.h
Normal file
504
src/libs/kpunch/audio/audio.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
180
src/libs/kpunch/audio/audioBas.c
Normal file
180
src/libs/kpunch/audio/audioBas.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
}
|
||||
181
src/libs/kpunch/audio/audioDesk.c
Normal file
181
src/libs/kpunch/audio/audioDesk.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
232
src/libs/kpunch/audio/audioDrv.h
Normal file
232
src/libs/kpunch/audio/audioDrv.h
Normal file
|
|
@ -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 <name>.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
|
||||
203
src/libs/kpunch/audio/audioInt.h
Normal file
203
src/libs/kpunch/audio/audioInt.h
Normal file
|
|
@ -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
|
||||
451
src/libs/kpunch/audio/audioLoad.c
Normal file
451
src/libs/kpunch/audio/audioLoad.c
Normal file
|
|
@ -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 <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// 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;
|
||||
}
|
||||
526
src/libs/kpunch/audio/audioMix.c
Normal file
526
src/libs/kpunch/audio/audioMix.c
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/libs/kpunch/audio/audioShell.c
Normal file
51
src/libs/kpunch/audio/audioShell.c
Normal file
|
|
@ -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);
|
||||
}
|
||||
567
src/libs/kpunch/audio/audioSynth.c
Normal file
567
src/libs/kpunch/audio/audioSynth.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
182
src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
Normal file
182
src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
Normal file
|
|
@ -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 <pc.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <strings.h>
|
||||
|
||||
#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
|
||||
}
|
||||
}
|
||||
72
src/libs/kpunch/audio/drivers/null/nullDrv.c
Normal file
72
src/libs/kpunch/audio/drivers/null/nullDrv.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
|
||||
// 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();
|
||||
}
|
||||
214
src/libs/kpunch/audio/drivers/opl/oplDrv.c
Normal file
214
src/libs/kpunch/audio/drivers/opl/oplDrv.c
Normal file
|
|
@ -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 <pc.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
218
src/libs/kpunch/audio/drivers/sb/sbCalc.c
Normal file
218
src/libs/kpunch/audio/drivers/sb/sbCalc.c
Normal file
|
|
@ -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 <stddef.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
90
src/libs/kpunch/audio/drivers/sb/sbCalc.h
Normal file
90
src/libs/kpunch/audio/drivers/sb/sbCalc.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
773
src/libs/kpunch/audio/drivers/sb/sbDrv.c
Normal file
773
src/libs/kpunch/audio/drivers/sb/sbDrv.c
Normal file
|
|
@ -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 <dpmi.h>
|
||||
#include <go32.h>
|
||||
#include <pc.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/farptr.h>
|
||||
|
||||
// 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);
|
||||
}
|
||||
202
src/libs/kpunch/audio/drivers/speaker/spkDrv.c
Normal file
202
src/libs/kpunch/audio/drivers/speaker/spkDrv.c
Normal file
|
|
@ -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 <pc.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
64
src/libs/kpunch/game/Makefile
Normal file
64
src/libs/kpunch/game/Makefile
Normal file
|
|
@ -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)
|
||||
182
src/libs/kpunch/game/game.c
Normal file
182
src/libs/kpunch/game/game.c
Normal file
|
|
@ -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 <string.h>
|
||||
|
||||
// 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);
|
||||
}
|
||||
2
src/libs/kpunch/game/game.dep
Normal file
2
src/libs/kpunch/game/game.dep
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
libtasks
|
||||
libdvx
|
||||
94
src/libs/kpunch/game/game.dhs
Normal file
94
src/libs/kpunch/game/game.dhs
Normal file
|
|
@ -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.
|
||||
124
src/libs/kpunch/game/game.h
Normal file
124
src/libs/kpunch/game/game.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
61
src/libs/kpunch/game/gameBas.c
Normal file
61
src/libs/kpunch/game/gameBas.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
73
src/libs/kpunch/joystick/Makefile
Normal file
73
src/libs/kpunch/joystick/Makefile
Normal file
|
|
@ -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/<name>/ directory is one
|
||||
# driver module, built from all its sources into drivers/<name>.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)
|
||||
142
src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c
Normal file
142
src/libs/kpunch/joystick/drivers/joykeys/joyKeys.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#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]);
|
||||
}
|
||||
}
|
||||
327
src/libs/kpunch/joystick/drivers/joyport/joyPort.c
Normal file
327
src/libs/kpunch/joystick/drivers/joyport/joyPort.c
Normal file
|
|
@ -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 <inttypes.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/libs/kpunch/joystick/drivers/joyport/joyPort.h
Normal file
88
src/libs/kpunch/joystick/drivers/joyport/joyPort.h
Normal file
|
|
@ -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
|
||||
70
src/libs/kpunch/joystick/drivers/joyport/joyPortDos.c
Normal file
70
src/libs/kpunch/joystick/drivers/joyport/joyPortDos.c
Normal file
|
|
@ -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 <pc.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
278
src/libs/kpunch/joystick/joyBas.c
Normal file
278
src/libs/kpunch/joystick/joyBas.c
Normal file
|
|
@ -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 <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
38
src/libs/kpunch/joystick/joyInt.h
Normal file
38
src/libs/kpunch/joystick/joyInt.h
Normal file
|
|
@ -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
|
||||
65
src/libs/kpunch/joystick/joyShell.c
Normal file
65
src/libs/kpunch/joystick/joyShell.c
Normal file
|
|
@ -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);
|
||||
}
|
||||
1190
src/libs/kpunch/joystick/joystick.c
Normal file
1190
src/libs/kpunch/joystick/joystick.c
Normal file
File diff suppressed because it is too large
Load diff
2
src/libs/kpunch/joystick/joystick.dep
Normal file
2
src/libs/kpunch/joystick/joystick.dep
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
libdvx
|
||||
dvxshell
|
||||
156
src/libs/kpunch/joystick/joystick.dhs
Normal file
156
src/libs/kpunch/joystick/joystick.dhs
Normal file
|
|
@ -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.
|
||||
<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.
|
||||
.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 <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.
|
||||
|
||||
.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.
|
||||
365
src/libs/kpunch/joystick/joystick.h
Normal file
365
src/libs/kpunch/joystick/joystick.h
Normal file
|
|
@ -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 <inttypes.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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.<slot> = 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), <driver>.<name> for a
|
||||
// driver's own settings, <id>.axis<n> = min,center,max and <id>.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 <name>.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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue