diff --git a/Makefile b/Makefile
index 0147c5d9..1a1f1933 100644
--- a/Makefile
+++ b/Makefile
@@ -39,11 +39,11 @@
LIBS = $(sort $(notdir $(patsubst %/Makefile,%,$(wildcard src/libs/kpunch/*/Makefile))))
CORE_LIBS = libdvx libtasks
-.PHONY: all clean $(LIBS) loader widgets apps tools installer dist deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-modules test-samples coverage test-qa test-fuzz bench bench-compare
+.PHONY: all clean $(LIBS) loader widgets apps savers tools installer dist deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-modules test-samples coverage test-qa test-fuzz bench bench-compare
# Every build ends by making the release media in dist/ (a second or
# two), so dist/ always holds the latest DVX.
-all: $(LIBS) loader tools widgets apps installer deploy-helpsrc compile-help deploy-sdk
+all: $(LIBS) loader tools widgets apps savers installer deploy-helpsrc compile-help deploy-sdk
src/installer/mkDist.sh $(or $(FLOPPY),1.44M)
$(LIBS):
@@ -77,6 +77,11 @@ dist: all
apps: libdvx libtasks dvxshell tools
$(MAKE) -C src/apps/kpunch
+# The screen savers (bin/shared/savers): BASIC ones need bascomp, which
+# the apps build makes.
+savers: apps
+ $(MAKE) -C src/savers/kpunch
+
# Copies help sources into the module directories under bin/, so every
# module that owns one must be built first.
deploy-helpsrc: $(LIBS) widgets apps tools
@@ -132,7 +137,7 @@ test-samples:
# Builds every module first so the check sees the current binaries.
BASIC_STUB = obj/dvxbasic/basstub.app
-test-modules: $(LIBS) loader widgets apps
+test-modules: $(LIBS) loader widgets apps savers
src/test/shell/checkDxeExports.sh . $(BASIC_STUB)
# BASIC compiler / VM benchmarks (src/test/basic/basBench.c, programs in
@@ -288,6 +293,7 @@ clean:
$(MAKE) -C src/loader clean
$(MAKE) -C src/widgets/kpunch clean
$(MAKE) -C src/apps/kpunch clean
+ $(MAKE) -C src/savers/kpunch clean
$(MAKE) -C src/tools clean
$(MAKE) -C src/tools/proxy clean
$(MAKE) -C src/installer clean
diff --git a/config/ASSOC.INI b/config/ASSOC.INI
index 37ce1201..00386929 100644
--- a/config/ASSOC.INI
+++ b/config/ASSOC.INI
@@ -24,3 +24,11 @@ CTL = apps/kpunch/dvxbasic/dvxbasic.app
HLP = apps/kpunch/dvxhelp/dvxhelp.app
DHS = apps/kpunch/dvxhelp/helpedit.app
DPK = apps/kpunch/software/software.app
+MOD = apps/kpunch/player/player.app
+S3M = apps/kpunch/player/player.app
+XM = apps/kpunch/player/player.app
+IT = apps/kpunch/player/player.app
+MID = apps/kpunch/player/player.app
+RMI = apps/kpunch/player/player.app
+WAV = apps/kpunch/player/player.app
+VOC = apps/kpunch/player/player.app
diff --git a/config/DOSPROG.BMP b/config/DOSPROG.BMP
new file mode 100644
index 00000000..8ad13ad6
--- /dev/null
+++ b/config/DOSPROG.BMP
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8dad889643c3f2f3efc39e0d5c418233ce0b742d39e7545bbd079b4f1e10a344
+size 3126
diff --git a/config/README.md b/config/README.md
index 86fe89e1..9a34d669 100644
--- a/config/README.md
+++ b/config/README.md
@@ -16,6 +16,7 @@ the shipped list.
|-------------|-------------|-------------|
| `dvx.ini` | `bin/system/DVX.INI` | The settings a new user starts with |
| `ASSOC.INI` | `bin/system/ASSOC.INI` | File associations: which program opens each document type (shellAssoc.h; changed on the Control Panel's File Types page, in the user's ASSOC.INI) |
+| `DOSPROG.BMP` | `bin/system/DOSPROG.BMP` | The icon of a DOS program (EXE, COM, BAT) in Program Manager when it has no picture of its own (SHELL_DOS_ICON, shellProg.h) |
| `themes/cde.thm` | `bin/shared/themes/cde.thm` | CDE color theme |
| `themes/geos.thm` | `bin/shared/themes/geos.thm` | GEOS Ensemble theme |
| `themes/hotdog.thm` | `bin/shared/themes/hotdog.thm` | Hot Dog Stand theme |
@@ -99,6 +100,8 @@ compiled-in defaults.
|-----|---------|--------|-------------|
| `wallpaper` | (none) | file path | Path to wallpaper image |
| `mode` | stretch | stretch, tile, center | Wallpaper display mode |
+| `saver` | (none) | file name | Screen saver: a program in SHARED\SAVERS or the user's SAVERS |
+| `saverWait` | 10 | 1-60 | Minutes with no key or mouse input before the screen saver starts |
### [network] Section
@@ -125,9 +128,11 @@ as a whole in favour of the defaults.
| Key | Default | Values | Description |
|-----|---------|--------|-------------|
-| `port` | LPT1 | LPT1, LPT2, LPT3, FILE | Where printed output goes |
+| `port` | LPT1 | LPT1, LPT2, LPT3, COM1, COM2, COM3, COM4, FILE | Where printed output goes |
+| `baud` | 9600 | 300, 1200, 2400, 4800, 9600, 19200, 38400 | A serial printer's speed (port COM1-COM4) |
+| `handshake` | dtrdsr | none, xonxoff, rtscts, dtrdsr | How a serial printer holds the computer back: XON / XOFF characters, or its ready line on the computer's CTS or DSR (Apple's ImageWriter cable) |
| `file` | PRINTER.PRN | file path | The file used with port = FILE |
-| `model` | text | text, epson9, epson24, laserjet | Printer model; all but text print pictures |
+| `model` | text | text, epson9, epson24, laserjet, laserjet300, epson9color, epson24color, deskjet500c, deskjetcolor, postscript, postscriptcolor, imagewriter, imagewritercolor | Printer model; all but text print pictures; epson9color, epson24color, deskjet500c, deskjetcolor, postscriptcolor, and imagewritercolor in color |
| `linesPerPage` | 0 | 0, 10-200 | Lines per page; 0 = the model's usual length (66, laser 60) |
| `columns` | 80 | 20-255 | Characters across the page |
| `marginTop`, `marginBottom` | 3 | lines | Top and bottom margins (header / footer in their middle line) |
diff --git a/config/dvx.ini b/config/dvx.ini
index 471f6af0..ce52cfad 100644
--- a/config/dvx.ini
+++ b/config/dvx.ini
@@ -65,9 +65,19 @@ user =
password =
; Printer settings (Control Panel's Printer page and File > Page Setup).
-; port: LPT1, LPT2, LPT3 or FILE (print into the file named by file)
+; port: LPT1, LPT2, LPT3, COM1 to COM4 (a serial printer), or FILE
+; (print into the file named by file)
+; baud, handshake: a serial printer's speed (300 to 38400) and how it
+; holds the computer back: none, xonxoff, rtscts (its ready line on
+; CTS), or dtrdsr (on DSR, as Apple's ImageWriter cable wires it)
; model: text (any printer, text only), epson9, epson24 (Epson ESC/P
-; dot matrix) or laserjet (HP PCL); all but text print pictures
+; dot matrix), laserjet (HP PCL, 150 dpi), laserjet300 (HP
+; LaserJet III, DeskJet, and later; 300 dpi), epson9color,
+; epson24color (Epson with a color ribbon), deskjet500c,
+; deskjetcolor (DeskJet 550C and later), postscript,
+; postscriptcolor, imagewriter, or imagewritercolor (Apple
+; ImageWriter II); all but text print pictures, the color ones in
+; color
; linesPerPage: 0 = the printer's usual page length (66, laser 60)
; columns: characters across the page; margins in lines (top, bottom)
; and characters (left, right)
@@ -88,10 +98,14 @@ tabWidth = 8
wrap = true
header = &f
footer = Page &p
+baud = 9600
+handshake = dtrdsr
; Sound settings.
-; driver: auto (find the sound hardware), sb (Sound Blaster family),
-; speaker (PC speaker) or null (no sound)
+; driver: auto (find the sound hardware), wss (Windows Sound System),
+; sb (Sound Blaster family), gus (Gravis UltraSound), lpt (a
+; DAC on the printer port), 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
@@ -103,17 +117,37 @@ footer = Page &p
; 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)
+; midi: where MIDI music plays: auto (a Gravis UltraSound's or Sound
+; Blaster AWE's recorded instruments, else the synthesizer chip,
+; else a built-in synthesizer), mpu401 (an external synthesizer on
+; the MIDI port), fm (the chip), software (the built-in
+; synthesizer) or softwave (the SoundFont played by the computer)
+; midiIn: where MIDI input (a MIDI keyboard) comes from: none or mpu401
+; (the MIDI port, shared with MIDI music when midi = mpu401 too)
+; modules: where music modules (MOD, S3M, XM, IT) play: wavetable (a
+; Gravis UltraSound's or Sound Blaster AWE's own voices when no
+; other music plays there and the module fits, else mixed by
+; the computer) or software (always mixed by the computer)
; 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 for MIDI music (OP2 format): its name in
; SHARED\BANKS or USER\BANKS (an add-on's as VENDOR\NAME), or a path from
-; the DVX folder; not set, the bundled one. Control Panel picks it.
+; the DVX folder; not set, the bundled one. Control Panel picks it;
+; each MIDI music reads it as it starts.
+; soundFont = the recorded instruments (a SoundFont, .SF2 or .SBK) for a
+; Gravis UltraSound, a Sound Blaster AWE and softwave, named as midiBank
+; is, or the path of a Gravis UltraSound's ULTRASND.INI (its patches);
+; not set, KPUNCH\DVXGM.SF2 (an add-on), else the card's own.
; The Sound Blaster's port, IRQ and DMA come from the BLASTER setting (for
-; example SET BLASTER=A220 I5 D1 H5 in AUTOEXEC.BAT).
+; example SET BLASTER=A220 I5 D1 H5 in AUTOEXEC.BAT; an AWE adds E620),
+; the Gravis UltraSound's from ULTRASND (SET ULTRASND=220,1,1,11,5).
+; Optional: gusVoices (14-32, default 28: fewer play at a higher rate);
+; wssPort (530, 604, E80 or F40, hex), wssIrq (7, 9, 10 or 11, default 7)
+; and wssDma (0, 1 or 3, default 1) for a Windows Sound System card;
+; lptDac (auto, covox, disney or stereo; auto finds a Disney Sound Source
+; or a Stereo-on-1, a Covox must be named) and lptPort (hex, default
+; LPT1) for a DAC on the printer port.
[audio]
driver = auto
@@ -125,6 +159,8 @@ volume = 100
speakerPcm = false
fmTones = true
midi = auto
+midiIn = none
+modules = wavetable
mpuMode = gm
; Desktop sounds (Control Panel's Sounds page): the sound file (WAV or VOC)
diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html
index 753fdbc4..1a99ebb9 100644
--- a/docs/dvx_basic_reference.html
+++ b/docs/dvx_basic_reference.html
@@ -107,6 +107,7 @@ img { max-width: 100%; }
@@ -3653,6 +3714,17 @@ Loop While INKEY$ = ""
FULLSCREEN
Do
Loop While INKEY$ = ""
+
Writing a Screen Saver
+
A screen saver is a program with no form that shows its screen on the whole screen and draws until DVX closes it. Put the program (.app) in the SAVERS folder of your user folder (FileOpsKindDir$(FILEOPS_KIND_SAVERS, True)); it is then listed on Control Panel's Desktop page by its project Name. DVX starts it with the command line /S after the wait with no input, and closes its screen at the next key or mouse movement, which ends the program, so it needs no INKEY$ of its own.
+
SCREEN 13
+CLS
+FULLSCREEN ON
+
+DO
+ ' draw a frame
+ FRAMEWAIT 30
+LOOP
+
SysScreenSaver$, SysSetScreenSaver, SysScreenSaverStart, and SysIdleSeconds (system.bas) read and change the screen saver from a program.
File I/O
@@ -4681,6 +4753,62 @@ Print "Running from: " & App.Path
If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText()
A control or form of your own named Clipboard takes the name's place.
+
+
Printer Object
+
The Printer object draws pages on the printer chosen in the Control Panel, as in Visual Basic: text in any font at a size in points, lines, boxes, circles, dots, and pictures, anywhere on the page, page after page. It needs a printer model that prints pictures (the Control Panel's Printer page); on a color printer the colors print as they are, on the others in shades of grey.
+
On a PostScript printer the page is drawn by the printer itself, as lines, curves, and letters rather than dots, so it prints as sharp as the printer can. Text is in the printer's own Helvetica, Times, or Courier, whichever is nearest FontName: Courier for a fixed-pitch font or a name with Mono or Courier in it, Times for a name with Serif, Times, Roman, Tinos, or Ledger in it (but not Sans), and Helvetica for the rest. TextWidth and TextHeight measure the printer's letters, so text lines up as it will print.
+
A page is 8 by 10 inches. Its coordinates are twips (1440 an inch) until ScaleMode or Scale changes them, as on Visual Basic's Printer, so a program can place things in inches, centimetres, points, or its own units. Drawing goes to the page being made; NewPage prints it and starts the next, and EndDoc prints the last one and ends the document. The document starts when the first page is printed, at the printer and with the copies the program's last PrintChoose (print.bas) chose, or else as the Control Panel has them; while it prints, the Printing window shows, with Cancel. A program that ends with a page drawn or a document started has EndDoc done for it.
+
Method Description
+ ------ -----------
+ Printer.Print items Text at CurrentX / CurrentY in the font and ForeColor, as PRINT lays it out: ; joins, a comma goes to the next 14-character zone, TAB(n) and SPC(n) count characters of the font (its 0's width), and a new line goes down a line of the font. CurrentX / CurrentY move past it.
+ Printer.Line [(x1, y1)]-(x2, y2) [, color] [, B | BF] A line, a box, or a filled box, as on a PictureBox (STEP works too).
+ Printer.Circle (x, y), r [, color [, start, end [, aspect]]] A circle, ellipse, or arc; filled in FillColor when FillStyle is not transparent. Circles are round on paper on every printer.
+ Printer.PSet (x, y) [, color] One dot (DrawWidth across).
+ Printer.PaintPicture pic, x, y [, w, h [, srcX, srcY, srcW, srcH]] A picture (a PictureBox, a file, or LoadPicture's), stretched to w x h; without them, at its size on the screen (15 twips a pixel).
+ Printer.NewPage Print the page (even a blank one) and start the next; Page goes up by 1.
+ Printer.EndDoc Print the page drawn and end the document; Page goes back to 1.
+ Printer.KillDoc Throw the page drawn away and end the document (pages already printed stay printed).
+ Printer.TextWidth(text), TextHeight(text) The size text takes in the font, in the Printer's scale.
+ Printer.Scale (x1, y1)-(x2, y2), ScaleX, ScaleY As on a PictureBox: a scale of your own, and lengths from one scale to another.
+
Property Description
+ -------- -----------
+ CurrentX, CurrentY Where the next drawing starts.
+ ForeColor, FillColor, FillStyle, DrawWidth, DrawStyle As on a PictureBox (black, transparent fills, a solid pen 1 dot wide to start).
+ ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight The scale (vbTwips to start).
+ FontName, FontSize, FontBold, FontItalic, FontUnderline, FontStrikethru The font of Print's text; FontSize is in points (12 to start, in DejaVu Sans). Text prints crisp where the font has large sizes (the Printer Fonts add-on gives DejaVu Sans, Serif, and Sans Mono them), and blockier where a screen size is enlarged.
+ Copies How many times each page prints (1 to 99); as PrintChoose chose when it is not set.
+ Page The page being drawn (1 first). Read only.
+ Width, Height The page's size in twips (11520 by 14400). Read only.
+ TwipsPerPixelX, TwipsPerPixelY Twips in one of the printer's dots. Read only.
+ ColorMode vbPRCMColor on a color printer, else vbPRCMMonochrome. Read only.
+ Orientation vbPRORPortrait: pages print upright only.
+ DeviceName, Port The printer model and where it is connected, as the Control Panel names them. Read only.
The Printer has no Cls, Point, PAINT, or DRAW (Visual Basic's Printer has none of them either), and no Printer.Font object: use FontName, FontSize, and the other Font properties. A raster operation given to PaintPicture is not used on paper. A control or form of your own named Printer takes the name's place.
+
Printer Constants
+
Constant Value Description
+ -------- ----- -----------
+ vbPRORPortrait 1 Printer.Orientation: upright pages (the only one)
+ vbPRORLandscape 2 Printer.Orientation: sideways pages (not supported)
+ vbPRCMMonochrome 1 Printer.ColorMode: black (and grey) only
+ vbPRCMColor 2 Printer.ColorMode: color
+
Screen Object
The Screen object is the DVX desktop, as in Visual Basic: its size, so a program can place its forms (for example in a corner of the screen), its colour depth, the mouse pointer, and the arrangement of the windows on it.
@@ -4894,6 +5022,25 @@ End Sub
vbHimetric 8 Hundredths of a millimetre (ScaleX / ScaleY only; a Picture's Width and Height)
vbPicTypeNone 0 Picture.Type of an empty picture
vbPicTypeBitmap 1 Picture.Type of a loaded picture
+
Fill and Pen Constants
+
The values of FillStyle and DrawStyle on Forms, PictureBoxes, and the Printer (see Drawing on Forms and PictureBoxes).
+
Constant Value Description
+ -------- ----- -----------
+ vbFSSolid 0 FillStyle: filled solid in FillColor
+ vbFSTransparent 1 FillStyle: not filled (the default)
+ vbHorizontalLine 2 FillStyle: horizontal lines
+ vbVerticalLine 3 FillStyle: vertical lines
+ vbUpwardDiagonal 4 FillStyle: lines rising to the right
+ vbDownwardDiagonal 5 FillStyle: lines falling to the right
+ vbCross 6 FillStyle: a cross hatch
+ vbDiagonalCross 7 FillStyle: a diagonal cross hatch
+ vbSolid 0 DrawStyle: a solid pen (the default)
+ vbDash 1 DrawStyle: dashes
+ vbDot 2 DrawStyle: dots
+ vbDashDot 3 DrawStyle: dash, dot
+ vbDashDotDot 4 DrawStyle: dash, dot, dot
+ vbInvisible 5 DrawStyle: no lines or outlines (fills still drawn)
+ vbInsideSolid 6 DrawStyle: solid
VarType Constants
The values VARTYPE returns (see Variants). For an array, VARTYPE adds vbArray to the type of its elements.
Constant Value Kind of value
@@ -4957,8 +5104,8 @@ End Sub
security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace)
joystick.bas Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
- sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
- music.bas Music: play MOD, S3M, XM, IT, and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$)
+ sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
+ music.bas Music: play MOD, S3M, XM, IT, MIDI, WAV, and VOC files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); instruments from banks and SoundFonts, a sound card's own (MusicSetBank, MusicPatchSet$, MusicRomBank$); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$); what a MIDI keyboard plays, heard as it comes and recorded into MIDI files (MidiInOpen, MidiInMessage, MusicMidiSend, MidiRecordNew, MidiRecordSave, ...)
fileops.bas Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving, and deleting whole folders at once or as a job with progress, Cancel, and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...)
system.bas What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, and the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...)
font.bas Fonts: the font families DVX has, the sizes each comes in, whether one is fixed-pitch, and how wide text and how tall a line is in a font (FontFamilyCount, FontFamilyName$, FontFamilyFixed, FontSizeCount, FontSizeAt, FontNearestSize, FontTextWidth, FontLineHeight, FontAscent, FontRescan)
@@ -4967,7 +5114,7 @@ End Sub
compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...)
designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...)
- shell.bas DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
+ shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files and planning what installing them takes; installing and removing as a job with progress; trusting a publisher's key; and making packages, signing them, and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFile, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuild, PkgSign, PkgKeygen, ...)
json.bas JSON: read a document (a web service's reply, a settings file) and find values in it by path such as packages[3].version, and quote text for JSON of your own (JsonParse, JsonText$, JsonNumber, JsonCount, JsonKey$, JsonQuote$, ...)
http.bas Web requests: GET a page, a file, or a web service's answer, or POST to one, over the network a little at a time with progress, to memory or to a file (resuming one left part-way), following redirects (HttpOpen, HttpStart, HttpRunFor, HttpStatus, HttpBody$, HttpSaveTo, ...)
@@ -5079,7 +5226,7 @@ End Sub
SerGetRi(com) True while the other end rings.
SerLineErrors(com) Errors seen on received data, added up: SER_LINE_OVERRUN (data came faster than it was read), SER_LINE_PARITY, SER_LINE_FRAMING, SER_LINE_BREAK; 0 for none.
Printing
-
With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header, and a footer with the document name, date, and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does). PrintText and PrintPicture print without asking first. Each returns True when it printed.
+
With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header, and a footer with the document name, date, and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does), in color on a color printer (PrintCanPrintColor says whether it is one) and in shades of grey on the others. PrintText and PrintPicture print without asking first. Each returns True when it printed.
Sub mnuPrint_Click ()
PrintTextDialog txtNotes.Text, "Notes"
End Sub
@@ -5087,7 +5234,7 @@ End Sub
Sub mnuPageSetup_Click ()
PrintPageSetup
End Sub
-
For printer output a line at a time, see LPRINT and OPEN "LPT1:".
+
For printer output a line at a time, see LPRINT and OPEN "LPT1:". To draw whole pages (text in any font and size, lines, boxes, circles, and pictures), use the Printer object.
A program that prints its own way has more:
Function Description
-------- -----------
@@ -5119,8 +5266,13 @@ End If
Function Description
-------- -----------
PrintModelCount() / PrintModelLabel$(model) How many printer models there are, and the name of each.
- PrintDestCount() / PrintDestLabel$(dest) How many places the printer can be connected to (LPT1, LPT2, LPT3, a file), and the name of each.
+ PrintDestCount() / PrintDestLabel$(dest) How many places the printer can be connected to (LPT1 to LPT3, COM1 to COM4, a file), and the name of each.
PrintDestIsFile(dest) True for the destination that prints into a file.
+ PrintDestIsSerial(dest) True for a serial port (COM1 to COM4), which needs a speed and a handshake.
+ PrintBaudRate(index) The serial speeds offered, from index 0 (0 past the last).
+ PrintHandshakeCount() / PrintHandshakeLabel$(handshake) How many handshakes there are, and the name of each (None, XON / XOFF, Hardware (CTS), Hardware (DSR, Apple)).
+ PrintSettingBaud(prefs), PrintSettingHandshake(prefs) A serial printer's speed and handshake in the settings.
+ PrintSerialStore(prefs, baud, handshake) Put a serial printer's speed and handshake into the settings; False for a speed not offered.
PrintSettingModel(prefs) The printer model in the settings.
PrintSettingDest(prefs) Where the printer is connected.
PrintSettingFile$(prefs) The file printed into when the destination is a file.
@@ -5148,7 +5300,7 @@ End Sub
Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$, and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.
Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr, and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles, and FileOpsTreeFolders (the size of a whole folder), and FileOpsFormatSize$.
Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb, and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when nothing can be saved: DVX runs from such a disk and there is no TEMP folder to keep the user folder in. FileOpsTempDir$() is the folder for scratch files ("" when there is none).
-
DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, and MIDI instrument banks come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, and FILEOPS_KIND_BANKS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it.
+
DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, FILEOPS_KIND_BANKS, and FILEOPS_KIND_SAVERS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it.
FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder at once. For a long copy, move, or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
Dim job As Long
@@ -5176,7 +5328,7 @@ Sub Timer1_Timer ()
End Sub
Working with DVX
With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC.
-
Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), or opens a document in the program set for its type; it returns True when it started. ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (0 when it did not start; DVX has already said why). ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
+
Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$.
File types. ShellAssocProgram$(extension) tells which program opens files with that extension (with ",arguments" after it when it is given any), ShellAssocCommand$(document) just the program that opens a document. ShellAssocCount() file types are set; ShellAssocExt$(index) and ShellAssocValue$(index) read them (index from 0). ShellAssocSet(extension, program) sets one and ShellAssocRemove(extension) removes one, saved at once; ShellAssocNormalize$(extension) is the extension as stored (".txt" gives "TXT"; "" when it cannot be one).
Running programs. ShellRunningCount() says how many programs are running, and ShellRunningId(index) is the id of each one (index from 0; the same id ShellRunProgram returns). ShellAppId() is this program's own id. ShellRunningName$(id) and ShellRunningPath$(id) are the name a running program shows and the path of its .app ("" when it is not running), and ShellRunningFind(program) is the id of a running copy of a program, 0 when it is not running. ShellRunningActivate id brings a program to the front the way Task Manager's Switch To does (a minimized window is restored). ShellRunningClose id asks a program to close as if the user had closed its windows: it may ask to save its work and the user may cancel, and it is asked once your code has returned to DVX, so check from a Timer whether it is still running. ShellRunningKill id ends a program at once without asking it, the way Task Manager's End Task does: whatever it had not saved is lost, so keep it for a program that does not respond. ShellAppIsHung(id) is True when a program has been busy for 5 seconds or longer ("Not responding" in Task Manager). A program cannot end itself with ShellRunningKill (END does that). ShellAppMemory(id) is how many bytes of memory a program is using. ShellChangeCount() changes whenever a program starts or ends: check it from a Timer and update what you show when it is not the value you saw last. ShellTaskManager opens the Task Manager, ShellSystemInfo$() describes the computer (several lines), and ShellExitDvx exits DVX: every program is asked to close, each asking about unsaved work, and a program that stays open stops the exit (ask the user first).
@@ -5235,7 +5387,7 @@ End Sub
End Sub
Add-on Packages
With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.
-
What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it.
+
What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\BANKS\KPUNCH\DMXOPL.OP2"; "" when none did).
Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is.
offer = PkgOfferFile(s, path)
If offer >= 0 And PkgPlan(s, CStr(offer)) Then
@@ -5289,12 +5441,13 @@ body = "{" & JsonQuote$("name") & ": " &am
Screen mode: SysVideoModeCount() says how many modes the display offers (numbered from 0), and SysVideoModeWidth, SysVideoModeHeight, and SysVideoModeBpp (bits per pixel) describe each. SysSetVideoMode(wide, high, bpp) changes the mode and keeps every window; it returns False when the mode could not be set (the old one stays), also while a program has the whole screen to itself. A mode the monitor cannot show leaves the user unable to answer, so do as Control Panel does: ask "Keep this mode?" and go back to the old mode by itself after some seconds. Screen.Width, Screen.Height, and Screen.BitsPerPixel tell the mode in use, and SYS_VIDEO_DEFAULT_WIDTH, SYS_VIDEO_DEFAULT_HEIGHT, and SYS_VIDEO_DEFAULT_BPP the mode DVX starts in when its settings name none. SysScreenshot(path) saves the whole screen as a picture file (BMP when path ends in .bmp, PNG otherwise) and returns True when it was written.
Colors: the system colors are numbered from 0 to SysColorCount() - 1. SysColorLabel$(id) is the text to show for one ("Desktop", "Window Face" ...), SysColorName$(id) the name it is stored as, and SysColorId(colorName) the id of a name (-1 for none) -- to draw in the user's colors, for example SysGetColor(SysColorId("desktop")). SysGetColor and SysSetColor read and set a color as an RGB() value; each SysSetColor redraws the whole screen. SysResetColors goes back to DVX's own colors. SysLoadTheme(path) and SysSaveTheme(path) read and write a theme file (.THM; DVX's own are in SHARED\THEMES, the user's in the THEMES folder of the user folder, FileOpsKindDir$(FILEOPS_KIND_THEMES, True)); both return True when they worked.
Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG, or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile, or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use.
+
Screen saver: SysSetScreenSaver fileName, minutes chooses the screen saver -- a program in the SAVERS folders, named as FileOpsKindList(FILEOPS_KIND_SAVERS) lists it, or "" for none -- and the minutes with no key or mouse input before it starts (SCREEN_SAVER_WAIT_MIN to SCREEN_SAVER_WAIT_MAX; DVX's default is SCREEN_SAVER_WAIT_DEFAULT). SysScreenSaver$() and SysScreenSaverWait() tell what is in use, SysScreenSaverStart() starts it now as Control Panel's Test does (False when none is chosen or one is running), and SysScreenSaverRunning() is True while one runs. SysIdleSeconds() is how long it has been since the last key or mouse input anywhere on the screen.
Text: SysSetTextSmoothing True smooths the edges of text and SysSetTextSmoothing False stops it; SysTextSmoothing() tells which the user chose. Text is smoothed only when the screen shows more than 256 colors, and only in the fonts and sizes that come smoothed. Every window is drawn again when it changes.
Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower), and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys, and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).
The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton, and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl, and Alt keys held now (vbShiftMask, vbCtrlMask, and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask, and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.
Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).
System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own.
-
Saving: SysColorsStore, SysWallpaperStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.
+
Saving: SysColorsStore, SysWallpaperStore, SysScreenSaverStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, the screen saver, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysScreenSaverLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.
Dim snap As Long
Dim oldW As Long, oldH As Long, oldBpp As Long
@@ -5420,8 +5573,8 @@ Sub cmdFire_Click ()
End If
End Sub
WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas.
-
PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
-
A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends.
+
PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
+
A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.
Dim Shared laser As Long
Sub Form1_Load ()
@@ -5433,15 +5586,21 @@ Sub Fire (x As Integer)
v = SoundPlay(laser, 80, (x - 160) * 100 \ 160, False)
SoundSetPitch v, 90 + Int(Rnd * 20)
End Sub
-
A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan, and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed:
+
A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan, SoundSetPitch, SoundPause, and SoundResume. SoundStreamFlush stream drops what is waiting to play, so what is written next is heard at once (to jump to another place in what you play), and SoundStreamClose stream stops it. A Timer keeps a stream fed:
Sub Timer1_Timer ()
Dim taken As Long
taken = SoundStreamWrite(stream, pending)
pending = Mid$(pending, taken + 1)
If Len(pending) = 0 Then pending = MakeMoreSound()
End Sub
+
A program can record sound from the sound card's microphone, line, or CD input into a WAV file. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.
+
If RecordStart("NOTE.WAV", 0, 0, 0, RECORD_MIC) Then
+ ' ... a Timer shows RecordLength() and RecordLevel()
+ ok = RecordStop()
+End If
On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).
-
sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts).
+
On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory" and "freememory" (bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100). WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. Everything a program took is given back when it ends.
+
sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play, SoundMidiInCount() and SoundMidiInLabel$(index) where MIDI input can come from, and SoundModulesCount() and SoundModulesLabel$(index) where music modules can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), SoundSettingModules(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, modules choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, modules, volume stores them (a new driver or MIDI choice takes effect when DVX next starts, a modules choice with the next music); SoundSettingMidiIn(handle) and SoundSettingMidiInStore handle, midiIn read and store the MIDI input choice (it takes effect when DVX next starts).
Dim ini As Long
Dim i As Long
@@ -5452,8 +5611,12 @@ For i = 0 To SoundEventCount() - 1
Next i
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.
+
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 (on a Gravis UltraSound's or Sound Blaster AWE's own voices, which takes the computer little time), 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, and WAV and VOC sound files wherever the computer can play sampled sound (a long WAV recording is read from the disk as it plays, so it needs no memory). 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.
+
MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY takes, with its notes on the General MIDI instruments its I commands name (the piano before any I): the tune can then be played in the background, paused, stopped, and made quieter like any music. MIDI music plays on the instruments chosen in Control Panel; MusicSetBank music, bank plays one music on another instrument bank, named as in the banks folders ("" is DVX's standard bank), before the music first plays (MusicError$ says when the bank is missing or damaged). A SoundFont (.SF2 or .SBK) gives the recorded instruments for a wavetable card or the built-in SoundFont synthesizer, as can a Gravis UltraSound's own patches: MusicPatchSet$() names them (their ULTRASND.INI, "" when there are none), and MusicRomBank$() names the instruments built into a Sound Blaster AWE card ("" when Creative's software is not installed).
Dim Shared tune As Long
Sub Form1_Load ()
@@ -5482,8 +5645,44 @@ Sub tmrNoteOff_Timer ()
MusicNoteOff keys, 1, 60
tmrNoteOff.Enabled = False
End Sub
-
MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi", or "live"), "where" ("stream", "mpu401", "fm", or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).
-
A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present", and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again), and MidiRelease gives the port back.
+
MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title, or the name a MIDI file gives itself; "" for a sound file), "output" (where it plays, for people), "kind" ("module", "midi", "live", or "sound"), "where" ("stream", "mpu401", "fm", "software", "wave", or "softwave"; "wave" is a wavetable card's voices, for a module too; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).
+
A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present", "owned" (a program has it), "input" (it brings MIDI in), and "inputowned" (a program reads what comes in). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again), and MidiRelease gives the port back.
+
When the settings take MIDI input from the MIDI port (Control Panel, Sounds, MIDI in), a program can read what a MIDI keyboard or another instrument plays. MidiInOpen() starts reading (False when there is no MIDI input or another program reads it; SoundError$() says which) and MidiInClose stops; a program's reading stops when it ends. MidiInMessage() gives the next message that came, or MIDI_NONE (-1) when none waits. A message is packed into one number: the status is midiMsg AND 255 (its kind, midiMsg AND MIDI_KIND_MASK, is MIDI_NOTE_ON, MIDI_NOTE_OFF, MIDI_CONTROL, MIDI_PROGRAM, MIDI_PITCH_BEND, MIDI_KEY_PRESSURE, or MIDI_CHANNEL_PRESSURE, and its channel, numbered as MusicNoteOn numbers them, is (midiMsg AND MIDI_CHANNEL_MASK) + 1), the first data byte (the note) is (midiMsg \ 256) AND 255, and the second (the velocity) (midiMsg \ 65536) AND 255. A note on with a velocity of 0 is a note off too. MidiInTime() is when the message MidiInMessage gave last came and MidiInClock() the time now, both in milliseconds since MidiInOpen; MidiInLost() counts bytes lost because the program read too late. MusicMidiSend music, midiMsg plays a message as it came on a live music (MIDI thru), so what is played is heard.
+
A MIDI recording keeps messages with their times and saves them as a MIDI file that MusicLoad plays. MidiRecordNew() returns a recording number (0 when there is no room; MusicError$() says why), MidiRecordAdd recording, milliseconds, midiMsg adds a message (MidiInTime() is the time to give, when the recording started with MidiInOpen), MidiRecordCount(recording) and MidiRecordLength(recording) tell how many messages it has and when the last came, MidiRecordSave(recording, fileName) writes the file (False when it could not be written; MusicError$() says why; a note still held at the end is let go), and MidiRecordFree recording frees it. A program's recordings are freed when it ends. The Sound Recorder records a MIDI keyboard this way:
+
Dim Shared take As Long
+Dim Shared keys As Long
+
+Sub cmdRecord_Click ()
+ If MidiInOpen() Then
+ take = MidiRecordNew()
+ keys = MusicMidiOpen()
+ tmrTake.Enabled = True
+ End If
+End Sub
+
+Sub tmrTake_Timer ()
+ Dim midiMsg As Long
+
+ midiMsg = MidiInMessage()
+
+ Do While midiMsg <> MIDI_NONE
+ MusicMidiSend keys, midiMsg
+ MidiRecordAdd take, MidiInTime(), midiMsg
+ midiMsg = MidiInMessage()
+ Loop
+End Sub
+
+Sub cmdStop_Click ()
+ tmrTake.Enabled = False
+ MidiInClose
+ MusicFree keys
+
+ If Not MidiRecordSave(take, "SONG.MID") Then
+ Print "Not saved: " & MusicError$()
+ End If
+
+ MidiRecordFree take
+End Sub
Network Connections
With net.bas in the project, a program talks to other computers over the network set up in the Control Panel. TcpConnect returns a handle (0 when it failed; NetError$ says why), TcpSend sends text, and TcpRecv$ returns whatever has arrived without waiting, so a Timer keeps the program responsive:
Dim conn As Long
@@ -6012,7 +6211,8 @@ End Sub
CurrentX Single 0 Where the next drawing or printing starts.
CurrentY Single 0
DrawWidth Integer 1 Pen width in pixels.
- FillStyle Integer 1 How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour.
+ DrawStyle Integer 0 The pen's lines and outlines: 0 solid, 1 dashes, 2 dots, 3 dash-dot, 4 dash-dot-dot (a pen 1 pixel wide only; a wider one draws solid), 5 none at all (fills are still drawn), 6 solid. Other values are an error. The constants vbSolid to vbInsideSolid name them.
+ FillStyle Integer 1 How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour. The constants vbFSSolid to vbDiagonalCross name them.
FillColor Long &H000000 Colour boxes and circles are filled with.
FontScale Integer 1 How many times as large Print writes its text (1 to 8): each dot of the font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not.
AutoRedraw Boolean False True: no Paint events (see above).
@@ -6425,7 +6625,7 @@ Dim g As New Form1 ' declares and makes a copy at once
End If
End Sub
Drawing
-
Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle, and so on.
+
Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, DrawStyle, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle, and so on.
Example
Sub Form1_Load ()
Form1.Caption = "Hello World"
@@ -6526,7 +6726,7 @@ End Sub
PropertyChanged name Tells a designer that a property changed, so its property sheet shows the new value.
Cls, Line, PSet, Circle, Print, Point, PaintPicture, DrawFocusRect, TextWidth, TextHeight, Scale, ScaleX, ScaleY Drawing, as on a PictureBox.
DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, TextWidthAccel, AccelKey Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox.
- CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, FontScale, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox.
+ CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, FontScale, DrawWidth, DrawStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox.
The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control.
The drawing stays until you draw again. UserControl_Paint runs when the control first appears, after its size changed, when you call Refresh, and after you call Invalidate; draw everything there and call UserControl.Invalidate (or UserControl.Refresh, to see the result at once) whenever what it shows changes.
Programs that can print have Print... in their File menu, and programs that print text also have Page Setup.... Every program prints to the same printer, so you only tell DVX once which printer you have and where it is plugged in (see Changing Settings). You can also send printouts into a file instead of a printer.
The Print Dialog
-
File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, or a file), how many copies you want, and (for a document of several pages) whether to print all of it or only some pages. Then choose Print. When the printout goes into a file, type the file's name, or click Browse... to pick it.
+
File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, a serial port such as COM1, or a file), how many copies you want, and (for a document of several pages) whether to print all of it or only some pages. Then choose Print. When the printout goes into a file, type the file's name, or click Browse... to pick it.
While a printout is going to the printer, a small Printing window shows it is busy. You can keep working in other programs, and Cancel stops the printout.
When the Printer Has a Problem
If the printer is out of paper, switched off, off line, or not answering, a message says what is wrong. Fix the problem and choose Retry to carry on, or Cancel to stop printing. Nothing waits forever.
Page Setup
File > Page Setup... chooses how text is laid out on paper: the margins, how many lines fit on a page, how wide a line is, and a header and footer printed on every page (for example the file name and the page number). Your choices are kept for next time and used by every program that prints text.
Pictures
-
Pictures are printed in black and white, as large as fits on the page. Printing pictures needs your printer's model to be set; with the setting for text only, a program tells you so.
+
Pictures print as large as fits on the page: in color on a color printer (one with a color ribbon or cartridge), and in shades of grey on the others. Printing pictures needs your printer's model to be set (see Changing Settings); with the setting for text only, a program tells you so.
+
Some programs draw whole pages of their own, such as reports with charts. On a PostScript printer the printer draws those pages itself, as sharp as it can print. On other printers their text prints crisper with the Printer Fonts add-on installed (see Adding and Removing Software).
Running Programs
@@ -2310,10 +2428,10 @@ img { max-width: 100%; }
Double-click an icon to start that program, or select it with the arrow keys and press Enter. The program opens in a new window. While it is loading, the mouse pointer shows an hourglass. The Program Manager's Search box finds a program in every group as you type its name.
To start a program file that has no icon, choose Run... from the Program Manager's File menu and type or pick the file.
Opening Documents
-
A document can be opened the same way as a program: DVX starts the program set up for its kind of file (the letters after the dot in its name, such as TXT) and gives it the document, which the program opens right away. Which program opens which kind of file is set in the settings program (see Changing Settings).
+
A document can be opened the same way as a program: DVX starts the program set up for its kind of file (the letters after the dot in its name, such as TXT) and gives it the document, which the program opens right away: a picture opens in the Image Viewer, and a music or sound file (MOD, S3M, XM, IT, MID, RMI, WAV, or VOC) plays in Media Player. Which program opens which kind of file is set in the settings program (see Changing Settings).
Running Several Programs
You can run many programs at the same time, each in its own window. They all keep running while you work in another one.
-
Most programs can only run once at a time. If you start a program that is already running, DVX tells you so; find its window instead with Alt+Tab or the Task Manager. Some programs can be opened more than once, each copy in its own window.
+
Most programs can only run once at a time. If you start a program that is already running, DVX tells you so; find its window instead with Alt+Tab or the Task Manager. A few take the document you opened instead: a music file opened while Media Player is running is added to its list and plays. Some programs can be opened more than once, each copy in its own window.
Switching Between Programs
Click anywhere in the program's window, or
@@ -2322,6 +2440,8 @@ img { max-width: 100%; }
Programs That Use the Whole Screen
Some programs, games especially, can fill the whole screen instead of a window (many use Alt+Enter to switch). Your other programs keep running while they do. Alt+Tab and Ctrl+Esc still work and bring the desktop back; when you return to the program, it fills the screen again.
+
Running DOS Programs
+
DVX can start ordinary DOS programs too (files ending in .EXE, .COM, or .BAT): from an icon in the Program Manager, from Run..., or by double-clicking one in the File Manager. Such a program has the whole screen to itself. Before it starts, DVX names the programs that will close and asks whether to go on; each one closes as it would if you closed it, asking about unsaved work. When the DOS program ends, DVX comes back with the Program Manager. The Program Manager's own help (DOS Programs) tells how to add one as an icon.
Closing a Program
Close the program's main window, or choose Exit from its File menu. A program that has unsaved work usually asks whether you want to save it first. See Working with Windows for all the ways to close a window.
Help for Each Program
@@ -2370,13 +2490,14 @@ img { max-width: 100%; }
Changing Settings
-
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.
+
The mouse, the screen colors, the desktop picture and the screen saver, 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.
+
A screen saver can fill the screen with a moving picture when the computer has not been used for a while; pressing a key or moving the mouse brings your work back. Choose one, and how many minutes to wait, on the Desktop page of the settings program. There is none until you choose one.
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.
Where your settings and files are kept
Everything DVX and its programs save for you goes in one folder, the USER folder inside the DVX folder (for example C:\DVX\USER): your settings, each program's settings and the files it saves (such as saved games), Terminal's downloads, and DVX's log. To back up your work, copy this one folder. Installing a new version of DVX never changes it.
-
The fonts, sounds, themes, and desktop pictures that come with DVX are in the SHARED folder. To add your own, put them in the folder of the same name in your USER folder: FONTS, SOUNDS, THEMES, or WPAPER. The settings program lists yours together with DVX's, and a file of yours with the same name as one of DVX's is used instead of it. Themes you save go there too.
+
The fonts, sounds, themes, desktop pictures, and screen savers that come with DVX are in the SHARED folder. To add your own, put them in the folder of the same name in your USER folder: FONTS, SOUNDS, THEMES, WPAPER, or SAVERS. The settings program lists yours together with DVX's, and a file of yours with the same name as one of DVX's is used instead of it. Themes you save go there too.
To keep the USER folder somewhere else (on another disk, or one for each person who uses the computer), set DVXUSER to that folder before starting DVX, for example SET DVXUSER=D:\MYDVX in AUTOEXEC.BAT.
@@ -2461,7 +2582,8 @@ img { max-width: 100%; }
When an add-on package's library or widget cannot be loaded (it was made for another version of DVX, it needs something that is not installed, or its file is damaged), DVX starts without it and a message once the desktop is up lists what was left out and why. The rest of DVX works as usual. Reinstall the add-on (a version made for this DVX), or remove it with the Software program.
You can pick another resolution at any time with the settings program (see Changing Settings).
There is no sound
-
DVX finds the sound hardware by itself. A Sound Blaster needs its BLASTER setting in AUTOEXEC.BAT, for example SET BLASTER=A220 I5 D1 H5 (the card's manual gives the numbers). Without a sound card DVX uses the PC speaker, which plays tones and beeps but not recorded sounds. The settings program shows the sound hardware in use, lets you pick it yourself, and sets the volume; if a sound is set to none there, nothing is heard for it. Without any sound hardware DVX and its programs work the same, silently.
+
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, and a Gravis UltraSound its ULTRASND setting, for example SET ULTRASND=220,1,1,11,5 (the card's manual or setup program gives the numbers; a Sound Blaster AWE's setting includes E620 for its music chip). A Windows Sound System card is found at its usual addresses; a sound device on the printer port (a Disney Sound Source or a Stereo-on-1) is found on its own, while a Covox Speech Thing must be chosen in the settings file (DVX.INI [audio] lptDac = covox). 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.
+
MIDI music plays on the sound card's own recorded instruments (a Gravis UltraSound or a Sound Blaster AWE) when DVX's General MIDI SoundFont is installed (the Software program offers it) or the card's own instruments are there; otherwise on its music chip, or on DVX's own synthesizer. Music modules (MOD, S3M, XM, and IT files) play on a Gravis UltraSound's or a Sound Blaster AWE's own voices, which takes the computer little time; on other cards the computer mixes them, which takes much of a slow computer's time (the Control Panel's Sounds page can have them always mixed).
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.
Installing, upgrading, and removing DVX
@@ -3616,7 +3738,7 @@ DVX_ABI_STAMP(mylib);
dvxText.h -- Fonts
-
The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps.
+
The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps. More files of the same family and style (up to four in all) add the sizes the face lacks, each size read from the file it came in: the printer-fonts add-on gives the DejaVu faces their large sizes that way, from FONTS\KPUNCH.
dvxFontOpen gives a BitmapFontT every text call draws and measures with. It never scales: it picks the face's size nearest the one asked for. A style the family has no face for is made from the nearest face -- bold by thickening each stroke a column (two from 24 pixels), italic by slanting a column per four rows -- and the font's flags say so. Every character of code page 437 draws: what a face lacks is filled in when the font loads. Box drawing, block, and shade characters are drawn to fill the character cell (the pitch, or the digit 0's width) so lines join, also in place of a face's own when its file asks; other characters come from FONT_FALLBACK_FAMILY (FONT_FALLBACK_FIXED for a fixed-pitch face, centered in its pitch) at the nearest size and the same style, or from the system font when that family is not installed.
A face can also hold a size smoothed (anti-aliased): each pixel one of four levels, from none to full, drawn as that blend of the text color and what is under it. When smoothing is on (dvxFontSetSmooth) such a size opens smoothed, with FONT_FLAG_GRAY set and FONT_GRAY_BPP bits per pixel; other sizes and faces stay 1-bit. At 8 bits per pixel there are no colors to blend: the pixels of FONT_GRAY_HALF and more are drawn, the rest not.
Fonts are shared and counted: the same family, size, and style opened again is the same font, and dvxFontClose gives one up. A few fonts no one holds stay loaded so reopening them is quick. A font's memory belongs to DVX, not to the program that opened it. The family list is read from DVX_FONTS_DIR the first time it is needed; a file that cannot be read is logged and skipped.
Add the .DFN at path, or held in data (length bytes, copied at once; name names it in the log), as a face of its family for program owner (an app id; 0 for every program, as SETUP.EXE adds the fonts it carries). Families added for owner 0 are seen by every program and kept by dvxFontRescan.
-
Returns: True when added; false, logged, when the font cannot be read, calls its family "System", or owner has a face of that family and style already.
+
Returns: True when added; false, logged, when the font cannot be read, calls its family "System", or brings no size that owner's face of that family and style lacks (a second file of a face adds its sizes to it).
Add every font resource (DVX_RES_FONT) of the file at path -- a program, a library, or a resource file -- for owner. The shell calls it for each program it loads.
dvxRunDos runs a DOS program (an .EXE, .COM, or .BAT file, or a command DOS knows) with DVX stepped aside, and comes back. Full screen, menus, and drags end; program is made absolute (a bare name stays one, for the PATH) and given to DOS by its short name, with args after it, in folder (NULL or "": the program's own folder). platformDosRun then runs the suspend hooks (newest first), lets the keyboard hook and the timer service go (the timers are kept and stand still), sets text mode, gives the INT 24h handler and Ctrl+Break back to DOS, makes the folder current, and runs the command line with system() (as at the DOS prompt: a batch file through the command interpreter, PATH, redirection); with pause it prints "Press any key to return to DVX." and waits for a key. Everything is taken back in the opposite order, the desktop's mode (and palette) set again, and the resume hooks run (oldest first); dvxRunDos sets the mouse up again where it was, forgets held keys, and repaints the whole screen. DVX logs "DOS: running" and "DOS: back from" with the command line. Nothing of DVX runs meanwhile: close the other programs first (the shell's shellRunDos asks them). Returns the program's exit code, or -1 when it could not be run (errno: ENOENT, ENOMEM, E2BIG for a command line longer than DOS's 126 characters). DVX stays in memory: the program gets the conventional memory DVX's DOS part and the DPMI host leave. On the host, platformDosRun is system() in the folder with the hooks around it.
+
platformPathAbsolute makes a path absolute against the current folder (it need not exist).
+
Idle Time: dvxIdleMs
+
uint32_t dvxIdleMs(const AppContextT *ctx);
+
The milliseconds since the last key or mouse input: a key (pressed, released, or typed), the mouse moving, a button, or the wheel, injected input (a test script, SysSendKey) included, and input that came while DVX was busy and was thrown away. DVX coming back from a DOS program or from full-screen mode counts as input too. The shell's screen saver starts after DVX.INI [desktop] saverWait minutes of it (shellSaver.h); BASIC: SysIdleSeconds. The time is kept in AppContextT.lastInputTime (platformClock ticks).
Accelerators
dvxCreateAccelTable
AccelTableT *dvxCreateAccelTable(void);
@@ -5362,7 +5493,7 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
WindowT *window Owning window
int32_t x, y, w, h Computed geometry (relative to content area)
int32_t calcMinW, calcMinH Computed minimum size (from layout pass)
- int32_t minW, minH, maxW, maxH, prefW, prefH Size hints (tagged sizes)
+ int32_t minW, minH, maxW, maxH Size hints (tagged sizes): the least and the most the layout gives the widget
int32_t weight Extra-space distribution weight (0=fixed, 100=normal)
WidgetAlignE align Main-axis alignment for children
int32_t spacing, padding Tagged sizes for child spacing and padding
@@ -6727,9 +6858,16 @@ void platformIrqRestore(uint32_t state);
typedef void (*PlatformShutdownFnT)(void);
bool platformShutdownHookAdd(PlatformShutdownFnT fn);
-void platformShutdownHookRemove(PlatformShutdownFnT fn);
+void platformShutdownHookRemove(PlatformShutdownFnT fn);
+
+#define PLATFORM_SUSPEND_HOOKS 8
+typedef void (*PlatformSuspendFnT)(bool suspending);
+
+bool platformSuspendHookAdd(PlatformSuspendFnT fn);
+void platformSuspendHookRemove(PlatformSuspendFnT fn);
platformIrqSave disables interrupts and returns the previous state for platformIrqRestore; bracket the few instructions that change data an interrupt handler reads (nothing happens on the host).
A shutdown hook runs when DVX ends without its normal shutdown (exit() or a crash with no recovery point), before the timer service and the keyboard hook are given back, so a module that drives hardware from an interrupt (a sound card playing by DMA) can stop it. Each hook runs once. A module removes its hook before it is unloaded; at most PLATFORM_SHUTDOWN_HOOKS exist at once (Add returns false when full; adding one already present does nothing).
+
A suspend hook runs as fn(true) before DVX steps aside for a DOS program (platformDosRun, dvxRunDos) and as fn(false) once DVX is back, so a module that drives hardware itself lets it go (and takes it again then, or at its next use): audio.lib stops every sound, closes the output and the synthesizers, and opens the synthesizers again; music.lib stops every music and lets go of where it plays, closing its context (and the SoundFont kept on the wavetable synthesizer) when none is left; dvxnet stops the packet driver (it starts again at the next use). Suspending runs the newest hook first (a library built on another, loaded after it, lets go before it), resuming the oldest first; a hook may remove itself as they run. At most PLATFORM_SUSPEND_HOOKS; the same rules as the shutdown hooks otherwise.
#define DVX_THEMES_DIR "THEMES"
#define DVX_WPAPER_DIR "WPAPER"
#define DVX_BANKS_DIR "BANKS"
+#define DVX_SAVERS_DIR "SAVERS"
#define DVX_VENDOR_DIR "KPUNCH"
typedef enum {
@@ -6938,6 +7077,7 @@ typedef enum {
DvxKindThemesE,
DvxKindWpaperE,
DvxKindBanksE,
+ DvxKindSaversE,
DvxKindCountE
} DvxKindE;
@@ -6960,12 +7100,12 @@ char **platformKindReadDir(DvxKindE kind);
SYSTEM DVX's own tools and files, and the shipped DVX.INI and ASSOC.INI
LIBS, WIDGETS The libraries and widgets
APPS\<vendor>\<program> A program, its help, and DATA (the files it ships; read-only)
- SHARED FONTS, SOUNDS, THEMES, WPAPER, and BANKS: what DVX and add-on packages ship for every program (an add-on's in a folder of its vendor's, such as FONTS\ACME)
- USER (the user folder) DVX.INI, ASSOC.INI (the user's changes to SYSTEM's), DVX.LOG, the user's own FONTS, SOUNDS, THEMES, WPAPER, and BANKS, KEYS (publishers' keys the user trusts), APPS\<program path> (each program's settings and saved files), and DOWNLOAD
+ SHARED FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS: what DVX and add-on packages ship for every program (an add-on's in a folder of its vendor's, such as FONTS\ACME)
+ USER (the user folder) DVX.INI, ASSOC.INI (the user's changes to SYSTEM's), DVX.LOG, the user's own FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS, KEYS (publishers' keys the user trusts), APPS\<program path> (each program's settings and saved files), and DOWNLOAD
Only packages put files in SHARED: DVX's own, and add-on packages (pkg.lib), each in its vendor's folders. A program reads it but never writes or installs into it; a program's own files live with it (its DATA folder, or resources inside the .app), under its own vendor and program folder, so two programs' files never collide.
platformUserInit, called once by the loader, decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup.
platformAppUserDir is a program's folder in the user folder: APPS\KPUNCH\TERMINAL\TERMINAL.APP gives USER\APPS\KPUNCH\TERMINAL; a program outside the DVX folder gives USER\APPS\<its file name without the extension>. It is not made here (the shell's userDir and BASIC's App.User are made on first use).
-
SHARED and the user folder have the same five kinds of folder (DvxKindE): fonts, sounds, themes, desktop pictures, and MIDI instrument banks. Each may hold folders of add-on vendors (SHARED\FONTS\ACME), one level deep, whose files are named VENDOR\NAME. platformKindName is a kind's folder name; platformKindDir is SHARED's (user false) or the user's (user true); platformKindBrowseDir is where a file dialog for the kind starts (the user's when it exists, else SHARED's). platformKindFind finds a file of a kind by name (NAME or VENDOR\NAME, either separator), the user's first, so a user's file of the same name wins; any other name with a folder in it, or an absolute one, is taken as the path it is (a VENDOR\NAME not in the kind's folders is tried that way too). platformKindNameOf is the other way: a file in one of the kind's folders gives its name alone, or VENDOR\NAME in a vendor's folder, which is what settings store (DVX.INI [sounds], [desktop] wallpaper, and [audio] midiBank), so moving the user folder or DVX keeps them working. platformKindReadDir lists both folders: the user's names (sorted, case ignored), then SHARED's not among them, with each vendor folder's files as VENDOR\NAME (the folders themselves are not listed); free it with dvxReadDirFree. BASIC: FileOpsUserDir$, FileOpsUserPath$, FileOpsUserInTemp, FileOpsKindDir$, FileOpsKindBrowseDir$, FileOpsKindFind$, FileOpsKindName$, and FileOpsKindList (fileops.bas, with FILEOPS_KIND_FONTS to FILEOPS_KIND_BANKS), and App.User for a program's own folder.
+
SHARED and the user folder have the same six kinds of folder (DvxKindE): fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers. Each may hold folders of add-on vendors (SHARED\FONTS\ACME), one level deep, whose files are named VENDOR\NAME. platformKindName is a kind's folder name; platformKindDir is SHARED's (user false) or the user's (user true); platformKindBrowseDir is where a file dialog for the kind starts (the user's when it exists, else SHARED's). platformKindFind finds a file of a kind by name (NAME or VENDOR\NAME, either separator), the user's first, so a user's file of the same name wins; any other name with a folder in it, or an absolute one, is taken as the path it is (a VENDOR\NAME not in the kind's folders is tried that way too). platformKindNameOf is the other way: a file in one of the kind's folders gives its name alone, or VENDOR\NAME in a vendor's folder, which is what settings store (DVX.INI [sounds], [desktop] wallpaper and saver, and [audio] midiBank), so moving the user folder or DVX keeps them working. platformKindReadDir lists both folders: the user's names (sorted, case ignored), then SHARED's not among them, with each vendor folder's files as VENDOR\NAME (the folders themselves are not listed); free it with dvxReadDirFree. BASIC: FileOpsUserDir$, FileOpsUserPath$, FileOpsUserInTemp, FileOpsKindDir$, FileOpsKindBrowseDir$, FileOpsKindFind$, FileOpsKindName$, and FileOpsKindList (fileops.bas, with FILEOPS_KIND_FONTS to FILEOPS_KIND_SAVERS), and App.User for a program's own folder.
#define PLATFORM_LOG_MEM_MAX 16384
@@ -7083,7 +7223,7 @@ audioClose(audio); // or let the program end
volume 0 .. AUDIO_VOLUME_MAX (256)
pan AUDIO_PAN_LEFT (-256) .. AUDIO_PAN_RIGHT (256); centre plays at full volume on both sides and panning turns the other side down
pitch AUDIO_PITCH_MIN (64) .. AUDIO_PITCH_MAX (1024), AUDIO_PITCH_NORMAL (256) = recorded speed
-
audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoiceStop, and audioVoiceIsPlaying take the context and the voice id; a voice id of another context, or of a voice that has ended, gives AUDIO_ERR_VOICE. audioStopAll stops every sample and tone of a context.
+
audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoicePause, 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. audioVoicePause(ctx, voice, paused) holds a sample's or a stream's voice where it is (it stays playing, silent, and takes nothing from its stream) or lets it go on; a tone or a note queue gives AUDIO_ERR_ARG. audioStopAll stops every sample and tone of a context.
audioSetEndFn(ctx, fn, userData) asks for a call when a voice of ctx ends, with the reason: AUDIO_END_FINISHED (played out), AUDIO_END_STOPPED (stopped, or its sample or stream went away), or AUDIO_END_DROPPED (the voice limit made room for a louder sound). The call comes from audioPump or the shell's idle time, as the program that owns the context, never from an interrupt. The mixer runs up to one buffer (about 50 ms) ahead of what is heard, so a notification can come that much before the sound has finished playing.
Tones
audioTone(ctx, wave, hz, volume, ms) plays a square (AUDIO_WAVE_SQUARE), triangle (AUDIO_WAVE_TRIANGLE), or noise (AUDIO_WAVE_NOISE) tone of hz (AUDIO_TONE_MIN_HZ 20 to AUDIO_TONE_MAX_HZ 20000) for ms milliseconds, or until stopped when ms is 0. For noise, hz is how fast the noise changes: higher sounds brighter. audioVoiceSetFrequency changes a playing tone. When the computer has an FM chip (and DVX.INI [audio] fmTones is not false), a square or triangle tone plays on a free FM voice (a square as a carrier modulated at twice its pitch, which gives odd harmonics like a square wave; a triangle as a sine) at up to the chip's highest note (about 6,200 Hz); when every FM voice is taken it is mixed like any sound. Its voice id, volume, pan, pitch, and end notification work the same either way. On a device with a DAC tones are mixed like any sound; on the PC speaker in its tones mode the loudest square or triangle tone playing is sounded by the timer chip.
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.
+
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. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.
+
audioDrv.h also declares, for audio.lib and the libraries built on it (music.lib), audioDecode (a WAV or VOC file in memory as 16-bit frames), audioConvert (8- or 16-bit frames as 16-bit ones), and audioWavParse(head, headBytes, fileBytes, &fmt, &dataAt, &dataBytes) (a WAV file's format and where its frames are, from its first bytes: the frames are clamped to the file and whole; AUDIO_ERR_FORMAT for anything but a RIFF WAVE file, AUDIO_ERR_UNSUPPORTED, AUDIO_ERR_CORRUPT).
audioRecOpen(ctx, fmt, source, bufferMs, &rec) records from the card's microphone, line, or CD input (AUDIO_REC_MIC, AUDIO_REC_LINE, AUDIO_REC_CD; AudioInfoT.canRecord, and recSources with bit 1 << source for each the card has) into a buffer of bufferMs (0: AUDIO_REC_MS_DEFAULT, 2 seconds; AUDIO_REC_MS_MIN to AUDIO_REC_MS_MAX). fmt asks for a rate, 8 or 16 bits, and mono or stereo (0: AUDIO_RATE_DEFAULT, AUDIO_REC_BITS_DEFAULT, mono) and comes back as what the card records: its nearest rate, and its own bits and channels when it cannot do the ones asked (a card before the SB Pro records 8-bit mono). The program takes the frames with audioRecRead(rec, pcm, frames) before the buffer fills; audioRecQueued says how many wait, audioRecLost how many were dropped because the buffer was full (whole blocks, so the frames stay in order), audioRecFrames how many were taken, and audioRecPeak the loudest sample since it was last asked, in percent (a level meter). audioRecFile(ctx, path, fmt, source, &rec) records into a WAV file instead: the library writes the frames as they come (audioPump and the shell's idle time) and the file's sizes at audioRecClose; the file starts with an empty header, so one cut short still reads. Everything that can be checked (the arguments, another recording, the card and its input) is checked before the file is made, so a recording that cannot start leaves a file of that name alone. audioRecClose stops and returns AUDIO_ERR_WRITE when the file could not all be written. audioWavHeader(out, fmt, dataBytes) writes the AUDIO_WAV_HEADER_BYTES of a WAV header for programs writing their own files.
+
The Sound Blasters (sb), Windows Sound System boards (wss), and the Gravis UltraSound (gus) record; the DACs on the printer port, the PC speaker, and null do not. Most cards record or play, not both, so recording takes the card: its output closes (as after a silence), voices already playing hold where they are, and a new voice gets AUDIO_ERR_BUSY (the desktop's sounds are skipped) until the recording ends and the held voices play on. One recording at a time for all of DVX (another gets AUDIO_ERR_BUSY). Closing the context (the program's end) and a DOS program run with DVX stepped aside (audioSuspend) end the recording.
FM Synthesizer
int32_t v = audioFmAlloc(audio); // AUDIO_ERR_NO_VOICE: all taken
@@ -7110,19 +7261,34 @@ audioFmFree(audio, v); // key off, back to everyoneAn 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.
+
Wavetable Synthesizer
+
uint32_t addr = 0;
+int32_t v = audioWtAlloc(audio); // AUDIO_ERR_NO_VOICE: all taken
+AudioWtVoiceT note;
+
+audioWtMemAlloc(audio, frames * 2, 16, &addr);
+audioWtUpload(audio, addr, pcm, frames, 16);
+audioWtNoteInit(¬e); // a plain note: no envelope, the filter open
+note.addr = addr;
+note.end = frames;
+note.bits = 16;
+audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_RATE (44,100) frames a second
+
A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.
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.
+
MIDI Input
+
With DVX.INI [audio] midiIn = mpu401 the same port also brings in what a MIDI keyboard or another instrument plays (the port is shared with MIDI music when midi = mpu401 too). One program reads it at a time: audioMidiInOpen(ctx) (AUDIO_ERR_NO_SYNTH when there is no MIDI input, AUDIO_ERR_BUSY when another program reads it), audioMidiInClose(ctx) (closing the context does it too, and a DOS program run with DVX stepped aside closes it). A timer reads the port AUDIO_MIDI_IN_HZ (1,000) times a second into a buffer of AUDIO_MIDI_IN_BYTES with the time each byte came; audioMidiInRead(ctx, &msg) gives the next whole message, oldest first (false when none waits): AudioMidiMsgT has its status (a channel message 80h..EFh, the kind and the channel, or system common F1h..F3h), data1, data2, length (1 to 3 bytes), and ms, when its first byte came in milliseconds since audioMidiInOpen. Running status is filled in; clock and active-sensing bytes are dropped and system exclusive messages skipped. audioMidiInClock(ctx) is the time now on that clock, and audioMidiInLost(ctx) the bytes dropped because the buffer was full (the program read too late). audioMidiGetInfo says whether there is MIDI input (inPresent) and whether a program reads it (inOwned). music.lib plays what comes in (musicMidiSend) and records it into MIDI files (musicMidiRecordNew); the Sound Recorder does both.
Volume and the Voice Limit
audioSetMasterVolume sets the volume of all sound (DVX.INI [audio] volume, in percent, sets it when the device starts); audioSetVolume sets the volume of everything one context plays. At most the voice limit (DVX.INI [audio] voices, 1 to AUDIO_VOICES_MAX, default 16) plays at once. Starting another sample or tone then stops the quietest one playing (it ends with AUDIO_END_DROPPED); when the new sound would itself be the quietest it is not started (AUDIO_ERR_NO_VOICE). Streams are never dropped.
Files
WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to 48000 Hz; WAVE_FORMAT_EXTENSIBLE with the PCM sub-format is accepted, other chunks are skipped, and a data chunk that claims more bytes than the file holds plays what is there. VOC (Creative Voice File, header checksum checked): sound blocks with 8-bit unsigned samples, continuation, silence, extended (rate and stereo for the next sound block), and new-format blocks with 8-bit or 16-bit PCM; markers and text are skipped, and a repeated section plays once. Compressed sound (ADPCM, 24-bit, floating point) gives AUDIO_ERR_UNSUPPORTED, a damaged file AUDIO_ERR_CORRUPT, a file that is neither AUDIO_ERR_FORMAT.
The Mixer
-
All sound is mixed in 16-bit integers at the rate the device achieved (the mixer never assumes the rate it asked for: a Sound Blaster Pro asked for 22,050 Hz plays at 22,222 Hz), and every source is resampled to it with linear interpolation. Gains combine volume, pan, context volume, and master volume; the sum is clipped to 16 bits. The mixer runs from the sound card's own interrupt (the Sound Blaster, at the end of each half buffer of about 23 ms), or from the timer service for devices without one, so sound keeps playing however busy the programs are. It uses no floating point and touches only memory pinned for interrupts (sample and stream buffers are pinned when they are made).
+
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 (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). A 486's multiplies are slow, so the mixer does none whose result is known: a sound at the device's rate is copied frame for frame without interpolating, a mono one is worked out once for both sides, and a centred mono one scaled once. Measured on an emulated 486DX2-66 at 22,050 Hz, eight mono sounds at the device's rate take about 19 percent of the CPU, eight at half of it (interpolated) about 30 percent.
Settings
DVX.INI section [audio]:
Key Meaning
--- -------
- driver auto (default: find the hardware), or a driver name: sb, speaker, null
+ driver auto (default: find the hardware), or a driver name: wss, sb, gus, lpt, 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
@@ -7131,11 +7297,19 @@ audioFmFree(audio, v); // key off, back to everyone
-
A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, volume) writes all three (an empty driver stores auto, a midi index out of range stores auto). audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.
+
A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingMidiIn(prefs) (an index in audioMidiInChoices, none when missing or unknown), audioSettingModules(prefs) (an index in audioModulesChoices, wavetable when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, modules, volume) writes all four (an empty driver stores auto, a midi index out of range stores auto, a modules index out of range wavetable); audioSettingMidiInStore(prefs, midiIn) writes midiIn (an index out of range stores none), which takes effect when the device next starts. audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h); audioMidiInChoices[AUDIO_MIDI_IN_CHOICE_COUNT] the midiIn choices, audioModulesChoices[AUDIO_MODULES_CHOICE_COUNT] the modules choices (wavetable: on a wavetable synthesizer's voices when it is free and the module fits there; software: always mixed in software). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.
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, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; 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). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS.
Key Event
@@ -7152,20 +7326,25 @@ audioFmFree(audio, v); // key off, back to everyoneDrivers
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.
+ 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). It records with the same modes' input commands (sbRecordPlan): before the Pro the microphone only, 8-bit mono up to 13,000 Hz (15,000 on DSP 2.01, high-speed above 13,000); the Pro and the 16 also the line and CD inputs (mixer 0Ch; 3Dh / 3Eh with the microphone at full on the 16), recording what they play; the mixer registers it changes are put back when it stops. 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.
+ wss A Windows Sound System board (an AD1848 codec or its kin: Microsoft's, Crystal CS4231 / CS4248, Aztech, OPTi in WSS mode) at wssPort: 8- or 16-bit, mono or stereo, the codec's nearest rate, double-buffered by auto-init DMA on wssIrq and wssDma (set through the board's configuration register; a Plug and Play board set up by its own utility must be on the ones DVX.INI names). It records the same way (the codec's capture on the same DMA channel): the line input, AUX1 (the CD on WSS boards), or the microphone with its 20 dB boost, putting back the inputs it found. Tried before the Sound Blaster, which such a board often also has.
+ gus A Gravis UltraSound, found through the ULTRASND variable (port, DMA channels, IRQs: SET ULTRASND=220,1,1,11,5): digital sound on two of its voices from a ring at the top of its memory, refilled by DMA at each half, and its wavetable synthesizer on the rest (gusVoices active in all). It records from the microphone or the line input on ULTRASND's second DMA channel (the first when the second is not given): an UltraSound MAX (board version 10 or 11) through its CS4231 codec, as a Windows Sound System board records (8- or 16-bit, mono or stereo, the codec's rates), any other board with the GF1's own sampling (8-bit, mono or stereo, 5,510 to 44,100 Hz, each half started from the terminal count of the one before, since the GF1 does not run on by itself); no emulator DVX is tested on records on a GUS, so this follows Linux's drivers. Tried after a Sound Blaster beside it, which then plays the digital sound while the GUS plays music (guswt).
+ lpt A DAC on the parallel port (lptDac, lptPort): a Covox Speech Thing (used only when lptDac = covox: it cannot be found), a Disney Sound Source (a FIFO at a fixed 7 kHz, topped up by the timer), or a Stereo-on-1. 8-bit at most 11,048 Hz; the Covox and the Stereo-on-1 take a timer interrupt for every frame.
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.
+ opl (synthesizer) The OPL2 or OPL3 FM chip of an AdLib or Sound Blaster at 388h (oplPort), found by the AdLib timer test (timer 1 must overflow at its fastest count and not at its slowest, which also tells the AdLib-compatible timers of a Gravis UltraSound, which has no FM, from a chip); 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.
+ awe (synthesizer) The EMU8000 of a Sound Blaster AWE32 or AWE64 (BLASTER's E, else A + 400h), set up as Creative's programmer's guide gives: 30 voices (two carry the FM chip's sound), its memory in 512 KB steps, envelopes, LFOs, filter, reverb and chorus in hardware, and its 1 MB General MIDI sample ROM.
+ gus, guswt (synthesizer) The Gravis UltraSound's voices playing samples from its memory, with the library running the envelopes and LFOs: gus.drv's own when it plays the digital sound too, else guswt.drv (the GUS beside another sound device, all its voices for music).
+ mpu401 (synthesizer) An MPU-401 MIDI port in UART mode at mpuPort, only with midi or midiIn = 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 waiting then is kept for get, the MIDI input's timer (64 bytes; more is dropped while nobody reads it). Closing sends All Notes Off on every channel and resets the port.
audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen; AUDIO_DRIVERS_MAX is the most DVX's own callers ask for).
-
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.
+
Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Windows Sound System, Sound Blaster, Gravis UltraSound, parallel port DAC, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: the sound device's own first (the GUS's), then of each kind (FM chip, MIDI port, wavetable) the first that finds its hardware, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.
Writing a Driver
-
A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
-
A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM or AUDIO_SYNTH_MIDI, .open = ..., .close = ..., .write = ... or .put = ...). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready. Both run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver.
+
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). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). 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, AUDIO_SYNTH_MIDI, or AUDIO_SYNTH_WAVETABLE, .open = ..., .close = ..., then .write, .put, or the wavetable functions). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo; for a wavetable synthesizer its voices, memory, AUDIO_WT_* flags, and level, 0 when it is as loud as the digital sound or not known) 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; a MIDI driver whose port receives also has get(&byte), the next byte the port has (false when none), which the MIDI input's timer calls (mpu401.drv keeps what put finds waiting for get). A wavetable driver has upload(addr, frames, count, bits) (program time; addresses as audioWtMemAlloc gives them) and start, update, release, stop, and active for its voices (numbered from 0, as audio.lib hands them out); it plays a note as AudioWtVoiceT gives it, in its own units. write, put, and the voice functions run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver. A sound driver whose card also has a synthesizer (the GUS) points AudioDrvInfoT.synth at its own AudioSynthDriverT, which is opened before the others.
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.
-
For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs), and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
-
The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
+
For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundMidiInCount and SoundMidiInLabel$(index) (audioMidiInChoices); SoundModulesCount and SoundModulesLabel$(index) (audioModulesChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingMidiIn(prefs), SoundSettingModules(prefs), SoundSettingVolume(prefs), SoundSettingsStore(prefs, driver, midi, modules, volume), and SoundSettingMidiInStore(prefs, midiIn) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
+
The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$. Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamFlush (audioStreamFlush), SoundStreamClose. Recording, one per program (audioRecFile): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordStop (audioRecClose; True when the file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources". FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory", "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc, WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (a plain note: no envelope or LFO, the filter open; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree. The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).
@@ -7217,7 +7396,7 @@ audioFmFree(audio, v); // key off, back to everyoneThe project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl), or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet, and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText, the fonts the program carries ([Fonts]: prjAddFont, prjRemoveFont, prjFontCount, prjFontPath; the build makes them FONTn font resources, basBuildSpecT fonts), and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjFontCount, ProjFont$, ProjAddFont, ProjRemoveFont, ProjGet$, and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.
A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End, or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$, and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).
Running a Program (basRunHost.h, in basrt.lib)
-
basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
+
basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, and whether it is a standalone program (the stub's own: the runtime owns the module, frees it with itself, and is ended through the shell's kill hook, shellRegisterAppKill, when the program is ended by force; the IDE and basdev end their runtimes themselves). basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.
From BASIC
compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules, and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue, or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.
@@ -8039,6 +8218,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -8117,7 +8297,8 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
shutdownFn void (*)(void) Pointer to appShutdown (may be NULL).
dxeCtx DxeAppContextT * Heap-allocated context (address stable across realloc).
taskCodeDepth int32_t * Per task id: SHELL_APP_CALL calls into this app still open on that task's stack.
- killPending bool A force kill was deferred; shellReapApps finishes it.
+ killPending bool A force kill was deferred; shellReapApps finishes it.
+ faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill).
App Lifecycle API
@@ -8141,7 +8322,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Gracefully shut down a single app. Calls the app's shutdownFn (if present), destroys all windows belonging to the app, kills its task (if any), releases any widget classes and system-wide hotkeys the app registered and did not unregister (wgtUnregisterAppClasses, dvxUnregisterAppHotkeys), closes the DXE handle, and frees the context. The slot returns to AppStateFreeE.
Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery. The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.
+
Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.
Cleanup order: callbacks and windows first (removes them from the compositor), then the task (frees its stack; any dispatch frames still open on it are dropped from the context's dispatch depth), then the widget classes and system-wide hotkeys the app registered (its shutdownFn never got the chance to unregister them), then the DXE handle (unmaps the code). Closing the DXE before destroying windows would cause callbacks into unmapped code.
If the app's code may still be running underneath the call -- End Task clicked in a dialog the app has open, a kill issued from the app's own task, or from inside a shell call into the app -- the unload is deferred: the app's windows are hidden and queued for destruction, the slot is marked Terminating, and the shell finishes the kill (still without shutdownFn) at the next safe point of its main loop, once that code has returned. Event-handler frames on the app's own task do not hold the kill back, since they end with the task: an app stuck in an endless event handler is killed at once (or at the next main-loop pass).
Register a function the shell calls with an app's id whenever that app ends -- normally, force-killed, or after a crash -- once its windows are gone, next to the shell's own release of the app's widget classes and hotkeys. A library uses it to release what the app left open with it: the networking library closes the app's sockets this way. For library code that stays loaded; registering the same function again has no effect, and unregistering one that is not registered does nothing.
Register a function the shell calls with an app's id when the app is ended by force (End Task, a screen saver that would not close, DVX exiting) while none of the app's code is running and its windows are still there -- before the windows go and before the cleanup hooks. A library ends what it runs for the app the way the app's own end would: the BASIC runtime (basrt) frees a standalone program's VM, forms, screen, and module this way, closing its serial ports and its forms' windows, so End Task on a BASIC program leaks nothing. Not called for an app that faulted (its state cannot be trusted), nor for one whose kill had to wait until its code returned (End Task from inside the app's own dialog: its windows went at once). Registration rules as for cleanup hooks.
shellDesktopUpdate
void shellDesktopUpdate(void);
Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh. Each call also adds one to the change counter (shellDesktopChangeCount).
One way for every program to start another (Program Manager's items and Run..., the Task Manager's Run..., File Manager, setup programs). A command line is a program, in quotes if it contains blanks, followed by its arguments; it is split with dvxNextArg, the same rule a program uses to read its own arguments. A document in place of a program opens in the program associated with its file type (see File Associations).
Start program with args. A .app is loaded with shellLoadAppWithArgs, its arguments first passed through shellResolveArgs(folder, ...). Anything else is a document: it is looked for in folder first (when relative), must exist, and is opened with shellAssocCommand's program and arguments. Problems are shown to the user in an error box (a missing document, a document type with no association; load errors come from shellLoadApp as usual). Returns the new application id, or -1.
+
Start program with args. A .app is loaded with shellLoadAppWithArgs, its arguments first passed through shellResolveArgs(folder, ...). A DOS program (shellIsDosProgram: .EXE, .COM, or .BAT, any case) is set to run with shellRunDos (folder is where it runs; looked for in folder first when relative). Anything else is a document: it is looked for in folder first (when relative), must exist, and is opened with shellAssocCommand's program and arguments; when that program is a DOS program, the document goes to it by its full short name (it runs in its own folder) through shellRunDos. Problems are shown to the user in an error box (a missing document, a document type with no association; load errors come from shellLoadApp as usual). Returns the new application id, SHELL_RUN_DOS (0) for a DOS program set to run, or -1.
Run a DOS program with DVX stepped aside (libdvx's dvxRunDos, which takes the same program, args, folder, and pause). shellRunDos only records the request (a newer one replaces one still waiting): closing the other programs pumps the event loop, which is not safe from an event handler. The main loop calls shellRunDosRequest after shellRunExitRequest; keepAppId is the desktop, which stays. A program named with a folder must exist ("Cannot find X.", titled Run DOS Program); a bare name is looked for on the PATH by DOS. When programs other than the desktop are running, a question names them (by their program names, shellProgramInfo) and says each asks about unsaved work as it closes; No, or a program that keeps a window (shellCloseAllApps), cancels the run. A program that cannot be started is reported after DVX is back ("DVX could not run X." with the reason). The Task Manager is the shell's own window and is not asked. Program Manager's DOS items call shellRunDos with their "Wait for a key" setting (PmItemT.pause, PROGMAN.INI itemN.pause); shellRunProgram uses pause false.
+
SHELL_DOS_FILTER ("DOS programs (*.exe;*.com;*.bat)") is the file dialogs' filter for them (Run..., Program Manager's Properties, Control Panel's File Types). A Program Manager item for a DOS program with no icon file of its own shows SHELL_DOS_ICON (SYSTEM\DOSPROG.BMP, from config/DOSPROG.BMP).
Run Minimized. shellMinimizeApp minimizes every visible window of appId (an id from shellRunProgram) at once and again on each of the next SHELL_MINIMIZE_FRAMES (60) idle frames, because a program with a main loop opens its windows from its own task after it has started. Asking again for the same program starts its frames over; nothing happens for an id that is not running, and a program that ends is forgotten (an app-cleanup hook). shellMinimizeIdle is the per-frame step, called by the shell's idle dispatch. Program Manager's Run Minimized items and BASIC's ShellMinimizeApp use it.
+
+
The Screen Saver
+
Header: dvxshell/shellSaver.h
+
A screen saver is a program (.app, C or BASIC) in a SAVERS folder (DvxKindSaversE: SHARED\SAVERS, which DVX and add-ons fill, and the user's SAVERS); Program Manager does not list it. After the wait with no key or mouse input (dvxIdleMs) the shell starts the chosen one with SHELL_SAVER_ARGS ("/S"). It takes the whole screen (dvxExclusiveBegin; BASIC SCREEN and FULLSCREEN ON) and draws until its window is closed; it needs no input handling of its own, since the input that wakes the screen goes to it (full-screen mode), never to the program underneath. Input SHELL_SAVER_GRACE_MS (1,000 ms) or more after it started asks each of its windows to close (dvxRequestWindowClose, as Alt+F4 does); one still running SHELL_SAVER_END_MS (3,000 ms) later is ended (shellForceKillApp). The log says "Shell: screen saver X started" and "... ended".
+
The saver does not start while a dialog box is open (ctx->modalWindow) or a program has the full screen (ctx->exclusive.owner: a game played with a joystick gives no key or mouse input). After a saver ends (or fails to start: missing, or not a program, logged and never shown in a message box) the next wait counts from then.
+
DVX.INI [desktop] saver is the saver's name in SAVERS ("" or missing: none) and saverWait the minutes (SHELL_SAVER_WAIT_MIN to SHELL_SAVER_WAIT_MAX, 1 to 60; SHELL_SAVER_WAIT_DEFAULT, 10, when missing). The shell reads them at start; Control Panel's Desktop page sets them live and stores them with OK.
Function What it does
+ -------- ------------
+ shellSaverSet Choose the saver (a file in SAVERS is kept by its name there, platformKindNameOf; any other path as it is; "" or NULL: none) and the wait, clamped to the range; at once
+ shellSaverName / Wait The settings in force
+ shellSaverStart Start the saver now (Control Panel's Test); only recorded, the next shellSaverStep starts it. False with none chosen or one running
+ shellSaverRunning True while a saver runs
+ shellSaverStep The main loop's step (shell top level): starts the saver when due, ends a running one at input
+ shellSaverFromPrefs Read [desktop] saver and saverWait (start, Control Panel's Cancel)
+ shellSaverToPrefs Write them (Control Panel's OK and its Cancel snapshot)
+
BASIC: SysScreenSaver$, SysScreenSaverWait, SysSetScreenSaver, SysScreenSaverStart, SysScreenSaverRunning, SysScreenSaverStore, SysScreenSaverLoad, and SysIdleSeconds (system.bas; see BASIC System Settings).
+
Writing a screen saver
+
A C saver is a main-loop program that opens a window, takes the full screen with dvxExclusiveBegin, and draws (dvxExclusivePresent8, paced with gameFrameWait) until the window's onClose sets a flag; then it destroys the window and returns (src/savers/kpunch/starfld). A BASIC saver is a program with no form: SCREEN 13, FULLSCREEN ON, and a loop drawing with FRAMEWAIT (src/savers/kpunch/blank, marquee, mystify); closing its screen window ends it. Put the .app in SHARED\SAVERS (an add-on package's kind saver) or the user's SAVERS; the name Control Panel lists is the program's (its "name" resource, a BASIC project's Name).
ShellRunProgram is shellRunProgram (a document opens through its association; file names in args, and a relative document, are looked for in folder; "" for none), returning the new app id or 0. ShellRun is ShellRunProgram without a folder, True when it started; the two are one launch path. ShellRunCommand is shellRunCommand (id or 0). ShellRunDialog splits history at line ends (CHR$(10), a CR before it is dropped, empty lines skipped) into the list shellRunDialog shows, and returns the command chosen or "" when cancelled. ShellMinimizeApp is shellMinimizeApp. ShellOpenDocument is shellAssocOpenDocument (it shows nothing when there is no association; ShellRunProgram does).
+
ShellRunProgram is shellRunProgram (a document opens through its association; file names in args, and a relative document, are looked for in folder; "" for none), returning the new app id, -1 (shell.bas SHELL_RUN_DOS) for a DOS program set to run, or 0. ShellRun is ShellRunProgram without a folder, True when it started or a DOS program will run; the two are one launch path. ShellRunCommand is shellRunCommand (id, -1, or 0). ShellRunDos is shellRunDos (pause True or False): the DOS program runs once the calling procedure has returned and the programs besides the desktop, the caller too, have closed. ShellRunDialog splits history at line ends (CHR$(10), a CR before it is dropped, empty lines skipped) into the list shellRunDialog shows, and returns the command chosen or "" when cancelled. ShellMinimizeApp is shellMinimizeApp. ShellOpenDocument is shellAssocOpenDocument (it shows nothing when there is no association; ShellRunProgram does).
ShellProgramScan is shellProgramScan of folder ("" for SHELL_PROG_DIR) without skipPath ("" for none), paths only (readInfo false), so it never opens a program; it returns a list handle, 0 when nothing was found. ShellProgramCount and ShellProgramPath read it (0 / "" for a bad list or index); ShellProgramFree frees it. ShellProgramName and ShellProgramDescription are shellProgramInfo's name and description, read one program at a time so a long list can be filled with DoEvents in between (the C progress callback). ShellIsProgram is shellIsProgram.
ShellProgramScan is shellProgramScan of folder ("" for SHELL_PROG_DIR) without skipPath ("" for none), paths only (readInfo false), so it never opens a program; it returns a list handle, 0 when nothing was found. ShellProgramCount and ShellProgramPath read it (0 / "" for a bad list or index); ShellProgramFree frees it. ShellProgramName and ShellProgramDescription are shellProgramInfo's name and description, read one program at a time so a long list can be filled with DoEvents in between (the C progress callback). ShellIsProgram is shellIsProgram, ShellIsDosProgram shellIsDosProgram.
What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper, text smoothing, and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).
+
What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper, the screen saver, text smoothing, and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).
SysSetWallpaperMode dvxSetWallpaperMode with the value of prefsWallpaperChoices[mode]; other modes are ignored
SysWallpaper / Mode ctx->wallpaperPath; the prefsWallpaperChoices index of ctx->wallpaperMode
SysWallpaperStore / Load dvxWallpaperToPrefs / dvxWallpaperFromPrefs (Load: False when the image could not be loaded)
+ SysScreenSaver / Wait shellSaverName / shellSaverWait
+ SysSetScreenSaver shellSaverSet
+ SysScreenSaverStart shellSaverStart (True when it will start)
+ SysScreenSaverRunning shellSaverRunning
+ SysScreenSaverStore / Load shellSaverToPrefs / shellSaverFromPrefs
+ SysIdleSeconds dvxIdleMs / 1000
SysSetTextSmoothing dvxSetFontSmoothing (nonzero is on)
SysTextSmoothing ctx->fontSmoothing (the setting; text is smoothed only above 8 bits per pixel)
SysFontsStore / Load dvxFontsToPrefs / dvxFontsFromPrefs
@@ -9179,7 +9413,7 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
Sound, Music, and MIDI
-
Games play sound on whatever the computer has: a Sound Blaster family card, an FM synthesizer chip, a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.
+
Games play sound on whatever the computer has: a Sound Blaster family card, a Windows Sound System card, a Gravis UltraSound, a DAC on the printer port, an FM synthesizer chip, a wavetable synthesizer (a Gravis UltraSound's or a Sound Blaster AWE's), a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.
Sound Effects and Tones (audio.h)
audioOpen() gives the program an audio context (the first one opened starts the sound device).
audioSetVolume sets the program's volume; the master volume belongs to the user (Control Panel).
Music (music.h)
-
musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position, and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
+
musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position, and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else a wavetable card's recorded instruments (a Gravis UltraSound or a Sound Blaster AWE with a SoundFont), else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on a wavetable card's own voices (taking little of the CPU), else on any card that plays sampled sound (mixed in software). musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
The FM Chip and the MIDI Port
For music drivers of its own, a game can allocate two-operator FM voices shared with the other programs (audioFmAlloc, audioFmWrite with the AUDIO_FM_* register numbers), take the whole FM chip for itself (audioFmRawAcquire; refused with AUDIO_ERR_BUSY while another program has it), or send bytes to the MIDI port (audioMidiAcquire, audioMidiPut). Give them back as soon as the game does not need them.
Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
-
Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank is bin/libs/kpunch/music/genmidi.op2.
+
Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), WAV and VOC sound files, and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
+
Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).
musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state), and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
-
Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time.
+
musicLoad and musicLoadMem tell the kind by the contents: a WAV or VOC sound file (MUSIC_KIND_SOUND) or a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. A WAV loaded by its path is read from the disk as it plays (only its header is read at the load), so a long recording needs no memory; a VOC, or a WAV given in memory, is decoded into memory (audioDecode). Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_UNSUPPORTED (a sound file that is compressed or not 8- or 16-bit; its text is audio.lib's), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state; the title is a module's own, a MIDI file's sequence name (the first track's name, control characters as spaces), and none for a sound), and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
+
musicLoadMacro(macro, &music) makes a MIDI music from a music macro string (the language PLAY takes, below): its notes play on the General MIDI instruments its I commands name, the piano for notes before any I or after I0, and it is then played, paused, and freed like a MIDI file (MUSIC_ERR_MACRO for a mistake in the string). musicSetBank(music, bank) plays one MIDI music on another instrument bank than DVX.INI's choice: bank names it as midiBank and soundFont do ("" the standard OP2 bank, NULL DVX.INI's choices again). A SoundFont (.SF2 or .SBK) or an UltraSound's ULTRASND.INI is for wavetable music (the FM bank stays DVX.INI's), any other bank for the FM synthesizers. The bank is read at once, so a missing or damaged one is MUSIC_ERR_BANK there and then; it must come before the music's first musicPlay (MUSIC_ERR_ARG for a module, live MIDI, or a music that has played). Control Panel's Instruments Test is musicLoadMacro and musicSetBank.
+
Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time; a sound goes to the frame.
Where Music Plays
-
Modules are rendered by libxmp-lite into a stream of the sound device (16-bit stereo at the device's rate, 400 ms of buffer, linear interpolation), so they play on any device that plays sampled sound.
+
A module plays on the wavetable synthesizer's own voices (a Gravis UltraSound's, an AWE32's or AWE64's) when there is one and no other music plays on it (one music at a time, MIDI or a module; a stopped one gives it up): its samples go into the chip's memory and libxmp's voices are played on the chip's, nothing mixed in software (MUSIC_OUT_WAVE; see Modules). Otherwise libxmp-lite renders it into a stream of the sound device (16-bit stereo at the device's rate, 400 ms of buffer, linear interpolation), so it plays on any device that plays sampled sound (MUSIC_OUT_STREAM): with DVX.INI [audio] modules = software (audioModulesChoices; the Control Panel's Sounds page), when the module has more channels than the chip has voices or a stereo sample, or when its samples do not fit in the chip's memory (the SoundFont kept there for MIDI gives way first).
+
A sound always plays on a stream (MUSIC_OUT_STREAM) at its own rate and channels, 16-bit, with the same 400 ms of buffer; the mixer converts it to the device's rate. A pause, a seek, or a stop of any music on a stream is heard at once: the stream's voice is held where it is (audioVoicePause) and, for a seek or a stop, what it holds is dropped (audioStreamFlush).
MIDI goes to the best synthesizer there is, decided at the first musicPlay (or musicMidiOpen), with DVX.INI [audio] midi:
midi MIDI plays on
---- -------------
- auto the FM chip (OPL2 / OPL3) when there is one and no program has taken the whole chip; else the software synthesizer
+ auto a wavetable synthesizer (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64) when there is one, no other music is using it, and its SoundFont loads; else the FM chip (OPL2 / OPL3) when there is one and no program has taken the whole chip; else the software synthesizer
mpu401 the MPU-401 port (mpuPort, default 330h) when it was found and no other music is using it; else as auto
- fm as auto
- software the software synthesizer
+ fm the FM chip when there is one and it is free; else the software synthesizer
+ software the software synthesizer
+ softwave the SoundFont software synthesizer (below); else the software synthesizer
On the FM chip and the MPU-401 port the song is played by the timer service (500 times a second), so it keeps time however busy the programs are. On the FM chip every note takes a voice from audio.lib's shared pool; when none is free the note plays on the software synthesizer instead, which then runs beside the chip (its stream is always open for that). The software synthesizer runs in the stream's fill function (the shell's idle time), 64 frames between event checks, 150 ms ahead of what is heard; a program that keeps the desktop busy every frame should call audioPump once a frame, as for any stream.
General MIDI on FM
-
Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. Control Panel's Sounds page lists the banks to choose from. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). The bank is read once, at the first MIDI music that needs it, so a new choice takes effect when DVX next starts.
+
Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. Control Panel's Sounds page lists the banks to choose from; DVX's own add-ons offer DMXOPL and DMXOPL3 (MIT, by ConSiGno and Sneakernets) through Software. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). DVX.INI's choice is read as each MIDI music starts, so a new choice reaches the next one; a music keeps the bank it started with, and a bank is freed with the last music playing on it.
What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both, or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.
The Software Synthesizer
When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo, and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time, and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.
+
SoundFonts on a Wavetable Synthesizer
+
A wavetable synthesizer (audio.lib's gus and guswt drivers for a Gravis UltraSound, awe for the EMU8000 of a Sound Blaster AWE32 or AWE64) plays recorded instruments from its own memory. Its instruments come from a SoundFont, DVX.INI [audio] soundFont (MUSIC_INI_SOUND_FONT), named as midiBank names an OP2 bank; not set, the standard one (MUSIC_SOUND_FONT_DEFAULT, KPUNCH\DVXGM.SF2: DVX's General MIDI SoundFont add-on, made from FluidR3_GM by mksf, about 3.4 MB). SoundFont 2 files are read in full; SoundFont 1 (.SBK, the AWE32's own) as far as it is known (it has no public specification). soundFont may also name a Gravis UltraSound's ULTRASND.INI by its path, which plays that folder's .PAT patches as instruments; and where the standard SoundFont is not installed, the patches in the folder ULTRADIR names play (musicPatchSetPath), else the AWE's own bank for its sample ROM (musicRomBankPath: SFBANK\SYNTHGM.SF2 or SYNTHGM.SBK in the folder SOUND names, as Creative's drivers installed it). Control Panel's Sounds page lists all of these.
+
A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its SoundFont and samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC). Live MIDI loads a program's samples as it is chosen. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.
+
The AWE runs the SoundFont's envelopes, LFOs, filter, and reverb and chorus sends itself; on the GUS the library runs the envelopes and LFOs (its volume and pitch changed as the note goes on), without a filter or effects.
+
The SoundFont Software Synthesizer
+
With DVX.INI [audio] midi = softwave, MIDI plays from the SoundFont in software on any device that plays sampled sound: the song's samples are read into memory at their own rates and mixed into the music's stream (linear interpolation, the envelopes and LFOs as on the GUS, no filter or effects), with the software FM synthesizer beside it for the notes the bank lacks. It needs the memory for the song's samples (a General MIDI song of ten instruments and a drum kit about 360 KB of DVXGM's) and more CPU time than FM.
The MPU-401 Port
With DVX.INI [audio] midi = mpu401, MIDI goes out of the MPU-401 port (audio.lib's mpu401 driver) to an external synthesizer, one music at a time (a second MIDI music meanwhile plays on the FM chip or the software synthesizer). Bytes are queued and sent by the timer as fast as the port takes them, with a 40 ms pause after every system exclusive message (first-generation MT-32s lose data without it). The music's volume scales every channel volume (CC7) sent. DVX.INI [audio] mpuMode says what is on the port:
mpuMode Treatment
@@ -10099,9 +10342,25 @@ musicMidiOpen(&keys); // plays at once
musicMidiProgram(keys, 0, 73); // flute on channel 0
musicMidiNoteOn(keys, 0, 60, 100); // middle C
musicMidiNoteOff(keys, 0, 60);
-
musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl, and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.
+
musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl, and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range. musicMidiSend(music, status, data1, data2) sends any channel message as MIDI input brings it (audioMidiInRead: status 80h..EFh with the channel in it; a data byte the message does not use is ignored), so what a keyboard plays sounds as it comes (MIDI thru).
A MIDI recording keeps channel messages with their times (musicMidiRecordAdd(rec, ms, status, data1, data2), ms from the recording's start; a time before the last message's counts as the last's; MUSIC_ERR_ARG for anything but a channel message) and musicMidiRecordSave(rec, path) writes them as a Standard MIDI File: type 0, one track of 1,000 ticks a quarter note at 60 quarter notes a minute, so a tick is a millisecond. A note still held at the end is let go there, so the file never leaves a note sounding. MUSIC_ERR_WRITE when the file cannot be written (nothing is left of one not all written). musicMidiRecordCount and musicMidiRecordLength (the last message's time) say what it holds; musicMidiRecordFree frees it, and a program's recordings are freed when it ends. musicLoad plays the file. The music macros (below) are written into MIDI files by the same code.
Modules
-
MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.
+
MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite with DVX's patches, listed in its DVX-VERSION.md; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so it plays modules in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. On a stream libxmp mixes every channel in software (linear interpolation, three multiplies a channel a frame).
+
On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.
+
Measured on an emulated 486DX2-66 at 22,050 Hz, a 4-channel MOD with all four channels sounding takes about 49 percent of the CPU mixed in software and about 14 percent on a Gravis UltraSound's voices, most of that libxmp's own work each tick (about 1.3 ms of the 20).
A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
Several Programs at Once
-
Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music. When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.
+
Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music, and so does the wavetable synthesizer (MIDI or a module). When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.
BASIC
-
src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it. MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease; see the sound library).
+
src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.
The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
Licences
libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
The DVX General MIDI SoundFont add-on (DVXGM.SF2) is made from FluidR3_GM by Frank Wen (MIT licence); its notice is installed beside it (SHARED\BANKS\KPUNCH\DVXGM.TXT).
The GENMIDI instrument bank is from Freedoom 0.13.0, Copyright (C) 2001-2024 Contributors to the Freedoom project. All rights reserved. BSD 3-clause licence:
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Freedoom project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -10170,7 +10430,7 @@ pkgNetFree(n);
Add-on packages: the .DPK file format, manifests, Ed25519 keys and signatures, the packages installed in a DVX folder, the keys DVX trusts, planning what an install or removal takes, and installing and removing a package a step at a time. Software, Package Builder, the IDE's File > Make Package, DVXPKG.EXE, the build machine's dvxpkg, and Setup all use it.
Header: pkg/pkg.h (it includes the others below)
Loaded as: bin/libs/kpunch/pkg/pkg.lib
-
BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys).
+
BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys; PkgFileOwner$ is pkgDbOwner).
A Package
A package is NAME.DPK: a ZIP read front to back, whose first entry is PACKAGE.INI, the manifest; then PACKAGE.SIG when the publisher signed it; then the files, named relative to the DVX folder with / between parts.
[package]
@@ -10178,7 +10438,7 @@ format = 1
id = acme.chess ; vendor.name, each 1..8 of a-z 0-9 _
name = Chess
version = 1.2.0
-kind = app ; app library widget font bank theme wallpaper sound help bundle system
+kind = app ; app library widget font bank theme wallpaper sound saver help bundle system
summary = Play chess against the computer.
publisher = Acme Games ; also author, licence, homepage
dvx = 1.0.0 ; the oldest DVX it runs on
@@ -10195,6 +10455,7 @@ APPS/ACME/CHESS/CHESS.APP = 61234 <SHA-256, 64 hex digits>
font, bank SHARED/FONTS/VENDOR, SHARED/BANKS/VENDOR
theme, wallpaper SHARED/THEMES/VENDOR, SHARED/WPAPER/VENDOR
sound SHARED/SOUNDS/VENDOR
+ saver SHARED/SAVERS/VENDOR (a screen saver: a program, so it needs abi)
bundle no files: a set of packages, its dependencies
system DVX itself (kpunch.dvx): anywhere but the user folder
Manifests (pkgManifest.h)
@@ -10262,7 +10523,7 @@ dvxpkg list [-r DVXDIR]
Header: print/print.h
Loaded as: bin/libs/kpunch/print/print.lib
Settings
-
The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1, LPT2, LPT3, or a file), the printer model (PrintModelE: text only, Epson 9-pin, Epson 24-pin, HP LaserJet), and the page setup (PrintPageSetupT).
+
The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1 to LPT3, COM1 to COM4, or a file), a serial port's speed and handshake (baud, PrintHandshakeE), the printer model (PrintModelE: text only; Epson 9-pin or 24-pin, each with a black or a color ribbon; HP LaserJet at 150 dpi; HP LaserJet III or DeskJet at 300 dpi; HP DeskJet 500C; HP DeskJet 550C and later; PostScript, black or color; Apple ImageWriter II, black or color), and the page setup (PrintPageSetupT).
Function Description
-------- -----------
printSettingsLoad(&s) Read DVX.INI into a PrintSettingsT (defaults for anything missing; a page setup that cannot work is replaced by the defaults as a whole).
@@ -10276,7 +10537,7 @@ dvxpkg list [-r DVXDIR]
printDeviceProblem(dest, file) NULL when the destination can be used, else why not (printing to a file needs its name).
printDeviceStore(h, model, dest, file) Put model, destination, and file into the [printer] section of h with the page setup as it is in DVX.INI now (the caller saves h). The Control Panel's OK and print.bas use it.
Jobs and Printer Problems
-
A job sends bytes to a printer port or into a file. On DOS a port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line, or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.
+
A job sends bytes to a printer port or into a file. On DOS a parallel port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. A serial port (COM1 to COM4, for serial printers such as the ImageWriter II) is opened through the serial library when the job starts, at the speed and handshake in the settings (8 data bits, no parity, 1 stop bit); its interrupt handler sends from a buffer as the printer's handshake allows (XON / XOFF, or the printer's DTR seen on the computer's CTS, or on its DSR as Apple's cable wires it), the port is busy while the buffer is well filled, and when the job closes it waits for the buffer to empty before closing the port. A PrintPortOpsT's open, pending, and close (NULL for none) are these steps. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line, or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.
PrintHooksT hooks = { myAsk, myIdle, NULL, myState };
char err[PRINT_MSG_MAX];
PrintJobT *job = printJobOpen(PrintDestLpt1E, NULL, &hooks, err, sizeof(err));
@@ -10294,17 +10555,92 @@ if (!job) {
finished(ctx, closing) When a write that called idle returns, and (closing true) when the job is freed.
printJobWrite returns once every byte was taken, or false when the job is cancelled. printJobBytes gives the count sent, printJobCancelled whether it was cancelled. printStatusProblem(status, afterWrite, where, out, size) turns BIOS status bits (PRINT_STATUS_*) into the message the user sees. printSetPortOps replaces the port services (present / status / write) with your own, as the tests do to play a printer that is busy or out of paper; NULL restores the real ones. Elsewhere than DOS the real ports always exist, are always ready, and append to lpt1.prn .. lpt3.prn in the working directory.
Text
-
printTextJob(job, model, text, len, &pageSetup, &info) prints plain text on pages. Lines end with LF or CR LF; a form feed in the text starts a new page. Each printed line starts with marginLeft blanks and ends with CR LF, tabs are expanded to tabWidth, other control characters print as blanks, and a line longer than the text width is wrapped at the last blank before it (a word longer than a whole line is split) or, with wrap off, cut off. Every page ends with a form feed. For the Epson and LaserJet models the printer's reset command comes first (and, for the LaserJet, last).
+
printTextJob(job, model, text, len, &pageSetup, &info) prints plain text on pages. Lines end with LF or CR LF; a form feed in the text starts a new page. Each printed line starts with marginLeft blanks and ends with CR LF, tabs are expanded to tabWidth, other control characters print as blanks, and a line longer than the text width is wrapped at the last blank before it (a word longer than a whole line is split) or, with wrap off, cut off. Every page ends with a form feed. For the Epson and LaserJet models the printer's reset command comes first (and, for the LaserJet, last). A PostScript printer prints no plain text, so on the PostScript models the pages are PostScript: Courier as large as lets the page setup's columns fill 8 inches and its lines 10, a show per line (CP437 letters and signs in ISO Latin-1; line and block characters as -, |, +, and #).
A page is marginTop lines with the header in their middle line, the text, then marginBottom lines with the footer in their middle line. Header and footer codes: &f file name (PrintTextInfoT.title), &p page number, &n number of pages, &d date, &t time (PrintTextInfoT.date / time, or today and now), && an ampersand; &l, &c, and &r put what follows at the left, centre, or right (text before any of them is centred). printFormatHeader expands a header for one page.
PrintTextInfoT also chooses a page range (firstPage, lastPage; 0 = all) and the number of copies. printTextPageCount returns the number of pages text takes.
-
Pictures and Printer Drivers
-
printRasterJob(job, driver, rgb, w, h, pitch) prints a picture of R, G, B bytes on one page: scaled to the largest size that fits the printable area (8 by 10 inches) keeping its shape, and dithered to black and white with a 4x4 ordered pattern. The driver gets one band of dot rows at a time, so only one band is ever in memory. printDriver(model) returns the driver of a model (NULL for text only).
-
Model Driver
- ----- ------
- Epson 9-pin (ESC/P) 120 x 72 dots per inch (ESC * 1), 8-row bands, paper fed with ESC J; blank bands only feed paper, and each pass stops after its last black dot.
- Epson 24-pin (ESC/P) 180 x 180 dots per inch (ESC * 39, 3 bytes per column), 24-row bands.
- HP LaserJet (PCL) 150 dots per inch raster graphics, one uncompressed row transfer per dot row (every LaserJet understands it), trailing white bytes left out.
-
A PrintDriverT holds the resolution, the printable size in dots, the band height, the text reset strings and usual page length, and begin / band / end functions that write the printer's commands; a new printer family is one more driver.
+
Documents: Pictures and Drawn Pages
+
A document is pages of dot graphics on a picture-printing model. printDocOpen(job, model) starts one on a job (the document owns the job from then on, and printDocClose closes it). Each page is the whole printable area (8 by 10 inches): printDocPage(doc, fn, ctx) asks the program's PrintBandFnT for it a band at a time, top down, as R, G, B bytes at the printer's resolution (the band comes white), and printDocPicture(doc, rgb, w, h, pitch) prints a picture scaled to the largest size that fits, keeping its shape. Only one band of the page is ever in memory, so a page costs the same however large it is.
+
Function Description
+ -------- -----------
+ printModelPage(model, &info) The page a model prints on (PrintPageInfoT: dots per inch across and down, the printable area in dots, color or not, and vector: a PostScript printer, which can draw the page itself); false for text only. info may be NULL to ask only whether the model prints pictures.
+ printDocOpen(job, model) Start a document; NULL (the job closed) for a text-only model.
+ printDocInfo(doc, &info) The document's page.
+ printDocPage(doc, fn, ctx) Print a page the program draws. fn(ctx, rgb, width, y0, rows) fills rows y0 to y0 + rows - 1; false from it stops the page.
+ printDocPicture(doc, rgb, w, h, pitch) Print a picture as a page.
+ printDocCancelled(doc) True when the job was cancelled.
+ printDocClose(doc) End the document and close its job; true when every page printed. After a failure every later page fails at once.
+ printUiDocOpen(ctx, title, &choice) A document on the printer in the settings, at a Print dialog answer's destination (NULL: the settings'), with the Printing window up until it closes and the Retry box; tells the user why when it cannot open. Copies are the program's: print each page as often as wanted.
+ printUiDocClose(ctx, doc) Close it, telling the user of a failure (not of a cancel).
+
Each band is dithered to the printer's inks with an ordered pattern (4x4, 17 levels, on the dot-matrix printers and the 150 dpi LaserJet; 8x8, 65 levels, at 300 dpi). A black-only printer prints the brightness. A color printer prints cyan, magenta, and yellow from the red, green, and blue; with black ink as well, black takes what the three have in common (full undercolor removal). Each ink is dithered with its own offset of the pattern, so dots of different inks do not land on each other in a regular way, and the pattern follows the row's place on the page, so it runs on from band to band.
+
static bool drawBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows) {
+ // a red bar one inch down, a quarter inch high
+ const PrintPageInfoT *pi = (const PrintPageInfoT *)ctx;
+
+ for (int32_t r = 0; r < rows; r++) {
+ int32_t y = y0 + r;
+
+ if (y >= pi->dpiY && y < pi->dpiY + pi->dpiY / 4) {
+ for (int32_t x = 0; x < width; x++) {
+ uint8_t *p = rgb + ((r * width) + x) * 3;
+
+ p[0] = 0xFF;
+ p[1] = 0;
+ p[2] = 0;
+ }
+ }
+ }
+
+ return true;
+}
+
+PrintDocT *doc = printUiDocOpen(ctx, "Report", NULL);
+PrintPageInfoT pi;
+
+if (doc) {
+ printDocInfo(doc, &pi);
+ printDocPage(doc, drawBand, &pi);
+ printUiDocClose(ctx, doc);
+}
+
Vector Pages
+
On a PostScript printer (PrintPageInfoT.vector) a page can be drawn by the printer itself, as lines, curves, fills, and its own letters, instead of being sent as dots: sharper at any size, and much smaller. printVecBegin(doc) starts such a page and printVecEnd(doc) prints it; in between, the drawing calls below go to the printer as they are made. Coordinates are the page's dots (y down, a dot's centre at whole numbers, so a shape covers the same dots it would in a band), colors are 0xRRGGBB (grey on the black-and-white model), and widths are in dots. printVecBegin is false, and the drawing calls do nothing, on a printer that cannot or a document that failed; a raster page (printDocPage, printDocPicture) is refused while a vector page is open, and printDocClose ends one left open.
+
Function Description
+ -------- -----------
+ printVecBegin(doc), printVecEnd(doc) Start a vector page; print it (false when it did not print).
+ printVecLine(doc, x0, y0, x1, y1, color, width, dash) A line, round at the ends; dash is a 16-step pattern from its top bit (0xFFFF solid), a step a dot along the line's longer side, as BASIC's LINE style.
+ printVecRect(doc, x0, y0, x1, y1, color, fill) A filled box, corners inclusive.
+ printVecEllipseFill(doc, cx, cy, rx, ry, color, fill) A filled ellipse, out to the dots rx and ry from its centre.
+ printVecEllipse(doc, cx, cy, rx, ry, color, width, dash, arc, start, end) An ellipse's outline, or (arc) the part from start to end radians counterclockwise; dash as printVecLine's, along the outline from angle 0.
+ printVecDot(doc, x, y, color, width) A round dot width dots across.
+ printVecText(doc, font, x, y, text, len, color) Text with the top of its line at (x, y), with the font's underline and strike-through.
+ printVecImage(doc, rgb, w, h, sx, sy, sw, sh, dx, dy, dw, dh) The part sx, sy, sw, sh of a picture (R, G, B bytes) stretched over the dots dx, dy, dw, dh by the printer.
+
A fill (PrintFillE) is solid or a hatch of lines one dot wide every PRINT_HATCH_DOTS (8) dots, horizontal, vertical, either diagonal, or crossed, lined up across the page as BASIC's FillStyle hatches are. The pen (its color, width, and dashes) and the font are sent only when they change, and each of the printer's faces is made Latin-1 the first time the document uses it.
+
A font opened for a vector page (printFontOpen with page.vector set) is the printer's own Helvetica, Times, or Courier in the style asked for (FONT_STYLE_BOLD, FONT_STYLE_ITALIC): Courier for a fixed-pitch family or one with Mono or Courier in its name, Times for one with Serif, Times, Roman, Tinos, or Ledger in it (but not Sans), else Helvetica. It is exactly the size asked for, and printFontWidth, printFontHeight (1.2 times the size), and printFontAscent measure the printer's letters by Adobe's published widths, so text placed with them lines up as it prints. It draws only with printVecText (printFontSpans and printFontDraw draw nothing with it); for DVX's own letters drawn in bands on a PostScript printer, open the font with vector cleared in a copy of the page.
+
Text on a Page
+
A page a program draws can carry text in any font family (dvxText.h) at a size in points. printFontOpen(&page, family, decipoints, style) gives a PrintFontT for a page's resolution (its PrintPageInfoT): the size is in tenths of a point (120 for 12 points), the style any FONT_STYLE_ bits, underline and strike-through included. The font is the family's smallest strike at least as large as the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180), else its largest, scaled to the size across and down apart, since a 9-pin printer's dots are not square. Scaling is nearest pixel; scaling down, every pixel row and column of the strike still marks a dot, so thin strokes stay.
+
The base set's sizes go up to 24 pixels, so most printed sizes are scaled up from it and look blocky at 300 dpi. The Printer Fonts add-on (kpunch.prfonts) gives DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, about 6 to 24 points at 300 dpi and 10 to 40 at 180: installed, those families print crisp at those sizes with no change to the program.
+
Function Description
+ -------- -----------
+ printFontOpen(&page, family, decipoints, style) The font; NULL when out of memory or for a size of 0 or less. printFontClose gives it up.
+ printFontAscent(font), printFontHeight(font) Dots from the top of a line to its baseline, and to the next line.
+ printFontWidth(font, text, len) Dots across len characters of text.
+ printFontSpans(font, x, y, text, len, top, bottom, fn, ctx) The text with the top of its line at (x, y) as runs of dots (PrintSpanFnT fn(ctx, x0, x1, y)), only rows top to bottom: a line that misses them is skipped at once, so a page drawn band by band costs little for the lines elsewhere.
+ printFontDraw(font, x, y, text, len, color, rgb, width, y0, rows) The same painted in color (0xRRGGBB) into the band a PrintBandFnT is filling.
+
Positions are worked out in the strike's pixels from the start of the text and only then turned into dots, so printFontWidth and what is drawn always agree.
+
Printer Drivers
+
Model Driver
+ ----- ------
+ Epson 9-pin (ESC/P) 120 x 72 dots per inch (ESC * 1), 8-row bands, paper fed with ESC J; blank bands only feed paper, and each pass stops after its last black dot.
+ Epson 24-pin (ESC/P) 180 x 180 dots per inch (ESC * 39, 3 bytes per column), 24-row bands.
+ HP LaserJet (PCL) 150 dots per inch raster graphics, one uncompressed row transfer per dot row (every LaserJet understands it), trailing white bytes left out.
+ HP LaserJet III or DeskJet (300 dpi) 300 dots per inch (PCL 5, and PCL 3 on DeskJets): each row packed (compression mode 2, TIFF PackBits) and blank rows skipped (ESC * b n Y) instead of sent, so a page with white space on it costs next to nothing.
+ Epson 9-pin / 24-pin color As the black ones, with a four-color ribbon: a pass of the head for each ink with ESC r n choosing its band of the ribbon, yellow first and black last (dark ink does not smear into the light ones).
+ HP DeskJet 500C As the 300 dpi LaserJet, in three planes (ESC * r -3 U: cyan, magenta, yellow; black is the three together): ESC * b n V for each plane of a row but the last, ESC * b n W for the last.
+ HP DeskJet 550C and later As the 500C, in four planes (ESC * r -4 U: black, cyan, magenta, yellow).
+ PostScript Level 2 at 300 dpi: each band an imagemask of its black dots, packed (RunLengthDecode, the same PackBits) and in ASCII85, which any port carries; a band with no ink is not sent. The 8 by 10 inch area is centred across the paper the printer has loaded, half an inch from its top.
+ PostScript (color) As PostScript, each band a colorimage of 4 bits a dot (cyan, magenta, yellow, black) from the dithered planes. Both PostScript models also draw vector pages (see Vector Pages).
+ Apple ImageWriter II 144 x 144 dots per inch: 144 across (ESC p), and down by two passes of the 8 graphics wires 1/144 inch apart (the even rows of a 16-row band, a 1/144-inch line feed, the odd rows, then ESC T 15 and a line feed). A line is ESC G nnnn and its columns (bit 0 the top dot), placed at its first dot (ESC F nnnn). The document resets the printer (ESC c) and sets its switches whatever its DIP switches say: no line feed after CR, 8-bit data, no perforation skip; then unidirectional printing (ESC >), which keeps the passes in line.
+ Apple ImageWriter II (color) As the ImageWriter II with the four-color ribbon: a line for each ink with ESC K n choosing its band, yellow first and black last, CR between them.
+
The drivers are inside the library: each holds its resolution, the printable size in dots, the band height, the dither size, its inks, the text reset strings and usual page length, and begin / band / end functions that write the printer's commands. A new printer family is one more driver.
Dialogs and Ready-Made Printing
Function Description
-------- -----------
@@ -10317,7 +10653,8 @@ if (!job) {
printUiOpenPort(ctx, port) A job with the user interface hooks on LPTn (0-based) or PRINT_PORT_DEFAULT.
Jobs opened by these ask about printer problems with a Retry / Cancel message box. While a write waits, a modal Printing window with a Cancel button appears (at once for a document, after half a second for other output) and the whole desktop keeps running under it. Problems are told to the user; the functions return true only when everything was printed. Pictures need a printer model that prints them; on a text-only printer the user is told to choose the model in the Control Panel.
BASIC
-
src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup, and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem), and PrintSettingsStore (printDeviceStore).
+
BASIC's Printer object (the form runtime, formPrinter.c) is built on the calls above: it records a page's drawing, prints it with printUiDocOpen / printDocPage (each band drawn again from the record), and prints text with printFontOpen and printFontDraw; on a PostScript printer it draws the record once more as a vector page (printVec*), in the printer's own faces; its document goes where the program's last PrintChoose chose (printBasChoice, the running program's own answer only).
+
src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup, PrintCanPrintPictures, and PrintCanPrintColor (printModelPage). For serial printers it declares PrintDestIsSerial (printDestIsSerial), PrintBaudRate (printBaudRate), PrintHandshakeCount / PrintHandshakeLabel$ (printHandshakeLabel), PrintSettingBaud / PrintSettingHandshake (printSettingsRead), and PrintSerialStore (printSerialStore). For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem), and PrintSettingsStore (printDeviceStore).
BASIC C it calls
----- ----------
PrintPageSetting(prefs, field), PrintPageText$(prefs, field) printSettingsRead on a prefs.bas handle: one PrintPageSetupT field by name (Lines, Columns, MarginTop, MarginBottom, MarginLeft, MarginRight, TabWidth, Wrap; Header, Footer).
@@ -11350,9 +11687,8 @@ End Sub
----- ---- -----------
name char[32] Widget name for lookup via wgtFind().
x, y, w, h int32_t Computed geometry relative to the window content area (set by layout).
- minW, minH int32_t (tagged) Minimum size hints. Use wgtPixels(), wgtChars(), or wgtPercent(). 0 = auto.
- maxW, maxH int32_t (tagged) Maximum size constraints. 0 = no limit.
- prefW, prefH int32_t (tagged) Preferred size. 0 = auto.
+ minW, minH int32_t (tagged) Minimum size hints: the widget is never laid out smaller. Use wgtPixels(), wgtChars(), or wgtPercent(). 0 = auto. A fixed width in a row (a button's, a column of labels) is minW with weight 0.
+ maxW, maxH int32_t (tagged) Maximum size constraints: the widget never grows larger; across its box a capped widget is centred. 0 = no limit.
weight int32_t Extra-space distribution weight. 0 = fixed, 100 = normal. A widget with weight=200 gets twice the extra space of one with weight=100.
align WidgetAlignE Main-axis alignment for children: AlignStartE, AlignCenterE, AlignEndE.
spacing int32_t (tagged) Spacing between children (containers only). WGT_GAP_DEFAULT = the class's default; 0 = none.
@@ -12353,7 +12689,7 @@ wgtRichEditSetSelColor(re, DVX_RGB(0, 0, 160));
RichLabel
-
Text in many fonts, styles, and colors, wrapped and aligned paragraph by paragraph. The text is built by calls: wgtRichLabelSetFont, wgtRichLabelSetStyle, and wgtRichLabelSetColor choose what the following wgtRichLabelAddText uses; wgtRichLabelAddLineBreak starts a new line and wgtRichLabelAddParagraph a new paragraph with its own alignment. Text starts in the widget's own font (wgtSetFont, or its container's). The widget is laid out at its width hint (prefW, else minW, which BASIC's Width sets), wrapping at spaces (a word too wide for a line breaks inside it); without one each paragraph is one line. Every line is as tall as the tallest font on it, its text standing on one baseline. Not focusable; drawn the same enabled or not. It fills its rectangle as a Label does: with the nearest explicit bgColor, else the content background. Built on the text flow in libdvx (dvxFlow.h).
+
Text in many fonts, styles, and colors, wrapped and aligned paragraph by paragraph. The text is built by calls: wgtRichLabelSetFont, wgtRichLabelSetStyle, and wgtRichLabelSetColor choose what the following wgtRichLabelAddText uses; wgtRichLabelAddLineBreak starts a new line and wgtRichLabelAddParagraph a new paragraph with its own alignment. Text starts in the widget's own font (wgtSetFont, or its container's). The widget is laid out at its width hint (minW, which BASIC's Width sets), wrapping at spaces (a word too wide for a line breaks inside it); without one each paragraph is one line. Every line is as tall as the tallest font on it, its text standing on one baseline. Not focusable; drawn the same enabled or not. It fills its rectangle as a Label does: with the nearest explicit bgColor, else the content background. Built on the text flow in libdvx (dvxFlow.h).
Header: widgets/richLbl.h
Creation
WidgetT *rl = wgtRichLabel(parent);
diff --git a/installToolchain.sh b/installToolchain.sh
index 79dd4385..f2e22390 100755
--- a/installToolchain.sh
+++ b/installToolchain.sh
@@ -59,6 +59,9 @@
# toolchains/fonts which pins them): FreeType for the font converter,
# built from source, and every font DVX converts
# and ships, with its licence (src/fonts/sources.txt)
+# toolchains/soundfonts the SoundFonts the bank add-ons are made from
+# (src/addons/banks/installSoundFonts.sh, which pins
+# them): FluidR3_GM, for the dvxgm add-on
#
# Every download is checked against a pinned SHA-256 before it is unpacked,
# so a changed or corrupted archive is refused instead of installed.
@@ -346,13 +349,20 @@ else
"${ROOT}/src/fonts/installFonts.sh"
fi
+# The SoundFonts the bank add-ons are made from, pinned in their own script
+if [ "${FORCE}" -eq 1 ]; then
+ "${ROOT}/src/addons/banks/installSoundFonts.sh" --force
+else
+ "${ROOT}/src/addons/banks/installSoundFonts.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}" "${UPX_BIN}" "${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" "${TOOLCHAINS}/freetype/lib/libfreetype.a" "${TOOLCHAINS}/fonts/.installed"; do
+for f in "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/dxe3gen" "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/exe2coff" "${CWSDSTUB}" "${UPX_BIN}" "${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" "${TOOLCHAINS}/freetype/lib/libfreetype.a" "${TOOLCHAINS}/fonts/.installed" "${TOOLCHAINS}/soundfonts/FluidR3_GM.sf2"; do
if [ ! -e "${f}" ]; then
echo "installToolchain: expected file missing after install: ${f}" >&2
exit 1
diff --git a/screenshots/86box/addons-addon-no.png b/screenshots/86box/addons-addon-no.png
index 07e38fee..3b255300 100644
--- a/screenshots/86box/addons-addon-no.png
+++ b/screenshots/86box/addons-addon-no.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:87a79d1bb9a57eba5bc045892b359d85c20a332a95de259c233ec36b1de3ef2e
-size 18229
+oid sha256:ff64f081e2927482a5cc2dbf8470b9bf95da1d46ff64a7c146f5c1898020a2e1
+size 18045
diff --git a/screenshots/86box/apps-clock.png b/screenshots/86box/apps-clock.png
index aac25d3e..29394ee6 100644
--- a/screenshots/86box/apps-clock.png
+++ b/screenshots/86box/apps-clock.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c47ef1d6f91f6bf682c65cffe7a635fac96e7b09d59fd05a54736b9ae8e72880
-size 17993
+oid sha256:d3e5f6fdf925d1abd17b30278162c56c2ea6d6548128ec958fa0da73c120b3e9
+size 18012
diff --git a/screenshots/86box/apps-cpanel.png b/screenshots/86box/apps-cpanel.png
index eb7203d1..7c2c7af8 100644
--- a/screenshots/86box/apps-cpanel.png
+++ b/screenshots/86box/apps-cpanel.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bf51593a279f8352dde3ef83d94942cb7d82d3522c91d2a3b43758674bd3535
-size 16427
+oid sha256:88949a0a11f46387060a4e4a5faaca763bd1fd8da35bae21bce78e78e74b3c02
+size 15931
diff --git a/screenshots/86box/apps-desktop.png b/screenshots/86box/apps-desktop.png
index 9fc09f1a..a090d4e6 100644
--- a/screenshots/86box/apps-desktop.png
+++ b/screenshots/86box/apps-desktop.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0a697ed80887720ffc0e258516c4a59068e86521f79c4d9d777e5b6d703a73cf
-size 18588
+oid sha256:a12c2d2fcac3b118c581b7cab7237adb773677a002781b4819728fa927ed9130
+size 18563
diff --git a/screenshots/86box/apps-dvxbasic.png b/screenshots/86box/apps-dvxbasic.png
index 16b7bf11..3566300e 100644
--- a/screenshots/86box/apps-dvxbasic.png
+++ b/screenshots/86box/apps-dvxbasic.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c5b9aedac500ad8ff4cbbb7b59bf49751f4bde23f6e8f8a3ae9f0f34424307a1
-size 18393
+oid sha256:f36dae98688d62bd81a87e6129466223b5604ad6100b0b03bf358c72c0c62e54
+size 18388
diff --git a/screenshots/86box/apps-dvxhelp.png b/screenshots/86box/apps-dvxhelp.png
index 1bd4ab98..878b2d82 100644
--- a/screenshots/86box/apps-dvxhelp.png
+++ b/screenshots/86box/apps-dvxhelp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5e2c8e671952f6ddfb618246e4b20fb37acc9dc23aa3f9a1616a188a78f3a974
-size 21764
+oid sha256:4e557895dd4541eea4a1ff516cf44c08beb425ac7dc01359ddaaa914ee873430
+size 21777
diff --git a/screenshots/86box/apps-iconed.png b/screenshots/86box/apps-iconed.png
index 658cb98c..a9b291b1 100644
--- a/screenshots/86box/apps-iconed.png
+++ b/screenshots/86box/apps-iconed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:35e2e173eb0155a1799f985effc2d4100ea31be802ad7e312d5ca66751601fa0
-size 14832
+oid sha256:9e4cdc9de6d03c087b222bd6bb1a134718847b9f804098fb16e8c3f8c2608de5
+size 14813
diff --git a/screenshots/86box/apps-imgview.png b/screenshots/86box/apps-imgview.png
index 65619ad3..b6c84679 100644
--- a/screenshots/86box/apps-imgview.png
+++ b/screenshots/86box/apps-imgview.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20216cfc2f5c629596ee18af24de18501b2431af8ee731ceb5be4427e219661b
-size 12892
+oid sha256:9861190daaec8871a3fbab7bd5a3913ccd8a1e7fdfe990900ebc4013ce6a763d
+size 12874
diff --git a/screenshots/86box/apps-notepad.png b/screenshots/86box/apps-notepad.png
index f8d5861d..ed658741 100644
--- a/screenshots/86box/apps-notepad.png
+++ b/screenshots/86box/apps-notepad.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cbbc99f6095370b23c0cda96486df13fb5c1e6654ff371b9a67b8fe62a116eda
-size 13750
+oid sha256:04fc14a8ec0a1b19d97363f61a0e25abe53caa288aebb89dafbf610c81253653
+size 13752
diff --git a/screenshots/86box/apps-player.png b/screenshots/86box/apps-player.png
new file mode 100644
index 00000000..7305aa21
--- /dev/null
+++ b/screenshots/86box/apps-player.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52310f0edc3d1689175593eddb3e0a06345b933605ebecf5d47ca63987afe488
+size 13332
diff --git a/screenshots/86box/apps-resedit.png b/screenshots/86box/apps-resedit.png
index 4becf7fd..10bc49a9 100644
--- a/screenshots/86box/apps-resedit.png
+++ b/screenshots/86box/apps-resedit.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3fc86dd4c3acc583cdb962e02b1c3a563a67bd5d84c0ed1a19314937ab191c51
-size 14168
+oid sha256:fffa0df7faae7202429cf3c5e782703f68ceb804bb52177c0088f0f2c67e3288
+size 14176
diff --git a/screenshots/86box/banked-bk-back.png b/screenshots/86box/banked-bk-back.png
index 7976b145..9c404784 100644
--- a/screenshots/86box/banked-bk-back.png
+++ b/screenshots/86box/banked-bk-back.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3425e4a35d856bef29f68f9732c7fad18669dfd3ebd1178835c0191827029cfb
-size 18540
+oid sha256:28c79ae8f56c95b2bcfc337305fd2503a44b4a11cd21cae8bb2d8075288ce389
+size 18520
diff --git a/screenshots/86box/banked-bk-desk.png b/screenshots/86box/banked-bk-desk.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/86box/banked-bk-desk.png
+++ b/screenshots/86box/banked-bk-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/86box/banked-bk-drag.png b/screenshots/86box/banked-bk-drag.png
index 51540044..683e86c0 100644
--- a/screenshots/86box/banked-bk-drag.png
+++ b/screenshots/86box/banked-bk-drag.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d815eedadc4035fea2027775eb340ae6b33b2b4f9bf90115ddacfe565096f7d9
-size 15053
+oid sha256:914ffc97631ee498aa1422cde129c85adfd627f1b1ab9b0957ade2e4ec08dd34
+size 14781
diff --git a/screenshots/86box/banked-bk-note.png b/screenshots/86box/banked-bk-note.png
index f80814d6..ed658741 100644
--- a/screenshots/86box/banked-bk-note.png
+++ b/screenshots/86box/banked-bk-note.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1ffe8d8b71867ed7b7c676213a0b715f4de49565e1e83f0694466d2d701673cb
-size 13754
+oid sha256:04fc14a8ec0a1b19d97363f61a0e25abe53caa288aebb89dafbf610c81253653
+size 13752
diff --git a/screenshots/86box/boot-boot.png b/screenshots/86box/boot-boot.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/86box/boot-boot.png
+++ b/screenshots/86box/boot-boot.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/86box/cpanel-cp-color.png b/screenshots/86box/cpanel-cp-color.png
index 5261386c..afd39ff6 100644
--- a/screenshots/86box/cpanel-cp-color.png
+++ b/screenshots/86box/cpanel-cp-color.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:660bf211134751affcd3a05e3d5e8e8c297cda750a417873a642c2ee72861542
-size 18266
+oid sha256:e66c7fb68c7e1ae2a6e0f140254fab01fa9d355e0cd714e97982666b27968420
+size 18719
diff --git a/screenshots/86box/cpanel-cp-desk.png b/screenshots/86box/cpanel-cp-desk.png
index c912aaa5..1bc6c33c 100644
--- a/screenshots/86box/cpanel-cp-desk.png
+++ b/screenshots/86box/cpanel-cp-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:97022e5d29c1c26e7d47625d3fb54121173c54b9f58f884a17c451a555a3b3b3
-size 14436
+oid sha256:9030c3b8ed8d9e54aed50f1ab5d56a015e7938ea038ee487e3f188235de8e699
+size 15016
diff --git a/screenshots/86box/cpanel-cp-joy.png b/screenshots/86box/cpanel-cp-joy.png
index a2a740d4..0e0b2aaa 100644
--- a/screenshots/86box/cpanel-cp-joy.png
+++ b/screenshots/86box/cpanel-cp-joy.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eb4bdad8d10caa593d3cfab42126f537714a1d2ea70e06ca64656e26fbecb785
-size 16334
+oid sha256:a27192d2ccf98d8d10ff977d4ed3c8bcda6a911c1f9578001b1219a02ade9346
+size 15833
diff --git a/screenshots/86box/cpanel-cp-mouse.png b/screenshots/86box/cpanel-cp-mouse.png
index eb7203d1..7c2c7af8 100644
--- a/screenshots/86box/cpanel-cp-mouse.png
+++ b/screenshots/86box/cpanel-cp-mouse.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bf51593a279f8352dde3ef83d94942cb7d82d3522c91d2a3b43758674bd3535
-size 16427
+oid sha256:88949a0a11f46387060a4e4a5faaca763bd1fd8da35bae21bce78e78e74b3c02
+size 15931
diff --git a/screenshots/86box/cpanel-cp-net.png b/screenshots/86box/cpanel-cp-net.png
index 8a0078a8..c05764ef 100644
--- a/screenshots/86box/cpanel-cp-net.png
+++ b/screenshots/86box/cpanel-cp-net.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:27f0996dda3d29cd134257c40b19a6ef215ce45162999250c704004570995426
-size 16199
+oid sha256:11daa8703e086e9c3b3aa60d075616204bf2d9f7d228ffa86a0538dcbba25b0f
+size 15677
diff --git a/screenshots/86box/cpanel-cp-print.png b/screenshots/86box/cpanel-cp-print.png
index 6b3a71c0..10aa46db 100644
--- a/screenshots/86box/cpanel-cp-print.png
+++ b/screenshots/86box/cpanel-cp-print.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e35a2a6c663538c45692d66e5335d808e323bda1c5f1c53e51f1124a2e6cba85
-size 15909
+oid sha256:f36a45f38c72c9997d4b91ae8166beb9d557d304d2c298c718906dd5c0fb7f75
+size 16993
diff --git a/screenshots/86box/cpanel-cp-sound.png b/screenshots/86box/cpanel-cp-sound.png
index 8b22b99b..0d2ceba7 100644
--- a/screenshots/86box/cpanel-cp-sound.png
+++ b/screenshots/86box/cpanel-cp-sound.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8af0d409e9b907747748030acb9e6974893bdd3a426d142d679a023bfd1ac6ce
-size 17643
+oid sha256:685f0c278368b5a74e5aa3710c0dc03a71b2f8f0bf1d2519309957090d7bcbbf
+size 18726
diff --git a/screenshots/86box/cpanel-cp-types.png b/screenshots/86box/cpanel-cp-types.png
index 1e405480..0b7d27dd 100644
--- a/screenshots/86box/cpanel-cp-types.png
+++ b/screenshots/86box/cpanel-cp-types.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:53601bf8609488ed1a85b388e405854ec038f216b2cf6d4350d749be05195dd5
-size 23172
+oid sha256:2749e1d0ca5ca17f97455a0e6a4200d587f3b0961b135ce80fb37ee586325ef8
+size 24863
diff --git a/screenshots/86box/cpanel-cp-video.png b/screenshots/86box/cpanel-cp-video.png
index ba8d7ec3..20cde1f5 100644
--- a/screenshots/86box/cpanel-cp-video.png
+++ b/screenshots/86box/cpanel-cp-video.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0811ca3be22aeba9decba8821ed65220c26b37d3155d01320fd8b46ec9116285
-size 19815
+oid sha256:f1bbd95ec800c2b9dcb50cc3b6de3ce242a51358f7ff3fbadef67ff6da390ce0
+size 20039
diff --git a/screenshots/86box/dosprog-dp-after.png b/screenshots/86box/dosprog-dp-after.png
new file mode 100644
index 00000000..15ff8dc1
--- /dev/null
+++ b/screenshots/86box/dosprog-dp-after.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/86box/dosprog-dp-ask.png b/screenshots/86box/dosprog-dp-ask.png
new file mode 100644
index 00000000..4fa453f2
--- /dev/null
+++ b/screenshots/86box/dosprog-dp-ask.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d1eac79fc717600f2dbd52fd23090afb5d214ba255bc962e8c447394fc0da496
+size 17445
diff --git a/screenshots/86box/dosprog-dp-back.png b/screenshots/86box/dosprog-dp-back.png
new file mode 100644
index 00000000..15ff8dc1
--- /dev/null
+++ b/screenshots/86box/dosprog-dp-back.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/86box/exit-cancel.png b/screenshots/86box/exit-cancel.png
index cee10809..4583b6ea 100644
--- a/screenshots/86box/exit-cancel.png
+++ b/screenshots/86box/exit-cancel.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3478e1d84decc5ac10844883062e6bea6885e30ec7ebe0101ebee7d9444ae619
-size 14351
+oid sha256:4331208df0cdee00be0e014b9251f7c5e44e19c0ed02586d4c78d473349e3b48
+size 14347
diff --git a/screenshots/86box/exit-savechg.png b/screenshots/86box/exit-savechg.png
index 74b3e466..6dcf3389 100644
--- a/screenshots/86box/exit-savechg.png
+++ b/screenshots/86box/exit-savechg.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c375e0886534255d93d2ae1ecf32683d0036cfbfb628320140a7a5fc9b019695
-size 14348
+oid sha256:13ec25fc0b583981119747baa7fa24e7e97429106d1c5861be1494304ec29e6b
+size 14350
diff --git a/screenshots/86box/f1-h-basdem.png b/screenshots/86box/f1-h-basdem.png
index f178ed94..7593a315 100644
--- a/screenshots/86box/f1-h-basdem.png
+++ b/screenshots/86box/f1-h-basdem.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f5b3bc24e986cc704e74e6b7dd72b8f42ddc87c9a88e7c535d5c5d94d6c2c51c
-size 20728
+oid sha256:a360f2333c0d5f40cea16b7da81344b99ea18bd23e5c806a97b2d2443b179d59
+size 20718
diff --git a/screenshots/86box/f1-h-clock.png b/screenshots/86box/f1-h-clock.png
index e293aa1d..3c960f64 100644
--- a/screenshots/86box/f1-h-clock.png
+++ b/screenshots/86box/f1-h-clock.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0ce6bf30393be569792f8c906b1fe1506ee09aa304d58a1c5ac2c233bfd28d28
-size 18940
+oid sha256:47b038f1dacb57e532944605cc0c1ef39802378553953fd8ce052049876c8aaf
+size 18956
diff --git a/screenshots/86box/f1-h-cpanel.png b/screenshots/86box/f1-h-cpanel.png
index a75dbb95..775690f0 100644
--- a/screenshots/86box/f1-h-cpanel.png
+++ b/screenshots/86box/f1-h-cpanel.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e6fe705e41b7a4dc8b969945e455b2d79fd32d517f78b829ded6cfd44324db70
-size 22663
+oid sha256:30800dde17d30be96cbd5e8de2072d710eb89c88a2f49fa92136ed0d983affa1
+size 23161
diff --git a/screenshots/86box/f1-h-iconed.png b/screenshots/86box/f1-h-iconed.png
index a2a4f8da..c88408fd 100644
--- a/screenshots/86box/f1-h-iconed.png
+++ b/screenshots/86box/f1-h-iconed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ab09163d20f674e0bd2effd008ea3489f5b3c223355aa1c4790ec8f11edbe2a3
-size 17919
+oid sha256:3d48d0d2dcb8e4d9540d23830147904d5d23e35f02ccaf4155ed0f71e00b57a2
+size 17930
diff --git a/screenshots/86box/f1-h-imgvie.png b/screenshots/86box/f1-h-imgvie.png
index 2997b10f..2f5e2f34 100644
--- a/screenshots/86box/f1-h-imgvie.png
+++ b/screenshots/86box/f1-h-imgvie.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:551335eede657c3a0ba3d34ea23d8134214f3b7914510cdd02d417f14cd949dc
-size 19273
+oid sha256:20a69aa1199ae81c20ce4f7a506bb6f4cae993bf87dce5e363aa1dac097f8081
+size 19288
diff --git a/screenshots/86box/f1-h-notepa.png b/screenshots/86box/f1-h-notepa.png
index edb6ee26..d7321644 100644
--- a/screenshots/86box/f1-h-notepa.png
+++ b/screenshots/86box/f1-h-notepa.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8a377752193cd085742ecf9a616c990e897189c20443ca754434a3fb036c47d3
-size 18895
+oid sha256:f88980948aa279f05b4e22b59c98fb86299259bf5e0c8bf63ed397656cd136f7
+size 18896
diff --git a/screenshots/86box/f1-h-player.png b/screenshots/86box/f1-h-player.png
new file mode 100644
index 00000000..f02e0686
--- /dev/null
+++ b/screenshots/86box/f1-h-player.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:92aedcc272e3e121db4c7f83114d41465372ad0c0d1a0869eb6013efeb502073
+size 19021
diff --git a/screenshots/86box/f1-h-progmn.png b/screenshots/86box/f1-h-progmn.png
index d01028fe..ad036001 100644
--- a/screenshots/86box/f1-h-progmn.png
+++ b/screenshots/86box/f1-h-progmn.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:63826ce353d7dbb386da4d50f7ee526ab96d324db12761727fe418a9b8b857a5
-size 21918
+oid sha256:21085ae6d3c9049c88bdb7c0704c643722af5f6e92479264915f295e9b377c99
+size 22021
diff --git a/screenshots/86box/f1-h-resedi.png b/screenshots/86box/f1-h-resedi.png
index 3e519eee..3ce29b45 100644
--- a/screenshots/86box/f1-h-resedi.png
+++ b/screenshots/86box/f1-h-resedi.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ae20d685ccbd0230a20c48efa684c0fde6ae1eed48aeaf1a6fedc81b4c2ede50
-size 20875
+oid sha256:1a9e3cac4489b2633d731e6ea969c36f641f1404502d73d143d9a6befe02ac00
+size 20888
diff --git a/screenshots/86box/filedlg-fd-back.png b/screenshots/86box/filedlg-fd-back.png
index f579cfe5..9846da68 100644
--- a/screenshots/86box/filedlg-fd-back.png
+++ b/screenshots/86box/filedlg-fd-back.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7382ad12f6411a8448df45ea184d8306423a51e0fbd8bb2d46beeaa34b43ac29
-size 15234
+oid sha256:c7a84dbadd39c80e002a54630e71c97343f4f3def3ac0d5dc33eb461a2faa924
+size 15247
diff --git a/screenshots/86box/filedlg-fd-list.png b/screenshots/86box/filedlg-fd-list.png
index e0f6968e..556eb3eb 100644
--- a/screenshots/86box/filedlg-fd-list.png
+++ b/screenshots/86box/filedlg-fd-list.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6c48f8df6224e37237a612f8440c83040123955ea3b7ce6c2c33907df55df575
-size 15346
+oid sha256:eaab61394767ee5b4695072c145a64777c68ea0f5d620edbe68b390363e11b39
+size 15349
diff --git a/screenshots/86box/filedlg-fd-nordy.png b/screenshots/86box/filedlg-fd-nordy.png
index 383cd199..18d68769 100644
--- a/screenshots/86box/filedlg-fd-nordy.png
+++ b/screenshots/86box/filedlg-fd-nordy.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b6b6cd04bf85ff797c3be50ecff2fe0a3efc61e19ea397fc00c31ec7369f1111
-size 15443
+oid sha256:797486eee6e9cc31ea083b929ccc5262d1a612bda098c5f655833e5eec0318a1
+size 15453
diff --git a/screenshots/86box/filedlg-fd-open.png b/screenshots/86box/filedlg-fd-open.png
index 56bee79c..1adec5b7 100644
--- a/screenshots/86box/filedlg-fd-open.png
+++ b/screenshots/86box/filedlg-fd-open.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fa786de9621f9d9a39373e800572fbbce9807a7c432f0330572908f602a2060a
-size 15205
+oid sha256:87706be4507c6378bb3b126ec0ba89eb6d86bb1a07e822571d6cd6cecc45ce11
+size 15176
diff --git a/screenshots/86box/fullmode-fm-desk.png b/screenshots/86box/fullmode-fm-desk.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/86box/fullmode-fm-desk.png
+++ b/screenshots/86box/fullmode-fm-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/86box/fullscr-fs-desk.png b/screenshots/86box/fullscr-fs-desk.png
index c2e26be0..7b0f9490 100644
--- a/screenshots/86box/fullscr-fs-desk.png
+++ b/screenshots/86box/fullscr-fs-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:adb402decf2e7dc5f420b0f5d479eaaa0cec605c2e4b58ed44ab6575165a0fd5
-size 17558
+oid sha256:29abf7f02187f54f333e23f40a4f4fda4e3d6e306e8153e688553af9209df357
+size 17506
diff --git a/screenshots/86box/hang-ended.png b/screenshots/86box/hang-ended.png
index 87a65414..8f5b923b 100644
--- a/screenshots/86box/hang-ended.png
+++ b/screenshots/86box/hang-ended.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:967335f41a1b4b76a584e0eeffbfd607ce3063794b7c9a066ef097e3ce152d3b
-size 15165
+oid sha256:3fbb5bd05eb93ea9cd3195ac2990d9c0b538a3322e6f8f4ee1e8a8debcfa169d
+size 15148
diff --git a/screenshots/86box/hang-taskmgr.png b/screenshots/86box/hang-taskmgr.png
index f365e9ad..6a068bc2 100644
--- a/screenshots/86box/hang-taskmgr.png
+++ b/screenshots/86box/hang-taskmgr.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2953c993db395b28cbdae6e951af44a949cd201cda152d9ef7e9f4af80a661ec
-size 16192
+oid sha256:12e5fd681c5b852d1409666069caac5602cccea5a7017e98838d3e777d203089
+size 16018
diff --git a/screenshots/86box/install-in-desk.png b/screenshots/86box/install-in-desk.png
index 00444c34..7fbf6e91 100644
--- a/screenshots/86box/install-in-desk.png
+++ b/screenshots/86box/install-in-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:78ef264c66bdc27492ebbfc01453f7face1b2fbca1f9eb0621836481aae7c499
-size 18548
+oid sha256:1fe45de792395dc0311670f3607a216f3944928d61c85e4aa55d0349f2379e09
+size 18601
diff --git a/screenshots/86box/lfn-lfnsmk.png b/screenshots/86box/lfn-lfnsmk.png
index 27ce819e..43e7e7c8 100644
--- a/screenshots/86box/lfn-lfnsmk.png
+++ b/screenshots/86box/lfn-lfnsmk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8c9500f652006d5b48f34837dcc059eb25d878fc6d174a04aa2d17d1c137319c
-size 17034
+oid sha256:ca206b4f6fa0a6fe6d27bf8ab71106f174b5fd15eef660931d8c574894bed50a
+size 17119
diff --git a/screenshots/86box/menus-m-altdn.png b/screenshots/86box/menus-m-altdn.png
index 95e69470..a3180645 100644
--- a/screenshots/86box/menus-m-altdn.png
+++ b/screenshots/86box/menus-m-altdn.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0e1679bdc50865c2087f3687a210a7677c7175c07b9d050bcf6e04dc21d3d39e
-size 16816
+oid sha256:17dbbfe03c1e4e37960e82731d72920ecd7a1bad53ad51031a4b3ff6e708e8a7
+size 16401
diff --git a/screenshots/86box/menus-m-altup.png b/screenshots/86box/menus-m-altup.png
index 290bda7e..104e2339 100644
--- a/screenshots/86box/menus-m-altup.png
+++ b/screenshots/86box/menus-m-altup.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2bd9800a5cf14a50f770c69b5a785724482fdb62aac9ee7de8069aa9c16eb2ed
-size 16426
+oid sha256:4746eeae2ccbd52434df49c45e9045fc5c8a47f7a64ce0ab735c2ea62fb84571
+size 15971
diff --git a/screenshots/86box/menus-m-design.png b/screenshots/86box/menus-m-design.png
index 65a83dc5..d54d6915 100644
--- a/screenshots/86box/menus-m-design.png
+++ b/screenshots/86box/menus-m-design.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b520534eccc14ec24b1826a4d0ef7fe0c9e7d56f86befef504de05d7faa25b46
-size 23621
+oid sha256:c1eeb4d3e4cb61600af378d7f36c0f96290f74fe2da8fa14066ef8e779d1459c
+size 23804
diff --git a/screenshots/86box/menus-m-drop.png b/screenshots/86box/menus-m-drop.png
index 95e69470..a3180645 100644
--- a/screenshots/86box/menus-m-drop.png
+++ b/screenshots/86box/menus-m-drop.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0e1679bdc50865c2087f3687a210a7677c7175c07b9d050bcf6e04dc21d3d39e
-size 16816
+oid sha256:17dbbfe03c1e4e37960e82731d72920ecd7a1bad53ad51031a4b3ff6e708e8a7
+size 16401
diff --git a/screenshots/86box/menus-m-file.png b/screenshots/86box/menus-m-file.png
index bedb5c3d..62513274 100644
--- a/screenshots/86box/menus-m-file.png
+++ b/screenshots/86box/menus-m-file.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5805a5feb76aa404a93656e174b878dbce68fea3cb26a178b2ef6803e864611c
-size 14731
+oid sha256:e2197b06a2e6ced93066e8a8c96b085866685af9e117284a4e7061d547156633
+size 14721
diff --git a/screenshots/86box/menus-m-run.png b/screenshots/86box/menus-m-run.png
index 08415ab8..309d2dc5 100644
--- a/screenshots/86box/menus-m-run.png
+++ b/screenshots/86box/menus-m-run.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1cacc50a8de3c2c2594d2ab5f99adce74b3fd124f67c49652767f843a475a5b1
-size 18218
+oid sha256:2661a25a8cfcde559ce0b39092629459bb6e7c8618cbac4c9524da4d7cb3e622
+size 18231
diff --git a/screenshots/86box/midiin-rc-midi.png b/screenshots/86box/midiin-rc-midi.png
new file mode 100644
index 00000000..4fcb13ba
--- /dev/null
+++ b/screenshots/86box/midiin-rc-midi.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cd8a9e6857d3154e1bcbee3ed5e6ee46ff25c49ef23275f5f178f2a1ec437383
+size 16033
diff --git a/screenshots/86box/net-netsmk.png b/screenshots/86box/net-netsmk.png
index e6ed02ab..c7e8436a 100644
--- a/screenshots/86box/net-netsmk.png
+++ b/screenshots/86box/net-netsmk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2d559bf9d6b37813421ef3623c2c49427cb3356b0ad887e776f352f84f3fb54b
-size 16779
+oid sha256:6b99df6febae1feac39833afaa1ffb12c28c589632d8af95405fb55effeb3da1
+size 16763
diff --git a/screenshots/86box/pardsgn-pardsgn.png b/screenshots/86box/pardsgn-pardsgn.png
index ab8ba75f..9da764d4 100644
--- a/screenshots/86box/pardsgn-pardsgn.png
+++ b/screenshots/86box/pardsgn-pardsgn.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9d93f1f23f8ffc5fedd123ef6156982bd420a237247c7bf2b2e1a40029119945
-size 14439
+oid sha256:99b20a168970a3135ca8f7634a3a5896abc18ba673b33b9e20a2447b41d04d30
+size 14438
diff --git a/screenshots/86box/parhlp-parhlp-l.png b/screenshots/86box/parhlp-parhlp-l.png
index ba97be84..433432b8 100644
--- a/screenshots/86box/parhlp-parhlp-l.png
+++ b/screenshots/86box/parhlp-parhlp-l.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1466e4d7946dc061dbb1c3485acb38ee6cbcd0930806a25b48fc355f641a2020
-size 15611
+oid sha256:582f9570cb535cda8be533c31da0f425a8ef42c24e630511b5125b53058086fd
+size 15605
diff --git a/screenshots/86box/parhlp-parhlp.png b/screenshots/86box/parhlp-parhlp.png
index 72a5f511..be99b909 100644
--- a/screenshots/86box/parhlp-parhlp.png
+++ b/screenshots/86box/parhlp-parhlp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c114ad172296848ae10b9e4cd85574be604f42018ebb95024199603713ac0fda
-size 15611
+oid sha256:b4428ce7fb858c787b2bdb070c0c16f3d9190785cdb51f082046fa0e94bff114
+size 15603
diff --git a/screenshots/86box/parpm-parpm.png b/screenshots/86box/parpm-parpm.png
index 08b4193d..a50f176d 100644
--- a/screenshots/86box/parpm-parpm.png
+++ b/screenshots/86box/parpm-parpm.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:032f48556016077f70afc3308a88898fee372db590b371ba381e6408472626d5
-size 17348
+oid sha256:4fc2c96e65240cabcb060dbc0c92e4c59d5424afb2790448e3e66ecffe985f5a
+size 17400
diff --git a/screenshots/86box/paths-paths.png b/screenshots/86box/paths-paths.png
index 2b823c9e..879cd012 100644
--- a/screenshots/86box/paths-paths.png
+++ b/screenshots/86box/paths-paths.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:58503a736c280538316a7afa3e0d14485397b3122fc68a04a4ad94515662379d
-size 16660
+oid sha256:53ad7a1a3d0f2b1838d3f7544ffd8c82a39c3763defa4058cf7b6c331b82f132
+size 16687
diff --git a/screenshots/86box/player-p-mid.png b/screenshots/86box/player-p-mid.png
new file mode 100644
index 00000000..ee4771e1
--- /dev/null
+++ b/screenshots/86box/player-p-mid.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb2c251bc81190cf753890c9acb4b528d5637a8b3029dc6c7d37d17d5c8f68a3
+size 14998
diff --git a/screenshots/86box/player-p-mod.png b/screenshots/86box/player-p-mod.png
new file mode 100644
index 00000000..eee36d8d
--- /dev/null
+++ b/screenshots/86box/player-p-mod.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2115cb3af475de499f43b42efaa5ecedcb88ceede12d1238406512fb2c4de71
+size 14530
diff --git a/screenshots/86box/player-p-voc.png b/screenshots/86box/player-p-voc.png
new file mode 100644
index 00000000..67e72d18
--- /dev/null
+++ b/screenshots/86box/player-p-voc.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9375379dc9066aa01d0f857c6387134cba2f2e1d29315babade0d1fe062e63d6
+size 14712
diff --git a/screenshots/86box/player-p-wav.png b/screenshots/86box/player-p-wav.png
new file mode 100644
index 00000000..8c4ca0b1
--- /dev/null
+++ b/screenshots/86box/player-p-wav.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81ab1b214f13c292b7034488d9001d405c9fa9fce3b649c412fb98bb4d5ff9da
+size 13944
diff --git a/screenshots/86box/print-print1.png b/screenshots/86box/print-print1.png
index c82bc456..64f230bd 100644
--- a/screenshots/86box/print-print1.png
+++ b/screenshots/86box/print-print1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad64cbc363a6f7ec0491d1ea4e480ed1b48d2b47c09363c722cd89d2bd5d56ad
-size 14872
+oid sha256:211b7ed636aabb8995a4e3e2deedfe4231aa7a053fba9cd03302013ff3c7d6ca
+size 14867
diff --git a/screenshots/86box/record-rc-open.png b/screenshots/86box/record-rc-open.png
new file mode 100644
index 00000000..b72ad1d9
--- /dev/null
+++ b/screenshots/86box/record-rc-open.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d69371019843a140889ff6b81c8813ae24288caeee4507bee430dcf75e33cd93
+size 15955
diff --git a/screenshots/86box/safevid-sv-desk.png b/screenshots/86box/safevid-sv-desk.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/86box/safevid-sv-desk.png
+++ b/screenshots/86box/safevid-sv-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/86box/saver-sv-back.png b/screenshots/86box/saver-sv-back.png
new file mode 100644
index 00000000..15ff8dc1
--- /dev/null
+++ b/screenshots/86box/saver-sv-back.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/86box/saver-sv-blank.png b/screenshots/86box/saver-sv-blank.png
new file mode 100644
index 00000000..cfe14ee9
--- /dev/null
+++ b/screenshots/86box/saver-sv-blank.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:db875c74be9d218490440cedf7e28ebd06dd070fd4ceacbf34613e08798982e7
+size 1928
diff --git a/screenshots/86box/saver-sv-cpd.png b/screenshots/86box/saver-sv-cpd.png
new file mode 100644
index 00000000..d15c8fe4
--- /dev/null
+++ b/screenshots/86box/saver-sv-cpd.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:77deb8e3c185672514bdb5ecfd1a0a1e4029530c7b2c9a658281223c3b5b833f
+size 14857
diff --git a/screenshots/86box/sdk-h-sdkapp.png b/screenshots/86box/sdk-h-sdkapp.png
index 51487527..f55a89f1 100644
--- a/screenshots/86box/sdk-h-sdkapp.png
+++ b/screenshots/86box/sdk-h-sdkapp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:41b5260accda5a08f0fb80acd96eba344ac23a0375d7b0d08bf03189cfcbda02
-size 16834
+oid sha256:6c5c19fea8dc7918d3a13927feb14e7f2497e7b60a28b0e6df8f069c607c2399
+size 16860
diff --git a/screenshots/86box/sdk-sdkapp.png b/screenshots/86box/sdk-sdkapp.png
index 323617f2..baac89fd 100644
--- a/screenshots/86box/sdk-sdkapp.png
+++ b/screenshots/86box/sdk-sdkapp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:af5d5842a32438d1b385e419e7fbf828e01e6b80ae36f4b22836e754d0473fb7
-size 17648
+oid sha256:64fdda7f9f947dbe9e575486bb3aab472947df0e6952c3814ef37517f035d2a2
+size 17662
diff --git a/screenshots/86box/sdk-sdkbas.png b/screenshots/86box/sdk-sdkbas.png
index e4fcbf90..0baf4b36 100644
--- a/screenshots/86box/sdk-sdkbas.png
+++ b/screenshots/86box/sdk-sdkbas.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c8e1ec94de567e5720ad98859d35615b94e8407ecb7f360433024884925c7b44
-size 16835
+oid sha256:901624bb829f6d3183a65a9fe5e07574135feee06bc875fb38c758a419b068e8
+size 16723
diff --git a/screenshots/86box/sdk-sdkgreet.png b/screenshots/86box/sdk-sdkgreet.png
index 50ba0573..bb1f7484 100644
--- a/screenshots/86box/sdk-sdkgreet.png
+++ b/screenshots/86box/sdk-sdkgreet.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:963ffc7f70050d21d40be5d6212dc8cd74dcb0ed55dd80811a93c4f1a80d35a2
-size 16847
+oid sha256:aa33e5fbe856662b8a15e73c7ff774b301454c3084df4876e1bbf651d3c25fd3
+size 16838
diff --git a/screenshots/86box/sdk-sdktbx.png b/screenshots/86box/sdk-sdktbx.png
index 6b5272d7..24260f82 100644
--- a/screenshots/86box/sdk-sdktbx.png
+++ b/screenshots/86box/sdk-sdktbx.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ddbac3d1aa96225dcb75dbb31a10d3be96a14ea7a143b7929c1dfeb984dcf6a0
-size 23525
+oid sha256:fb2e99f81d2fd46ebf15c15fbd18dc40290b12aef056ce503bee8d34c72e5db8
+size 23699
diff --git a/screenshots/86box/sdkgame-sdkgame.png b/screenshots/86box/sdkgame-sdkgame.png
index 1674f772..6d68fe11 100644
--- a/screenshots/86box/sdkgame-sdkgame.png
+++ b/screenshots/86box/sdkgame-sdkgame.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0cc69633147d663f0529171d4ecbf484af883910f88afff826da778f590f4ecb
-size 14704
+oid sha256:8fac6dcd60199c3d55a21dcf16fac1ea45576356f2616bc7c7a08264ca6d2e7c
+size 14519
diff --git a/screenshots/86box/software-sw-done.png b/screenshots/86box/software-sw-done.png
index 91354225..57f4b35c 100644
--- a/screenshots/86box/software-sw-done.png
+++ b/screenshots/86box/software-sw-done.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b67f1571788b597396f49cfd195c8d5e7189726deddefa48f6f2f814ce5683c0
-size 16954
+oid sha256:fb87800e15edba0b804e70e95f56656184d4a949b8e78afbe8bc8fcfff056889
+size 16948
diff --git a/screenshots/86box/software-sw-plan.png b/screenshots/86box/software-sw-plan.png
index 441de340..8e06251a 100644
--- a/screenshots/86box/software-sw-plan.png
+++ b/screenshots/86box/software-sw-plan.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8f09da4c5d11fbe26a2c29aa35ab702dcced4dbbe764a631548e6ea2ff465504
-size 18411
+oid sha256:0d2eea6d6d92b615c178e12e004fbf4d74a029322e6a9634c1ec90a033b28105
+size 18391
diff --git a/screenshots/86box/sql-sqlsmk.png b/screenshots/86box/sql-sqlsmk.png
index aecc479c..91eecc55 100644
--- a/screenshots/86box/sql-sqlsmk.png
+++ b/screenshots/86box/sql-sqlsmk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:929127846378dfc4a48ec57d28bad1491a02d51fbeb4ae19212561e8a4bbb7e3
-size 16921
+oid sha256:6f0385872d52d227121faf4e1453cfbacae6e6be7b9568743fae5d9e53eeed3f
+size 16932
diff --git a/screenshots/86box/ucdemo-ucdemo.png b/screenshots/86box/ucdemo-ucdemo.png
index a007a82e..410cb0bc 100644
--- a/screenshots/86box/ucdemo-ucdemo.png
+++ b/screenshots/86box/ucdemo-ucdemo.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:788aadd3810a1f056f07ae5c8bbdfe0413439dbedd403672a40e719aad43a654
-size 15947
+oid sha256:942eaa7a6c067a823b30d0a337c18638964bf01ac8c21861f46db4a509b628f7
+size 15850
diff --git a/screenshots/86box/ucide-ucide-ds.png b/screenshots/86box/ucide-ucide-ds.png
index c6c86b18..26601f99 100644
--- a/screenshots/86box/ucide-ucide-ds.png
+++ b/screenshots/86box/ucide-ucide-ds.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c377444a4b6e7369fa35a92e916c3f86d90a3d95e961f16b3c8471464776712a
-size 31429
+oid sha256:98eea34512e3f348a3041b12b6ca7ef522f9edcabe494a0f15d2a8e896cf9a1e
+size 31561
diff --git a/screenshots/86box/ucide-ucide-ru.png b/screenshots/86box/ucide-ucide-ru.png
index b8aefcce..bcc692f5 100644
--- a/screenshots/86box/ucide-ucide-ru.png
+++ b/screenshots/86box/ucide-ucide-ru.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1eef572ce0f038dcadc4f2c4acefca46008c44589362cba01d43c3df697f2034
-size 25351
+oid sha256:1d7326ed86366bbe1b3359ee7235ba5f6e538c613bccd0b68c8e5bae4e45e7d7
+size 25273
diff --git a/screenshots/86box/ucide-ucide-tb.png b/screenshots/86box/ucide-ucide-tb.png
index aff9255b..646e96b4 100644
--- a/screenshots/86box/ucide-ucide-tb.png
+++ b/screenshots/86box/ucide-ucide-tb.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2a8dc683b59b689949ec7da17d049d42f8bbb16ba7ba7694d96546a747ca6462
-size 31580
+oid sha256:77eaddc942ed51d860f97a5701db753b7010a68fa5d74088922e1d10cc5ef039
+size 31708
diff --git a/screenshots/86box/video-v-800.png b/screenshots/86box/video-v-800.png
index 3ad661b2..e5db0736 100644
--- a/screenshots/86box/video-v-800.png
+++ b/screenshots/86box/video-v-800.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:60fd02f0d1a4a0dfcf9a542c2ee25d352a6aac068ea68d06450e671df055bd54
-size 26736
+oid sha256:116b45917198465fb7b0d5754b8ad6bd051fe7973513df6b2bec798b4f123484
+size 27408
diff --git a/screenshots/86box/video-v-pick.png b/screenshots/86box/video-v-pick.png
index 2d0f7057..c82dea0e 100644
--- a/screenshots/86box/video-v-pick.png
+++ b/screenshots/86box/video-v-pick.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:299f307b02e46be9d74b27c141734049f39f710e6cb11036d6065320c068cee7
-size 20592
+oid sha256:fd390e8064ab4e69f1e153d2a307dc6573a7f10838a8dd91fc9ede61490e263c
+size 20877
diff --git a/screenshots/86box/winops-w-back.png b/screenshots/86box/winops-w-back.png
index be07800e..30e1daa5 100644
--- a/screenshots/86box/winops-w-back.png
+++ b/screenshots/86box/winops-w-back.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7f11737da501c34b2ff60c4aa9793bbfb61d8249fa1963980baddafb039d39c6
-size 13351
+oid sha256:b03a051f539425d65c03f55ea55d268b7b5e5bc67a0a5d6a9adf91476a077c4b
+size 13342
diff --git a/screenshots/86box/winops-w-min.png b/screenshots/86box/winops-w-min.png
index 0764e0ab..df5efc3d 100644
--- a/screenshots/86box/winops-w-min.png
+++ b/screenshots/86box/winops-w-min.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9d27acebba6cb4ec6c56b2f4cc243456fead36d108df990672b1ed977038c4b4
-size 18646
+oid sha256:15ab809411b9700e376cbf85fac70eea54048454944da138591b11a2d0371a30
+size 18613
diff --git a/screenshots/86box/winops-w-move.png b/screenshots/86box/winops-w-move.png
index b47f2f37..8eace288 100644
--- a/screenshots/86box/winops-w-move.png
+++ b/screenshots/86box/winops-w-move.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c521f421f2386015fde0b8549294d22b2dc41da962bf0fd1a6c54d6d8279aa1f
-size 13484
+oid sha256:47c8b321c4d4195a5ceb0d714b8060f0dac35ecea4b823dac23c2c6008d33d03
+size 13472
diff --git a/screenshots/86box/winops-w-rest.png b/screenshots/86box/winops-w-rest.png
index e7af769f..290eacc3 100644
--- a/screenshots/86box/winops-w-rest.png
+++ b/screenshots/86box/winops-w-rest.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:65dc513b0eb279c182bebb578475fb679a276264fc0f10ea4bf2ee11566f7689
-size 13362
+oid sha256:fc09124460ff651d26956a59f0c72338efa7edb38561b2bd535513dcfdf0fb59
+size 13376
diff --git a/screenshots/86box/winops-w-size.png b/screenshots/86box/winops-w-size.png
index 6105bc85..750ba2c9 100644
--- a/screenshots/86box/winops-w-size.png
+++ b/screenshots/86box/winops-w-size.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ddb01ff055fb3275275546a598f70d27f1db2ea4a88dcccbc0cb9256924a49e6
-size 13319
+oid sha256:69dd279e18f40ef4a337f2e1455992924c7b465d39692060b4fb761e91edcf5e
+size 13329
diff --git a/screenshots/dosbox/addons-addon-no.png b/screenshots/dosbox/addons-addon-no.png
index 07e38fee..3b255300 100644
--- a/screenshots/dosbox/addons-addon-no.png
+++ b/screenshots/dosbox/addons-addon-no.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:87a79d1bb9a57eba5bc045892b359d85c20a332a95de259c233ec36b1de3ef2e
-size 18229
+oid sha256:ff64f081e2927482a5cc2dbf8470b9bf95da1d46ff64a7c146f5c1898020a2e1
+size 18045
diff --git a/screenshots/dosbox/apps-clock.png b/screenshots/dosbox/apps-clock.png
index e4a58726..27767c4b 100644
--- a/screenshots/dosbox/apps-clock.png
+++ b/screenshots/dosbox/apps-clock.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fb0cf0a293985c16f50cddb6287044fd462a35f99a88bba055ea9f4e4f9b403d
-size 18018
+oid sha256:018eafb9168ef5da0224b0ef56dc3ef8f2ad150a3ddb2c5f0e6445583cc2b980
+size 18024
diff --git a/screenshots/dosbox/apps-cpanel.png b/screenshots/dosbox/apps-cpanel.png
index eb7203d1..7c2c7af8 100644
--- a/screenshots/dosbox/apps-cpanel.png
+++ b/screenshots/dosbox/apps-cpanel.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bf51593a279f8352dde3ef83d94942cb7d82d3522c91d2a3b43758674bd3535
-size 16427
+oid sha256:88949a0a11f46387060a4e4a5faaca763bd1fd8da35bae21bce78e78e74b3c02
+size 15931
diff --git a/screenshots/dosbox/apps-desktop.png b/screenshots/dosbox/apps-desktop.png
index 9fc09f1a..a090d4e6 100644
--- a/screenshots/dosbox/apps-desktop.png
+++ b/screenshots/dosbox/apps-desktop.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0a697ed80887720ffc0e258516c4a59068e86521f79c4d9d777e5b6d703a73cf
-size 18588
+oid sha256:a12c2d2fcac3b118c581b7cab7237adb773677a002781b4819728fa927ed9130
+size 18563
diff --git a/screenshots/dosbox/apps-dvxbasic.png b/screenshots/dosbox/apps-dvxbasic.png
index 16b7bf11..3566300e 100644
--- a/screenshots/dosbox/apps-dvxbasic.png
+++ b/screenshots/dosbox/apps-dvxbasic.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c5b9aedac500ad8ff4cbbb7b59bf49751f4bde23f6e8f8a3ae9f0f34424307a1
-size 18393
+oid sha256:f36dae98688d62bd81a87e6129466223b5604ad6100b0b03bf358c72c0c62e54
+size 18388
diff --git a/screenshots/dosbox/apps-dvxhelp.png b/screenshots/dosbox/apps-dvxhelp.png
index 1bd4ab98..878b2d82 100644
--- a/screenshots/dosbox/apps-dvxhelp.png
+++ b/screenshots/dosbox/apps-dvxhelp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5e2c8e671952f6ddfb618246e4b20fb37acc9dc23aa3f9a1616a188a78f3a974
-size 21764
+oid sha256:4e557895dd4541eea4a1ff516cf44c08beb425ac7dc01359ddaaa914ee873430
+size 21777
diff --git a/screenshots/dosbox/apps-iconed.png b/screenshots/dosbox/apps-iconed.png
index 658cb98c..a9b291b1 100644
--- a/screenshots/dosbox/apps-iconed.png
+++ b/screenshots/dosbox/apps-iconed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:35e2e173eb0155a1799f985effc2d4100ea31be802ad7e312d5ca66751601fa0
-size 14832
+oid sha256:9e4cdc9de6d03c087b222bd6bb1a134718847b9f804098fb16e8c3f8c2608de5
+size 14813
diff --git a/screenshots/dosbox/apps-imgview.png b/screenshots/dosbox/apps-imgview.png
index 65619ad3..b6c84679 100644
--- a/screenshots/dosbox/apps-imgview.png
+++ b/screenshots/dosbox/apps-imgview.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20216cfc2f5c629596ee18af24de18501b2431af8ee731ceb5be4427e219661b
-size 12892
+oid sha256:9861190daaec8871a3fbab7bd5a3913ccd8a1e7fdfe990900ebc4013ce6a763d
+size 12874
diff --git a/screenshots/dosbox/apps-notepad.png b/screenshots/dosbox/apps-notepad.png
index f80814d6..c3ac5869 100644
--- a/screenshots/dosbox/apps-notepad.png
+++ b/screenshots/dosbox/apps-notepad.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1ffe8d8b71867ed7b7c676213a0b715f4de49565e1e83f0694466d2d701673cb
-size 13754
+oid sha256:3cc900212b164e48a971c16706b2a1f50319dbc1936e2864c5550e07ab6c631c
+size 13751
diff --git a/screenshots/dosbox/apps-player.png b/screenshots/dosbox/apps-player.png
new file mode 100644
index 00000000..7305aa21
--- /dev/null
+++ b/screenshots/dosbox/apps-player.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52310f0edc3d1689175593eddb3e0a06345b933605ebecf5d47ca63987afe488
+size 13332
diff --git a/screenshots/dosbox/apps-resedit.png b/screenshots/dosbox/apps-resedit.png
index 4becf7fd..10bc49a9 100644
--- a/screenshots/dosbox/apps-resedit.png
+++ b/screenshots/dosbox/apps-resedit.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3fc86dd4c3acc583cdb962e02b1c3a563a67bd5d84c0ed1a19314937ab191c51
-size 14168
+oid sha256:fffa0df7faae7202429cf3c5e782703f68ceb804bb52177c0088f0f2c67e3288
+size 14176
diff --git a/screenshots/dosbox/banked-bk-back.png b/screenshots/dosbox/banked-bk-back.png
index 7976b145..9c404784 100644
--- a/screenshots/dosbox/banked-bk-back.png
+++ b/screenshots/dosbox/banked-bk-back.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3425e4a35d856bef29f68f9732c7fad18669dfd3ebd1178835c0191827029cfb
-size 18540
+oid sha256:28c79ae8f56c95b2bcfc337305fd2503a44b4a11cd21cae8bb2d8075288ce389
+size 18520
diff --git a/screenshots/dosbox/banked-bk-desk.png b/screenshots/dosbox/banked-bk-desk.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/dosbox/banked-bk-desk.png
+++ b/screenshots/dosbox/banked-bk-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/dosbox/banked-bk-drag.png b/screenshots/dosbox/banked-bk-drag.png
index 51540044..683e86c0 100644
--- a/screenshots/dosbox/banked-bk-drag.png
+++ b/screenshots/dosbox/banked-bk-drag.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d815eedadc4035fea2027775eb340ae6b33b2b4f9bf90115ddacfe565096f7d9
-size 15053
+oid sha256:914ffc97631ee498aa1422cde129c85adfd627f1b1ab9b0957ade2e4ec08dd34
+size 14781
diff --git a/screenshots/dosbox/banked-bk-note.png b/screenshots/dosbox/banked-bk-note.png
index f8d5861d..c3ac5869 100644
--- a/screenshots/dosbox/banked-bk-note.png
+++ b/screenshots/dosbox/banked-bk-note.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:cbbc99f6095370b23c0cda96486df13fb5c1e6654ff371b9a67b8fe62a116eda
-size 13750
+oid sha256:3cc900212b164e48a971c16706b2a1f50319dbc1936e2864c5550e07ab6c631c
+size 13751
diff --git a/screenshots/dosbox/boot-boot.png b/screenshots/dosbox/boot-boot.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/dosbox/boot-boot.png
+++ b/screenshots/dosbox/boot-boot.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/dosbox/cpanel-cp-color.png b/screenshots/dosbox/cpanel-cp-color.png
index 5261386c..afd39ff6 100644
--- a/screenshots/dosbox/cpanel-cp-color.png
+++ b/screenshots/dosbox/cpanel-cp-color.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:660bf211134751affcd3a05e3d5e8e8c297cda750a417873a642c2ee72861542
-size 18266
+oid sha256:e66c7fb68c7e1ae2a6e0f140254fab01fa9d355e0cd714e97982666b27968420
+size 18719
diff --git a/screenshots/dosbox/cpanel-cp-desk.png b/screenshots/dosbox/cpanel-cp-desk.png
index c912aaa5..1bc6c33c 100644
--- a/screenshots/dosbox/cpanel-cp-desk.png
+++ b/screenshots/dosbox/cpanel-cp-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:97022e5d29c1c26e7d47625d3fb54121173c54b9f58f884a17c451a555a3b3b3
-size 14436
+oid sha256:9030c3b8ed8d9e54aed50f1ab5d56a015e7938ea038ee487e3f188235de8e699
+size 15016
diff --git a/screenshots/dosbox/cpanel-cp-joy.png b/screenshots/dosbox/cpanel-cp-joy.png
index a2a740d4..0e0b2aaa 100644
--- a/screenshots/dosbox/cpanel-cp-joy.png
+++ b/screenshots/dosbox/cpanel-cp-joy.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eb4bdad8d10caa593d3cfab42126f537714a1d2ea70e06ca64656e26fbecb785
-size 16334
+oid sha256:a27192d2ccf98d8d10ff977d4ed3c8bcda6a911c1f9578001b1219a02ade9346
+size 15833
diff --git a/screenshots/dosbox/cpanel-cp-mouse.png b/screenshots/dosbox/cpanel-cp-mouse.png
index eb7203d1..7c2c7af8 100644
--- a/screenshots/dosbox/cpanel-cp-mouse.png
+++ b/screenshots/dosbox/cpanel-cp-mouse.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bf51593a279f8352dde3ef83d94942cb7d82d3522c91d2a3b43758674bd3535
-size 16427
+oid sha256:88949a0a11f46387060a4e4a5faaca763bd1fd8da35bae21bce78e78e74b3c02
+size 15931
diff --git a/screenshots/dosbox/cpanel-cp-net.png b/screenshots/dosbox/cpanel-cp-net.png
index 8a0078a8..c05764ef 100644
--- a/screenshots/dosbox/cpanel-cp-net.png
+++ b/screenshots/dosbox/cpanel-cp-net.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:27f0996dda3d29cd134257c40b19a6ef215ce45162999250c704004570995426
-size 16199
+oid sha256:11daa8703e086e9c3b3aa60d075616204bf2d9f7d228ffa86a0538dcbba25b0f
+size 15677
diff --git a/screenshots/dosbox/cpanel-cp-print.png b/screenshots/dosbox/cpanel-cp-print.png
index 6b3a71c0..10aa46db 100644
--- a/screenshots/dosbox/cpanel-cp-print.png
+++ b/screenshots/dosbox/cpanel-cp-print.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e35a2a6c663538c45692d66e5335d808e323bda1c5f1c53e51f1124a2e6cba85
-size 15909
+oid sha256:f36a45f38c72c9997d4b91ae8166beb9d557d304d2c298c718906dd5c0fb7f75
+size 16993
diff --git a/screenshots/dosbox/cpanel-cp-sound.png b/screenshots/dosbox/cpanel-cp-sound.png
index 8b22b99b..0d2ceba7 100644
--- a/screenshots/dosbox/cpanel-cp-sound.png
+++ b/screenshots/dosbox/cpanel-cp-sound.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8af0d409e9b907747748030acb9e6974893bdd3a426d142d679a023bfd1ac6ce
-size 17643
+oid sha256:685f0c278368b5a74e5aa3710c0dc03a71b2f8f0bf1d2519309957090d7bcbbf
+size 18726
diff --git a/screenshots/dosbox/cpanel-cp-types.png b/screenshots/dosbox/cpanel-cp-types.png
index 1e405480..0b7d27dd 100644
--- a/screenshots/dosbox/cpanel-cp-types.png
+++ b/screenshots/dosbox/cpanel-cp-types.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:53601bf8609488ed1a85b388e405854ec038f216b2cf6d4350d749be05195dd5
-size 23172
+oid sha256:2749e1d0ca5ca17f97455a0e6a4200d587f3b0961b135ce80fb37ee586325ef8
+size 24863
diff --git a/screenshots/dosbox/cpanel-cp-video.png b/screenshots/dosbox/cpanel-cp-video.png
index e9b7cb6f..051600cc 100644
--- a/screenshots/dosbox/cpanel-cp-video.png
+++ b/screenshots/dosbox/cpanel-cp-video.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d4cc901354dfe241498eab49317b5c9c91603c4c582209dbec21973b61054c7b
-size 20198
+oid sha256:f2fb899519c6fa3f2ece1475e58d57e038cce4903d1e43dc512327f488fe0432
+size 20329
diff --git a/screenshots/dosbox/dosprog-dp-after.png b/screenshots/dosbox/dosprog-dp-after.png
new file mode 100644
index 00000000..15ff8dc1
--- /dev/null
+++ b/screenshots/dosbox/dosprog-dp-after.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/dosbox/dosprog-dp-ask.png b/screenshots/dosbox/dosprog-dp-ask.png
new file mode 100644
index 00000000..4fa453f2
--- /dev/null
+++ b/screenshots/dosbox/dosprog-dp-ask.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d1eac79fc717600f2dbd52fd23090afb5d214ba255bc962e8c447394fc0da496
+size 17445
diff --git a/screenshots/dosbox/dosprog-dp-back.png b/screenshots/dosbox/dosprog-dp-back.png
new file mode 100644
index 00000000..15ff8dc1
--- /dev/null
+++ b/screenshots/dosbox/dosprog-dp-back.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/dosbox/exit-cancel.png b/screenshots/dosbox/exit-cancel.png
index e565888c..06fbf701 100644
--- a/screenshots/dosbox/exit-cancel.png
+++ b/screenshots/dosbox/exit-cancel.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f7508aa18520dc0675e54492895fe3e348ff659bf52b6c0c7f2efe3e6035c38d
-size 14342
+oid sha256:60f5b5910fb8aae98bc0beb01d144a15fc8622c9966f0ff6d4bc4bf894e5517e
+size 14349
diff --git a/screenshots/dosbox/exit-savechg.png b/screenshots/dosbox/exit-savechg.png
index 74b3e466..6dcf3389 100644
--- a/screenshots/dosbox/exit-savechg.png
+++ b/screenshots/dosbox/exit-savechg.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c375e0886534255d93d2ae1ecf32683d0036cfbfb628320140a7a5fc9b019695
-size 14348
+oid sha256:13ec25fc0b583981119747baa7fa24e7e97429106d1c5861be1494304ec29e6b
+size 14350
diff --git a/screenshots/dosbox/f1-h-basdem.png b/screenshots/dosbox/f1-h-basdem.png
index f178ed94..7593a315 100644
--- a/screenshots/dosbox/f1-h-basdem.png
+++ b/screenshots/dosbox/f1-h-basdem.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f5b3bc24e986cc704e74e6b7dd72b8f42ddc87c9a88e7c535d5c5d94d6c2c51c
-size 20728
+oid sha256:a360f2333c0d5f40cea16b7da81344b99ea18bd23e5c806a97b2d2443b179d59
+size 20718
diff --git a/screenshots/dosbox/f1-h-clock.png b/screenshots/dosbox/f1-h-clock.png
index e293aa1d..3c960f64 100644
--- a/screenshots/dosbox/f1-h-clock.png
+++ b/screenshots/dosbox/f1-h-clock.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0ce6bf30393be569792f8c906b1fe1506ee09aa304d58a1c5ac2c233bfd28d28
-size 18940
+oid sha256:47b038f1dacb57e532944605cc0c1ef39802378553953fd8ce052049876c8aaf
+size 18956
diff --git a/screenshots/dosbox/f1-h-cpanel.png b/screenshots/dosbox/f1-h-cpanel.png
index a75dbb95..775690f0 100644
--- a/screenshots/dosbox/f1-h-cpanel.png
+++ b/screenshots/dosbox/f1-h-cpanel.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e6fe705e41b7a4dc8b969945e455b2d79fd32d517f78b829ded6cfd44324db70
-size 22663
+oid sha256:30800dde17d30be96cbd5e8de2072d710eb89c88a2f49fa92136ed0d983affa1
+size 23161
diff --git a/screenshots/dosbox/f1-h-iconed.png b/screenshots/dosbox/f1-h-iconed.png
index a2a4f8da..c88408fd 100644
--- a/screenshots/dosbox/f1-h-iconed.png
+++ b/screenshots/dosbox/f1-h-iconed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ab09163d20f674e0bd2effd008ea3489f5b3c223355aa1c4790ec8f11edbe2a3
-size 17919
+oid sha256:3d48d0d2dcb8e4d9540d23830147904d5d23e35f02ccaf4155ed0f71e00b57a2
+size 17930
diff --git a/screenshots/dosbox/f1-h-imgvie.png b/screenshots/dosbox/f1-h-imgvie.png
index 2997b10f..2f5e2f34 100644
--- a/screenshots/dosbox/f1-h-imgvie.png
+++ b/screenshots/dosbox/f1-h-imgvie.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:551335eede657c3a0ba3d34ea23d8134214f3b7914510cdd02d417f14cd949dc
-size 19273
+oid sha256:20a69aa1199ae81c20ce4f7a506bb6f4cae993bf87dce5e363aa1dac097f8081
+size 19288
diff --git a/screenshots/dosbox/f1-h-notepa.png b/screenshots/dosbox/f1-h-notepa.png
index edb6ee26..d7321644 100644
--- a/screenshots/dosbox/f1-h-notepa.png
+++ b/screenshots/dosbox/f1-h-notepa.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8a377752193cd085742ecf9a616c990e897189c20443ca754434a3fb036c47d3
-size 18895
+oid sha256:f88980948aa279f05b4e22b59c98fb86299259bf5e0c8bf63ed397656cd136f7
+size 18896
diff --git a/screenshots/dosbox/f1-h-player.png b/screenshots/dosbox/f1-h-player.png
new file mode 100644
index 00000000..f02e0686
--- /dev/null
+++ b/screenshots/dosbox/f1-h-player.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:92aedcc272e3e121db4c7f83114d41465372ad0c0d1a0869eb6013efeb502073
+size 19021
diff --git a/screenshots/dosbox/f1-h-progmn.png b/screenshots/dosbox/f1-h-progmn.png
index d01028fe..ad036001 100644
--- a/screenshots/dosbox/f1-h-progmn.png
+++ b/screenshots/dosbox/f1-h-progmn.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:63826ce353d7dbb386da4d50f7ee526ab96d324db12761727fe418a9b8b857a5
-size 21918
+oid sha256:21085ae6d3c9049c88bdb7c0704c643722af5f6e92479264915f295e9b377c99
+size 22021
diff --git a/screenshots/dosbox/f1-h-resedi.png b/screenshots/dosbox/f1-h-resedi.png
index 3e519eee..3ce29b45 100644
--- a/screenshots/dosbox/f1-h-resedi.png
+++ b/screenshots/dosbox/f1-h-resedi.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ae20d685ccbd0230a20c48efa684c0fde6ae1eed48aeaf1a6fedc81b4c2ede50
-size 20875
+oid sha256:1a9e3cac4489b2633d731e6ea969c36f641f1404502d73d143d9a6befe02ac00
+size 20888
diff --git a/screenshots/dosbox/filedlg-fd-back.png b/screenshots/dosbox/filedlg-fd-back.png
index f579cfe5..9846da68 100644
--- a/screenshots/dosbox/filedlg-fd-back.png
+++ b/screenshots/dosbox/filedlg-fd-back.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7382ad12f6411a8448df45ea184d8306423a51e0fbd8bb2d46beeaa34b43ac29
-size 15234
+oid sha256:c7a84dbadd39c80e002a54630e71c97343f4f3def3ac0d5dc33eb461a2faa924
+size 15247
diff --git a/screenshots/dosbox/filedlg-fd-list.png b/screenshots/dosbox/filedlg-fd-list.png
index eee89a60..7dccbfe0 100644
--- a/screenshots/dosbox/filedlg-fd-list.png
+++ b/screenshots/dosbox/filedlg-fd-list.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6e5d6348e54496727f261d2760cbc13a37de24c8a96a20741c479dcb25602c90
-size 15447
+oid sha256:7dd6b181c9e9144228a7f89b94b670543083ec7018ae9f68e10ddcfc45e612c2
+size 15451
diff --git a/screenshots/dosbox/filedlg-fd-nordy.png b/screenshots/dosbox/filedlg-fd-nordy.png
index 383cd199..18d68769 100644
--- a/screenshots/dosbox/filedlg-fd-nordy.png
+++ b/screenshots/dosbox/filedlg-fd-nordy.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b6b6cd04bf85ff797c3be50ecff2fe0a3efc61e19ea397fc00c31ec7369f1111
-size 15443
+oid sha256:797486eee6e9cc31ea083b929ccc5262d1a612bda098c5f655833e5eec0318a1
+size 15453
diff --git a/screenshots/dosbox/filedlg-fd-open.png b/screenshots/dosbox/filedlg-fd-open.png
index 56bee79c..1adec5b7 100644
--- a/screenshots/dosbox/filedlg-fd-open.png
+++ b/screenshots/dosbox/filedlg-fd-open.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fa786de9621f9d9a39373e800572fbbce9807a7c432f0330572908f602a2060a
-size 15205
+oid sha256:87706be4507c6378bb3b126ec0ba89eb6d86bb1a07e822571d6cd6cecc45ce11
+size 15176
diff --git a/screenshots/dosbox/fullmode-fm-desk.png b/screenshots/dosbox/fullmode-fm-desk.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/dosbox/fullmode-fm-desk.png
+++ b/screenshots/dosbox/fullmode-fm-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/dosbox/fullscr-fs-desk.png b/screenshots/dosbox/fullscr-fs-desk.png
index c2e26be0..7b0f9490 100644
--- a/screenshots/dosbox/fullscr-fs-desk.png
+++ b/screenshots/dosbox/fullscr-fs-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:adb402decf2e7dc5f420b0f5d479eaaa0cec605c2e4b58ed44ab6575165a0fd5
-size 17558
+oid sha256:29abf7f02187f54f333e23f40a4f4fda4e3d6e306e8153e688553af9209df357
+size 17506
diff --git a/screenshots/dosbox/hang-ended.png b/screenshots/dosbox/hang-ended.png
index 903e1f9c..e8dd2143 100644
--- a/screenshots/dosbox/hang-ended.png
+++ b/screenshots/dosbox/hang-ended.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20c8a70d7547ebefa20be0b9c5cb5e9a1be589abae0e0eea1aba70977c3fbc61
-size 15158
+oid sha256:a8e96d2fa598cf563ef80728c0201e51306b0eb9d7ab4169107d33ac62f279bb
+size 15155
diff --git a/screenshots/dosbox/hang-taskmgr.png b/screenshots/dosbox/hang-taskmgr.png
index 3d00f119..3aba658d 100644
--- a/screenshots/dosbox/hang-taskmgr.png
+++ b/screenshots/dosbox/hang-taskmgr.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b4c43dbc18bb351f0053d1862ce9ff76815cb11db0520454868b684f8efce1cc
-size 16206
+oid sha256:45d1606cf7d9d2da3dc9f7abcccce1f1bea6b55b95fe4a27f0b0c618a8884065
+size 16019
diff --git a/screenshots/dosbox/install-in-desk.png b/screenshots/dosbox/install-in-desk.png
index 00444c34..7fbf6e91 100644
--- a/screenshots/dosbox/install-in-desk.png
+++ b/screenshots/dosbox/install-in-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:78ef264c66bdc27492ebbfc01453f7face1b2fbca1f9eb0621836481aae7c499
-size 18548
+oid sha256:1fe45de792395dc0311670f3607a216f3944928d61c85e4aa55d0349f2379e09
+size 18601
diff --git a/screenshots/dosbox/menus-m-altdn.png b/screenshots/dosbox/menus-m-altdn.png
index 95e69470..a3180645 100644
--- a/screenshots/dosbox/menus-m-altdn.png
+++ b/screenshots/dosbox/menus-m-altdn.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0e1679bdc50865c2087f3687a210a7677c7175c07b9d050bcf6e04dc21d3d39e
-size 16816
+oid sha256:17dbbfe03c1e4e37960e82731d72920ecd7a1bad53ad51031a4b3ff6e708e8a7
+size 16401
diff --git a/screenshots/dosbox/menus-m-altup.png b/screenshots/dosbox/menus-m-altup.png
index 290bda7e..104e2339 100644
--- a/screenshots/dosbox/menus-m-altup.png
+++ b/screenshots/dosbox/menus-m-altup.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2bd9800a5cf14a50f770c69b5a785724482fdb62aac9ee7de8069aa9c16eb2ed
-size 16426
+oid sha256:4746eeae2ccbd52434df49c45e9045fc5c8a47f7a64ce0ab735c2ea62fb84571
+size 15971
diff --git a/screenshots/dosbox/menus-m-design.png b/screenshots/dosbox/menus-m-design.png
index 65a83dc5..d54d6915 100644
--- a/screenshots/dosbox/menus-m-design.png
+++ b/screenshots/dosbox/menus-m-design.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b520534eccc14ec24b1826a4d0ef7fe0c9e7d56f86befef504de05d7faa25b46
-size 23621
+oid sha256:c1eeb4d3e4cb61600af378d7f36c0f96290f74fe2da8fa14066ef8e779d1459c
+size 23804
diff --git a/screenshots/dosbox/menus-m-drop.png b/screenshots/dosbox/menus-m-drop.png
index 95e69470..a3180645 100644
--- a/screenshots/dosbox/menus-m-drop.png
+++ b/screenshots/dosbox/menus-m-drop.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0e1679bdc50865c2087f3687a210a7677c7175c07b9d050bcf6e04dc21d3d39e
-size 16816
+oid sha256:17dbbfe03c1e4e37960e82731d72920ecd7a1bad53ad51031a4b3ff6e708e8a7
+size 16401
diff --git a/screenshots/dosbox/menus-m-file.png b/screenshots/dosbox/menus-m-file.png
index bedb5c3d..62513274 100644
--- a/screenshots/dosbox/menus-m-file.png
+++ b/screenshots/dosbox/menus-m-file.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5805a5feb76aa404a93656e174b878dbce68fea3cb26a178b2ef6803e864611c
-size 14731
+oid sha256:e2197b06a2e6ced93066e8a8c96b085866685af9e117284a4e7061d547156633
+size 14721
diff --git a/screenshots/dosbox/menus-m-run.png b/screenshots/dosbox/menus-m-run.png
index 08415ab8..309d2dc5 100644
--- a/screenshots/dosbox/menus-m-run.png
+++ b/screenshots/dosbox/menus-m-run.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1cacc50a8de3c2c2594d2ab5f99adce74b3fd124f67c49652767f843a475a5b1
-size 18218
+oid sha256:2661a25a8cfcde559ce0b39092629459bb6e7c8618cbac4c9524da4d7cb3e622
+size 18231
diff --git a/screenshots/dosbox/midiin-rc-midi.png b/screenshots/dosbox/midiin-rc-midi.png
new file mode 100644
index 00000000..4fcb13ba
--- /dev/null
+++ b/screenshots/dosbox/midiin-rc-midi.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cd8a9e6857d3154e1bcbee3ed5e6ee46ff25c49ef23275f5f178f2a1ec437383
+size 16033
diff --git a/screenshots/dosbox/net-netsmk.png b/screenshots/dosbox/net-netsmk.png
index e6ed02ab..c7e8436a 100644
--- a/screenshots/dosbox/net-netsmk.png
+++ b/screenshots/dosbox/net-netsmk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2d559bf9d6b37813421ef3623c2c49427cb3356b0ad887e776f352f84f3fb54b
-size 16779
+oid sha256:6b99df6febae1feac39833afaa1ffb12c28c589632d8af95405fb55effeb3da1
+size 16763
diff --git a/screenshots/dosbox/pardsgn-pardsgn.png b/screenshots/dosbox/pardsgn-pardsgn.png
index ab8ba75f..9da764d4 100644
--- a/screenshots/dosbox/pardsgn-pardsgn.png
+++ b/screenshots/dosbox/pardsgn-pardsgn.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9d93f1f23f8ffc5fedd123ef6156982bd420a237247c7bf2b2e1a40029119945
-size 14439
+oid sha256:99b20a168970a3135ca8f7634a3a5896abc18ba673b33b9e20a2447b41d04d30
+size 14438
diff --git a/screenshots/dosbox/parhlp-parhlp-l.png b/screenshots/dosbox/parhlp-parhlp-l.png
index ba97be84..433432b8 100644
--- a/screenshots/dosbox/parhlp-parhlp-l.png
+++ b/screenshots/dosbox/parhlp-parhlp-l.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1466e4d7946dc061dbb1c3485acb38ee6cbcd0930806a25b48fc355f641a2020
-size 15611
+oid sha256:582f9570cb535cda8be533c31da0f425a8ef42c24e630511b5125b53058086fd
+size 15605
diff --git a/screenshots/dosbox/parhlp-parhlp.png b/screenshots/dosbox/parhlp-parhlp.png
index 72a5f511..be99b909 100644
--- a/screenshots/dosbox/parhlp-parhlp.png
+++ b/screenshots/dosbox/parhlp-parhlp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c114ad172296848ae10b9e4cd85574be604f42018ebb95024199603713ac0fda
-size 15611
+oid sha256:b4428ce7fb858c787b2bdb070c0c16f3d9190785cdb51f082046fa0e94bff114
+size 15603
diff --git a/screenshots/dosbox/parpm-parpm.png b/screenshots/dosbox/parpm-parpm.png
index cb031cc7..6b631e94 100644
--- a/screenshots/dosbox/parpm-parpm.png
+++ b/screenshots/dosbox/parpm-parpm.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:87c7a3bf5f3fdc650ec7d00458828d5dca01d28be6d1ae33ac0b85ccb2023616
-size 17434
+oid sha256:698945834aca4b2659a1f02658f2128f135459f9d28a7e10d4d19ce5c22c846d
+size 17471
diff --git a/screenshots/dosbox/paths-paths.png b/screenshots/dosbox/paths-paths.png
index 2b823c9e..879cd012 100644
--- a/screenshots/dosbox/paths-paths.png
+++ b/screenshots/dosbox/paths-paths.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:58503a736c280538316a7afa3e0d14485397b3122fc68a04a4ad94515662379d
-size 16660
+oid sha256:53ad7a1a3d0f2b1838d3f7544ffd8c82a39c3763defa4058cf7b6c331b82f132
+size 16687
diff --git a/screenshots/dosbox/player-p-mid.png b/screenshots/dosbox/player-p-mid.png
new file mode 100644
index 00000000..ee4771e1
--- /dev/null
+++ b/screenshots/dosbox/player-p-mid.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb2c251bc81190cf753890c9acb4b528d5637a8b3029dc6c7d37d17d5c8f68a3
+size 14998
diff --git a/screenshots/dosbox/player-p-mod.png b/screenshots/dosbox/player-p-mod.png
new file mode 100644
index 00000000..eee36d8d
--- /dev/null
+++ b/screenshots/dosbox/player-p-mod.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d2115cb3af475de499f43b42efaa5ecedcb88ceede12d1238406512fb2c4de71
+size 14530
diff --git a/screenshots/dosbox/player-p-voc.png b/screenshots/dosbox/player-p-voc.png
new file mode 100644
index 00000000..67e72d18
--- /dev/null
+++ b/screenshots/dosbox/player-p-voc.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9375379dc9066aa01d0f857c6387134cba2f2e1d29315babade0d1fe062e63d6
+size 14712
diff --git a/screenshots/dosbox/player-p-wav.png b/screenshots/dosbox/player-p-wav.png
new file mode 100644
index 00000000..8c4ca0b1
--- /dev/null
+++ b/screenshots/dosbox/player-p-wav.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81ab1b214f13c292b7034488d9001d405c9fa9fce3b649c412fb98bb4d5ff9da
+size 13944
diff --git a/screenshots/dosbox/print-print1.png b/screenshots/dosbox/print-print1.png
index c82bc456..64f230bd 100644
--- a/screenshots/dosbox/print-print1.png
+++ b/screenshots/dosbox/print-print1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ad64cbc363a6f7ec0491d1ea4e480ed1b48d2b47c09363c722cd89d2bd5d56ad
-size 14872
+oid sha256:211b7ed636aabb8995a4e3e2deedfe4231aa7a053fba9cd03302013ff3c7d6ca
+size 14867
diff --git a/screenshots/dosbox/record-rc-open.png b/screenshots/dosbox/record-rc-open.png
new file mode 100644
index 00000000..b72ad1d9
--- /dev/null
+++ b/screenshots/dosbox/record-rc-open.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d69371019843a140889ff6b81c8813ae24288caeee4507bee430dcf75e33cd93
+size 15955
diff --git a/screenshots/dosbox/safevid-sv-desk.png b/screenshots/dosbox/safevid-sv-desk.png
index c93e492a..15ff8dc1 100644
--- a/screenshots/dosbox/safevid-sv-desk.png
+++ b/screenshots/dosbox/safevid-sv-desk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:588e46fa25956b91e94945fa2c0ee8f26648a4748da1317a7997b72967defd4e
-size 18426
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/dosbox/saver-sv-back.png b/screenshots/dosbox/saver-sv-back.png
new file mode 100644
index 00000000..15ff8dc1
--- /dev/null
+++ b/screenshots/dosbox/saver-sv-back.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cea09d3eadedea2fcea39c17002b18035d67234c98d346cf3d950d4cf75df82d
+size 18427
diff --git a/screenshots/dosbox/saver-sv-blank.png b/screenshots/dosbox/saver-sv-blank.png
new file mode 100644
index 00000000..cfe14ee9
--- /dev/null
+++ b/screenshots/dosbox/saver-sv-blank.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:db875c74be9d218490440cedf7e28ebd06dd070fd4ceacbf34613e08798982e7
+size 1928
diff --git a/screenshots/dosbox/saver-sv-cpd.png b/screenshots/dosbox/saver-sv-cpd.png
new file mode 100644
index 00000000..d15c8fe4
--- /dev/null
+++ b/screenshots/dosbox/saver-sv-cpd.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:77deb8e3c185672514bdb5ecfd1a0a1e4029530c7b2c9a658281223c3b5b833f
+size 14857
diff --git a/screenshots/dosbox/sdk-h-sdkapp.png b/screenshots/dosbox/sdk-h-sdkapp.png
index 51487527..f55a89f1 100644
--- a/screenshots/dosbox/sdk-h-sdkapp.png
+++ b/screenshots/dosbox/sdk-h-sdkapp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:41b5260accda5a08f0fb80acd96eba344ac23a0375d7b0d08bf03189cfcbda02
-size 16834
+oid sha256:6c5c19fea8dc7918d3a13927feb14e7f2497e7b60a28b0e6df8f069c607c2399
+size 16860
diff --git a/screenshots/dosbox/sdk-sdkapp.png b/screenshots/dosbox/sdk-sdkapp.png
index 323617f2..baac89fd 100644
--- a/screenshots/dosbox/sdk-sdkapp.png
+++ b/screenshots/dosbox/sdk-sdkapp.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:af5d5842a32438d1b385e419e7fbf828e01e6b80ae36f4b22836e754d0473fb7
-size 17648
+oid sha256:64fdda7f9f947dbe9e575486bb3aab472947df0e6952c3814ef37517f035d2a2
+size 17662
diff --git a/screenshots/dosbox/sdk-sdkbas.png b/screenshots/dosbox/sdk-sdkbas.png
index e4fcbf90..0baf4b36 100644
--- a/screenshots/dosbox/sdk-sdkbas.png
+++ b/screenshots/dosbox/sdk-sdkbas.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c8e1ec94de567e5720ad98859d35615b94e8407ecb7f360433024884925c7b44
-size 16835
+oid sha256:901624bb829f6d3183a65a9fe5e07574135feee06bc875fb38c758a419b068e8
+size 16723
diff --git a/screenshots/dosbox/sdk-sdkgreet.png b/screenshots/dosbox/sdk-sdkgreet.png
index 50ba0573..bb1f7484 100644
--- a/screenshots/dosbox/sdk-sdkgreet.png
+++ b/screenshots/dosbox/sdk-sdkgreet.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:963ffc7f70050d21d40be5d6212dc8cd74dcb0ed55dd80811a93c4f1a80d35a2
-size 16847
+oid sha256:aa33e5fbe856662b8a15e73c7ff774b301454c3084df4876e1bbf651d3c25fd3
+size 16838
diff --git a/screenshots/dosbox/sdk-sdktbx.png b/screenshots/dosbox/sdk-sdktbx.png
index 6b5272d7..24260f82 100644
--- a/screenshots/dosbox/sdk-sdktbx.png
+++ b/screenshots/dosbox/sdk-sdktbx.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ddbac3d1aa96225dcb75dbb31a10d3be96a14ea7a143b7929c1dfeb984dcf6a0
-size 23525
+oid sha256:fb2e99f81d2fd46ebf15c15fbd18dc40290b12aef056ce503bee8d34c72e5db8
+size 23699
diff --git a/screenshots/dosbox/software-sw-done.png b/screenshots/dosbox/software-sw-done.png
index 91354225..57f4b35c 100644
--- a/screenshots/dosbox/software-sw-done.png
+++ b/screenshots/dosbox/software-sw-done.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b67f1571788b597396f49cfd195c8d5e7189726deddefa48f6f2f814ce5683c0
-size 16954
+oid sha256:fb87800e15edba0b804e70e95f56656184d4a949b8e78afbe8bc8fcfff056889
+size 16948
diff --git a/screenshots/dosbox/software-sw-plan.png b/screenshots/dosbox/software-sw-plan.png
index 441de340..8e06251a 100644
--- a/screenshots/dosbox/software-sw-plan.png
+++ b/screenshots/dosbox/software-sw-plan.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8f09da4c5d11fbe26a2c29aa35ab702dcced4dbbe764a631548e6ea2ff465504
-size 18411
+oid sha256:0d2eea6d6d92b615c178e12e004fbf4d74a029322e6a9634c1ec90a033b28105
+size 18391
diff --git a/screenshots/dosbox/sql-sqlsmk.png b/screenshots/dosbox/sql-sqlsmk.png
index aecc479c..91eecc55 100644
--- a/screenshots/dosbox/sql-sqlsmk.png
+++ b/screenshots/dosbox/sql-sqlsmk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:929127846378dfc4a48ec57d28bad1491a02d51fbeb4ae19212561e8a4bbb7e3
-size 16921
+oid sha256:6f0385872d52d227121faf4e1453cfbacae6e6be7b9568743fae5d9e53eeed3f
+size 16932
diff --git a/screenshots/dosbox/ucdemo-ucdemo.png b/screenshots/dosbox/ucdemo-ucdemo.png
index a007a82e..410cb0bc 100644
--- a/screenshots/dosbox/ucdemo-ucdemo.png
+++ b/screenshots/dosbox/ucdemo-ucdemo.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:788aadd3810a1f056f07ae5c8bbdfe0413439dbedd403672a40e719aad43a654
-size 15947
+oid sha256:942eaa7a6c067a823b30d0a337c18638964bf01ac8c21861f46db4a509b628f7
+size 15850
diff --git a/screenshots/dosbox/ucide-ucide-ds.png b/screenshots/dosbox/ucide-ucide-ds.png
index c6c86b18..26601f99 100644
--- a/screenshots/dosbox/ucide-ucide-ds.png
+++ b/screenshots/dosbox/ucide-ucide-ds.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c377444a4b6e7369fa35a92e916c3f86d90a3d95e961f16b3c8471464776712a
-size 31429
+oid sha256:98eea34512e3f348a3041b12b6ca7ef522f9edcabe494a0f15d2a8e896cf9a1e
+size 31561
diff --git a/screenshots/dosbox/ucide-ucide-ru.png b/screenshots/dosbox/ucide-ucide-ru.png
index b8aefcce..bcc692f5 100644
--- a/screenshots/dosbox/ucide-ucide-ru.png
+++ b/screenshots/dosbox/ucide-ucide-ru.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1eef572ce0f038dcadc4f2c4acefca46008c44589362cba01d43c3df697f2034
-size 25351
+oid sha256:1d7326ed86366bbe1b3359ee7235ba5f6e538c613bccd0b68c8e5bae4e45e7d7
+size 25273
diff --git a/screenshots/dosbox/ucide-ucide-tb.png b/screenshots/dosbox/ucide-ucide-tb.png
index aff9255b..646e96b4 100644
--- a/screenshots/dosbox/ucide-ucide-tb.png
+++ b/screenshots/dosbox/ucide-ucide-tb.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2a8dc683b59b689949ec7da17d049d42f8bbb16ba7ba7694d96546a747ca6462
-size 31580
+oid sha256:77eaddc942ed51d860f97a5701db753b7010a68fa5d74088922e1d10cc5ef039
+size 31708
diff --git a/screenshots/dosbox/video-v-800.png b/screenshots/dosbox/video-v-800.png
index 7ecc5797..237f1f13 100644
--- a/screenshots/dosbox/video-v-800.png
+++ b/screenshots/dosbox/video-v-800.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:b1c866d754308a03556dbc88d9829173ba968b7493ee3e26ef0d595e66c63947
-size 26979
+oid sha256:c6698d96ca96377e94ad30267d92ac46e66d1e02704e802d9cc905d557063caa
+size 27692
diff --git a/screenshots/dosbox/video-v-pick.png b/screenshots/dosbox/video-v-pick.png
index ddfa2e65..f919cb7f 100644
--- a/screenshots/dosbox/video-v-pick.png
+++ b/screenshots/dosbox/video-v-pick.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:14b9358260721db22e68dde83e99a1ee2618b0e888e7163bdc445711c913750f
-size 20572
+oid sha256:6feb187165573f5514faf88e0a924acd32278a3b9db50728214ad7c51ae2d6a7
+size 20904
diff --git a/screenshots/dosbox/winops-w-back.png b/screenshots/dosbox/winops-w-back.png
index be07800e..30e1daa5 100644
--- a/screenshots/dosbox/winops-w-back.png
+++ b/screenshots/dosbox/winops-w-back.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7f11737da501c34b2ff60c4aa9793bbfb61d8249fa1963980baddafb039d39c6
-size 13351
+oid sha256:b03a051f539425d65c03f55ea55d268b7b5e5bc67a0a5d6a9adf91476a077c4b
+size 13342
diff --git a/screenshots/dosbox/winops-w-min.png b/screenshots/dosbox/winops-w-min.png
index 0764e0ab..df5efc3d 100644
--- a/screenshots/dosbox/winops-w-min.png
+++ b/screenshots/dosbox/winops-w-min.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9d27acebba6cb4ec6c56b2f4cc243456fead36d108df990672b1ed977038c4b4
-size 18646
+oid sha256:15ab809411b9700e376cbf85fac70eea54048454944da138591b11a2d0371a30
+size 18613
diff --git a/screenshots/dosbox/winops-w-move.png b/screenshots/dosbox/winops-w-move.png
index b47f2f37..8eace288 100644
--- a/screenshots/dosbox/winops-w-move.png
+++ b/screenshots/dosbox/winops-w-move.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c521f421f2386015fde0b8549294d22b2dc41da962bf0fd1a6c54d6d8279aa1f
-size 13484
+oid sha256:47c8b321c4d4195a5ceb0d714b8060f0dac35ecea4b823dac23c2c6008d33d03
+size 13472
diff --git a/screenshots/dosbox/winops-w-rest.png b/screenshots/dosbox/winops-w-rest.png
index e7af769f..290eacc3 100644
--- a/screenshots/dosbox/winops-w-rest.png
+++ b/screenshots/dosbox/winops-w-rest.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:65dc513b0eb279c182bebb578475fb679a276264fc0f10ea4bf2ee11566f7689
-size 13362
+oid sha256:fc09124460ff651d26956a59f0c72338efa7edb38561b2bd535513dcfdf0fb59
+size 13376
diff --git a/screenshots/dosbox/winops-w-size.png b/screenshots/dosbox/winops-w-size.png
index 6105bc85..750ba2c9 100644
--- a/screenshots/dosbox/winops-w-size.png
+++ b/screenshots/dosbox/winops-w-size.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ddb01ff055fb3275275546a598f70d27f1db2ea4a88dcccbc0cb9256924a49e6
-size 13319
+oid sha256:69dd279e18f40ef4a337f2e1455992924c7b465d39692060b4fb761e91edcf5e
+size 13329
diff --git a/screenshots/masks/install-su-lic.mask b/screenshots/masks/install-su-lic.mask
new file mode 100644
index 00000000..f635c0f2
--- /dev/null
+++ b/screenshots/masks/install-su-lic.mask
@@ -0,0 +1,2 @@
+# Blinking text caret at the start of the licence text
+77 210 3 18
diff --git a/server/README.md b/server/README.md
index 94782a85..2a2985f8 100644
--- a/server/README.md
+++ b/server/README.md
@@ -121,6 +121,16 @@ Software then lists it first on its Updates page. DVX installs it only when the
signature checks against the key in its own `SYSTEM\KEYS`, and names the add-ons a new ABI would
leave unable to load before anything is installed.
+The same build writes DVX's own add-ons to `dist/addons/` (`src/addons/README.md`): instrument
+banks for MIDI music, kept out of the base install. They are uploaded and approved the same way:
+
+ for dpk in dist/addons/*.DPK; do
+ curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/octet-stream" \
+ --data-binary @"$dpk" https://packages.example.com/v1/upload
+ done
+ calog tools/review.lua -- review.conf approve kpunch.dmxopl 1.11.0
+ calog tools/review.lua -- review.conf approve kpunch.dmxopl3 2.11.0
+
## Configuration
`dvx.conf.example` lists every key with a comment. Beyond singemaster's (`bindHost`, `bindPort`,
diff --git a/server/test/run.sh b/server/test/run.sh
index 3681b25c..fc2ceeb8 100755
--- a/server/test/run.sh
+++ b/server/test/run.sh
@@ -8,7 +8,7 @@
# Exit status 0 when every suite passed.
set -u
-cd "$(dirname "$0")/.."
+cd "$(dirname "$0")/.." || exit 1
CALOG=${CALOG:-$HOME/claude/calog/bin/calog}
SINGE=${SINGE_HOME:-$(cd ../../singemaster && pwd)}
DVXPKG=$(cd .. && pwd)/bin/host/dvxpkg
@@ -29,7 +29,7 @@ package() {
mkdir -p "$(dirname "$stage/$3")"
printf 'contents of %s\n' "$3" > "$stage/$3"
printf '%b' "$2" > "$DATA/$1.ini"
- "$DVXPKG" build "$DATA/$1.ini" "$stage" "$DATA/$1.dpk" ${4:-} > /dev/null || echo "fixture $1 failed"
+ "$DVXPKG" build "$DATA/$1.ini" "$stage" "$DATA/$1.dpk" ${4:+"$4"} > /dev/null || echo "fixture $1 failed"
}
package cards '[package]\nid = acme.cards\nname = Cards\nversion = 1.0.0\nkind = library\nsummary = Card games; for everyone\n' LIBS/ACME/CARDS/CARDS.LIB "$DATA/ACME.SEC"
package cards2 '[package]\nid = acme.cards\nname = Cards\nversion = 1.1.0\nkind = library\n' LIBS/ACME/CARDS/CARDS.LIB
@@ -57,7 +57,7 @@ CONF
stop() { # stop : ask, then insist, so a stuck server cannot hang the run
kill "$1" 2>/dev/null
- for i in $(seq 1 40); do
+ for _ in $(seq 1 40); do
kill -0 "$1" 2>/dev/null || break
sleep 0.25
done
@@ -66,7 +66,7 @@ stop() { # stop : ask, then insist, so a stuck server cannot hang the run
}
wait_for() { # wait_for
- for i in $(seq 1 40); do
+ for _ in $(seq 1 40); do
curl -s -m 1 "http://127.0.0.1:$1/v1/health" > /dev/null 2>&1 && return 0
sleep 0.25
done
diff --git a/server/test/serve.sh b/server/test/serve.sh
index 21df12b9..3bb43a5a 100755
--- a/server/test/serve.sh
+++ b/server/test/serve.sh
@@ -17,7 +17,7 @@
# when calog or singemaster is not there.
set -u
-cd "$(dirname "$0")/.."
+cd "$(dirname "$0")/.." || exit 1
CALOG=${CALOG:-$HOME/claude/calog/bin/calog}
SINGE=${SINGE_HOME:-$(cd ../../singemaster 2>/dev/null && pwd)}
DATA=$1
@@ -47,7 +47,9 @@ trap 'stop; exit 0' TERM INT
trap stop EXIT
[ -x "$CALOG" ] || fail "calog is not at $CALOG (set CALOG)"
-[ -n "$SINGE" ] && [ -f "$SINGE/lib/service.lua" ] || fail "singemaster is not beside this repository (set SINGE_HOME)"
+if [ -z "$SINGE" ] || [ ! -f "$SINGE/lib/service.lua" ]; then
+ fail "singemaster is not beside this repository (set SINGE_HOME)"
+fi
[ -x "$DVXPKG" ] || fail "the package tool is not at $DVXPKG"
rm -rf "$DATA" && mkdir -p "$DATA/server" "$DATA/empty" "$DATA/next/SYSTEM" "$GUEST/SYSTEM/KEYS"
@@ -97,7 +99,7 @@ CONF
"$CALOG" dvxserver.lua -- "singe=$SINGE" "$DATA/dvx.conf" > "$DATA/server.log" 2>&1 &
SERVER=$!
-for i in $(seq 1 40); do
+for _ in $(seq 1 40); do
curl -s -m 1 "http://127.0.0.1:$PORT/v1/health" > /dev/null 2>&1 && break
sleep 0.25
done
diff --git a/src/addons/README.md b/src/addons/README.md
new file mode 100644
index 00000000..52658a92
--- /dev/null
+++ b/src/addons/README.md
@@ -0,0 +1,24 @@
+# DVX's own add-ons
+
+Add-on packages that DVX's operator publishes on the package server, kept
+out of the base install to keep it small. Each folder `KIND/NAME` holds a
+package template (`PACKAGE.INI`, without `[files]`) and the files the
+package installs, laid out as they go into the DVX folder. Every build
+(`src/installer/mkDist.sh`, run by `make`) packs each one into
+`dist/addons/NAME.DPK`; they are uploaded and approved like any other
+package (`server/README.md`).
+
+Third-party content is kept unchanged, with its origin (the commit it came
+from and its SHA-256) in the comments of `PACKAGE.INI` and its licence in a
+text file the package installs beside it.
+
+A folder with a `fonts.ini` instead holds a font manifest (as
+`src/fonts/fonts.ini`): the build makes its `.DFN` files with mkfont into
+`obj/addons/NAME/SHARED/FONTS/KPUNCH`, and mkDist packs that folder. Each
+`.DFN` carries its licence text.
+
+| Folder | Package | What it is |
+|---|---|---|
+| `banks/dmxopl` | `kpunch.dmxopl` | DMXOPL 1.11, a General MIDI instrument bank by ConSiGno (MIT) |
+| `banks/dmxopl3` | `kpunch.dmxopl3` | DMXOPL3, its OPL3 successor, by Sneakernets (MIT) |
+| `fonts/prfonts` | `kpunch.prfonts` | Printer Fonts: DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, for crisp printed text (about 6 MB installed) |
diff --git a/src/addons/banks/dmxopl/PACKAGE.INI b/src/addons/banks/dmxopl/PACKAGE.INI
new file mode 100644
index 00000000..6885a53e
--- /dev/null
+++ b/src/addons/banks/dmxopl/PACKAGE.INI
@@ -0,0 +1,20 @@
+; DMXOPL 1.11 as an add-on package: a General MIDI instrument bank for
+; MIDI music on the FM chip and DVX's FM software synthesizer. mkDist
+; builds it into dist/addons/DMXOPL.DPK for the package server.
+;
+; From https://github.com/sneakernets/DMXOPL, branch master, commit
+; 63bd4821c6adaff29074f1f6486261961c2014b4 (2017-08-22, "V1.11 Release"):
+; GENMIDI.op2, unchanged, as SHARED\BANKS\KPUNCH\DMXOPL.OP2 (SHA-256
+; d3feb807e5102d7de3075c2351cced7e853ba2da8708eff98cc1c8ef8f240ce2).
+; MIT licence; DMXOPL.TXT beside it carries the licence and the credits.
+[package]
+format = 1
+id = kpunch.dmxopl
+name = DMXOPL
+version = 1.11
+kind = bank
+summary = General MIDI instruments for FM music, made to sound like a Roland Sound Canvas.
+publisher = Kangaroo Punch Studios
+author = ConSiGno
+licence = MIT
+homepage = https://github.com/sneakernets/DMXOPL
diff --git a/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.OP2 b/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.OP2
new file mode 100644
index 00000000..a483b71b
Binary files /dev/null and b/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.OP2 differ
diff --git a/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.TXT b/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.TXT
new file mode 100644
index 00000000..b5882bfd
--- /dev/null
+++ b/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.TXT
@@ -0,0 +1,43 @@
+DMXOPL 1.11
+===========
+
+A General MIDI instrument bank for MIDI music on the FM chip and on DVX's
+FM software synthesizer, made to sound like a Roland Sound Canvas (SC-55
+and SC-88). Choose it in Control Panel: Sounds, Instruments.
+
+Author: ConSiGno (https://github.com/sneakernets/DMXOPL)
+Released: August 22, 2017, as DMXOPL v1.11
+File: DMXOPL.OP2, the release's GENMIDI.op2, unchanged
+
+Credits, from the release: Wohlstand, Bisqwuit, Fraggle, Esselfortium,
+Jimmy, The Fat Man, Diode Milliampere, Patchouli, Graf Zahl, Randi, Fisk,
+Stewboy, Xaser, Minigunner, Altazimuth, MTrop, Quasar, Glaice, BlastFrog,
+Vogons Forums, AtariAge, NintendoAge, and John Chowning, the father of FM
+synthesis. Extra thanks to Doomworld Forums and the IRC/Discord channels,
+Giest118, Nuke.YKT, kode54, Patch93, GYRIDION, MovieMovies, Gali, and
+Infurnus.
+
+Licence
+-------
+
+MIT License
+
+Copyright (c) 2017
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/addons/banks/dmxopl3/PACKAGE.INI b/src/addons/banks/dmxopl3/PACKAGE.INI
new file mode 100644
index 00000000..5bfd5786
--- /dev/null
+++ b/src/addons/banks/dmxopl3/PACKAGE.INI
@@ -0,0 +1,23 @@
+; DMXOPL3 as an add-on package: a General MIDI instrument bank for MIDI
+; music on the FM chip and DVX's FM software synthesizer. mkDist builds
+; it into dist/addons/DMXOPL3.DPK for the package server.
+;
+; From https://github.com/sneakernets/DMXOPL, branch DMXOPL3, commit
+; e83c144a2e6d3c283e20f9d16d011126931892f5 (2018-07-13, after the v2.11d
+; release): GENMIDI.op2, unchanged, as SHARED\BANKS\KPUNCH\DMXOPL3.OP2
+; (SHA-256 92fc03c7734c45b1ece2bfad1f79d1abbd2b27ec6b45fbfb1c5a74f5ce0138c9).
+; Made for the OPL3: DVX plays its OPL3-only waveforms as the OPL2's four,
+; as it does for every bank. MIT licence; DMXOPL3.TXT beside it carries
+; the licence and the credits ("DMXOPL3 by Sneakernets", as the author
+; asks).
+[package]
+format = 1
+id = kpunch.dmxopl3
+name = DMXOPL3
+version = 2.11
+kind = bank
+summary = General MIDI instruments for FM music, the OPL3 successor to DMXOPL.
+publisher = Kangaroo Punch Studios
+author = Sneakernets
+licence = MIT
+homepage = https://github.com/sneakernets/DMXOPL
diff --git a/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.OP2 b/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.OP2
new file mode 100644
index 00000000..5c86b778
Binary files /dev/null and b/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.OP2 differ
diff --git a/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.TXT b/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.TXT
new file mode 100644
index 00000000..961e910d
--- /dev/null
+++ b/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.TXT
@@ -0,0 +1,44 @@
+DMXOPL3
+=======
+
+A General MIDI instrument bank for MIDI music on the FM chip and on DVX's
+FM software synthesizer, made to sound like a Roland Sound Canvas (SC-55
+and SC-88): DMXOPL3 by Sneakernets. Choose it in Control Panel: Sounds,
+Instruments. It was made for the OPL3; DVX plays it, like every bank,
+with the OPL2's four waveforms.
+
+Author: Sneakernets (https://github.com/sneakernets/DMXOPL)
+Version: 2.11 (the DMXOPL3 branch as of July 13, 2018)
+File: DMXOPL3.OP2, the branch's GENMIDI.op2, unchanged
+
+Credits, from the project: Wohlstand (OPL3BankEditor), Bisqwuit (ADLMidi),
+Fraggle (Chocolate Doom and GENMIDI research), SubZ3ro (AdLib Tracker II),
+Esselfortium, Jimmy, The Fat Man, Diode Milliampere, Patchouli, Graf Zahl,
+Randi, Fisk, Stewboy, Xaser, Minigunner, Altazimuth, MTrop, Quasar, Glaice,
+BlastFrog, Vogons Forums, AtariAge, NintendoAge, and John Chowning, the
+father of FM synthesis.
+
+Licence
+-------
+
+MIT License
+
+Copyright (c) 2017
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/addons/banks/dvxgm/PACKAGE.INI b/src/addons/banks/dvxgm/PACKAGE.INI
new file mode 100644
index 00000000..8ce283d8
--- /dev/null
+++ b/src/addons/banks/dvxgm/PACKAGE.INI
@@ -0,0 +1,19 @@
+; DVX General MIDI as an add-on package: a General MIDI SoundFont for MIDI
+; music on wavetable sound cards (Gravis UltraSound, Sound Blaster AWE32
+; and AWE64), the standard one DVX looks for (KPUNCH\DVXGM.SF2). The
+; build makes it from Frank Wen's Fluid (R3) General MIDI SoundFont (MIT
+; licence, pinned by src/addons/banks/installSoundFonts.sh) with mksf and
+; soundfont.ini beside this file, into obj/addons/dvxgm; mkDist packs it
+; into dist/addons/DVXGM.DPK for the package server. DVXGM.TXT beside the
+; bank carries the licence and the credits.
+[package]
+format = 1
+id = kpunch.dvxgm
+name = DVX General MIDI
+version = 1.0
+kind = bank
+summary = General MIDI instruments for MIDI music on wavetable sound cards (Gravis UltraSound, Sound Blaster AWE32 and AWE64).
+publisher = Kangaroo Punch Studios
+author = Frank Wen (Fluid R3); cut down for DVX by Kangaroo Punch Studios
+licence = MIT
+homepage = https://packages.debian.org/fluid-soundfont-gm
diff --git a/src/addons/banks/dvxgm/SHARED/BANKS/KPUNCH/DVXGM.TXT b/src/addons/banks/dvxgm/SHARED/BANKS/KPUNCH/DVXGM.TXT
new file mode 100644
index 00000000..07060cbf
--- /dev/null
+++ b/src/addons/banks/dvxgm/SHARED/BANKS/KPUNCH/DVXGM.TXT
@@ -0,0 +1,46 @@
+DVX General MIDI 1.0
+====================
+
+A General MIDI SoundFont for MIDI music on wavetable sound cards (Gravis
+UltraSound, Sound Blaster AWE32 and AWE64): the 128 General MIDI
+instruments and the standard drum kit. DVX uses it for MIDI music
+whenever a wavetable card is there; Control Panel: Sounds, Instruments
+chooses another.
+
+Made from Frank Wen's Fluid (R3) General MIDI SoundFont (FluidR3_GM.sf2,
+from the Debian package fluid-soundfont-gm 3.1-5.3), cut down to fit a
+wavetable card's memory: one velocity layer, mono, a few key regions an
+instrument, short loops, and samples at most 22,050 Hz.
+
+Credits, from the original: "Fluid was constructed in part from samples
+found in the public domain that I edited/cleaned/remixed/programmed and
+largely from recordings of my own and in conjunction with the people
+below who helped along the way: Suren M. Seron, Scott Hanan, Steve
+Aupperle, Chris Gillman, Alex Taubr, Chris Prola, Andrew Klenk, Winfried
+Hubbe, Dylan, Tim, Gort, Uros Katic, Ethan Winer
+(http://www.ethanwiner.com)" -- Frank Wen.
+
+Licence
+-------
+
+Copyright (C) 2000-2002, 2008 Frank Wen
+Copyright (C) 2008 Toby Smithe
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the
+following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/src/addons/banks/dvxgm/soundfont.ini b/src/addons/banks/dvxgm/soundfont.ini
new file mode 100644
index 00000000..24067aa1
--- /dev/null
+++ b/src/addons/banks/dvxgm/soundfont.ini
@@ -0,0 +1,19 @@
+; soundfont.ini -- the DVX General MIDI SoundFont (kpunch.dvxgm), made by
+; mksf (src/tools/mksf.c documents every key and the cuts) from
+; FluidR3_GM.sf2 into SHARED\BANKS\KPUNCH\DVXGM.SF2.
+;
+; Budgets: what one instrument's samples may take (16-bit), and each drum
+; key's. A song loads only the instruments and drum keys it plays, so a
+; song of ten instruments and a dozen drums needs about half a megabyte
+; at most: it fits a 512 KB AWE32 at full quality and smaller cards at 8
+; bits or a lower rate (DVX does that as it loads).
+
+[soundfont]
+source = FluidR3_GM.sf2
+output = DVXGM.SF2
+name = DVX General MIDI
+copyright = Copyright (C) 2000-2002, 2008 Frank Wen; MIT licence (see DVXGM.TXT)
+comment = Made for DVX from Frank Wen's Fluid (R3) General MIDI SoundFont: one velocity layer, mono, a few key regions an instrument, short loops, at most 22,050 Hz.
+velocity = 100
+presetBytes = 40960
+drumBytes = 12288
diff --git a/src/addons/banks/installSoundFonts.sh b/src/addons/banks/installSoundFonts.sh
new file mode 100755
index 00000000..3267a6fc
--- /dev/null
+++ b/src/addons/banks/installSoundFonts.sh
@@ -0,0 +1,89 @@
+#!/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.
+
+# installSoundFonts.sh [--force] -- the SoundFonts DVX's bank add-ons are
+# made from (src/addons/banks/*/soundfont.ini, by src/tools/mksf), pinned,
+# into toolchains/soundfonts:
+#
+# FluidR3_GM.sf2 Frank Wen's Fluid (R3) General MIDI SoundFont, MIT
+# licence, from the Debian package fluid-soundfont-gm
+# 3.1-5.3 (the dvxgm add-on is made from it); its
+# copyright file beside it as FluidR3_GM.copyright
+#
+# Called by installToolchain.sh; safe to run on its own. Nothing is
+# written outside the repository and nothing needs root.
+
+set -euo pipefail
+
+FLUID_FILE="fluid-soundfont-gm_3.1-5.3_all.deb"
+FLUID_URL="https://snapshot.debian.org/archive/debian/20230101T000000Z/pool/main/f/fluid-soundfont/${FLUID_FILE}"
+FLUID_SHA256="6f531493ac4e4d9772fd96b2488ea1790af81c196135fbdd25997da0781fc60e"
+FLUID_MEMBER="./usr/share/sounds/sf2/FluidR3_GM.sf2"
+FLUID_COPYRIGHT="./usr/share/doc/fluid-soundfont-gm/copyright"
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+TOOLCHAINS="${ROOT}/toolchains"
+CACHE="${TOOLCHAINS}/cache"
+SF_DIR="${TOOLCHAINS}/soundfonts"
+PIN_FILE=".installed"
+FORCE=0
+
+if [ "${1:-}" = "--force" ]; then
+ FORCE=1
+fi
+
+mkdir -p "${CACHE}"
+
+
+# fetch URL FILE SHA256 -- FILE in the cache, checked.
+fetch() {
+ local archive="${CACHE}/$2"
+
+ if [ -f "${archive}" ] && echo "$3 ${archive}" | sha256sum -c --status; then
+ return 0
+ fi
+
+ echo "installSoundFonts: downloading $2"
+ curl -fL --retry 3 -o "${archive}.part" "$1"
+ mv "${archive}.part" "${archive}"
+
+ if ! echo "$3 ${archive}" | sha256sum -c --status; then
+ echo "installSoundFonts: checksum mismatch for $2 (expected $3); refusing to install" >&2
+ rm -f "${archive}"
+ exit 1
+ fi
+}
+
+
+if [ "${FORCE}" -eq 0 ] && [ -f "${SF_DIR}/${PIN_FILE}" ] && [ "$(cat "${SF_DIR}/${PIN_FILE}")" = "${FLUID_SHA256}" ] && [ -f "${SF_DIR}/FluidR3_GM.sf2" ]; then
+ echo "installSoundFonts: FluidR3_GM already installed"
+ exit 0
+fi
+
+fetch "${FLUID_URL}" "${FLUID_FILE}" "${FLUID_SHA256}"
+rm -rf "${SF_DIR}" "${SF_DIR}.new"
+mkdir -p "${SF_DIR}.new"
+(cd "${SF_DIR}.new" && ar x "${CACHE}/${FLUID_FILE}" data.tar.xz && tar -xJf data.tar.xz "${FLUID_MEMBER}" "${FLUID_COPYRIGHT}" && mv "${FLUID_MEMBER}" FluidR3_GM.sf2 && mv "${FLUID_COPYRIGHT}" FluidR3_GM.copyright && rm -rf usr data.tar.xz)
+mv "${SF_DIR}.new" "${SF_DIR}"
+echo "${FLUID_SHA256}" > "${SF_DIR}/${PIN_FILE}"
+echo "installSoundFonts: FluidR3_GM installed in ${SF_DIR}"
diff --git a/src/addons/fonts/prfonts/PACKAGE.INI b/src/addons/fonts/prfonts/PACKAGE.INI
new file mode 100644
index 00000000..0819c958
--- /dev/null
+++ b/src/addons/fonts/prfonts/PACKAGE.INI
@@ -0,0 +1,18 @@
+; Printer Fonts as an add-on package: the DejaVu Sans, Serif, and Sans
+; Mono faces DVX ships, in every style, at the large sizes text is
+; printed at (25 to 100 pixels), so printed text is crisp instead of
+; scaled up from the screen sizes. The build makes the .DFN files from
+; fonts.ini beside this file (mkfont, src/tools/Makefile) into
+; obj/addons/prfonts, and mkDist packs them into dist/addons/PRFONTS.DPK
+; for the package server. Each file carries the DejaVu licence text.
+[package]
+format = 1
+id = kpunch.prfonts
+name = Printer Fonts
+version = 2.37
+kind = font
+summary = Large sizes of the DejaVu fonts, for sharp printed text.
+publisher = Kangaroo Punch Studios
+author = DejaVu fonts team
+licence = Bitstream Vera and Arev (DejaVu changes public domain)
+homepage = https://dejavu-fonts.github.io
diff --git a/src/addons/fonts/prfonts/fonts.ini b/src/addons/fonts/prfonts/fonts.ini
new file mode 100644
index 00000000..1b20befa
--- /dev/null
+++ b/src/addons/fonts/prfonts/fonts.ini
@@ -0,0 +1,126 @@
+; fonts.ini -- the printer-fonts add-on (kpunch.prfonts): the DejaVu
+; Sans, Serif, and Sans Mono faces DVX ships, at the large sizes text is
+; printed at, made by mkfont (src/tools/mkfont.c documents every key)
+; into SHARED\FONTS\KPUNCH. Each file adds its sizes to the face of the
+; same family and style in the base set, so printing finds a strike near
+; the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180)
+; and scales it only a little; without the add-on it scales the base
+; set's largest (24 pixels). Sizes: 25 to 100 pixels, about 6 to 24
+; points at 300 dpi and 10 to 40 at 180. Nothing smoothed: printers
+; take dots.
+;
+; Names: P (printer), the family's short form (SANS, SERF, MONO), then
+; B, I, or Z as in the base set.
+
+[fonts]
+PSANS = DejaVu Sans
+PSANSB = DejaVu Sans
+PSANSI = DejaVu Sans
+PSANSZ = DejaVu Sans
+PSERF = DejaVu Serif
+PSERFB = DejaVu Serif
+PSERFI = DejaVu Serif
+PSERFZ = DejaVu Serif
+PMONO = DejaVu Sans Mono
+PMONOB = DejaVu Sans Mono
+PMONOI = DejaVu Sans Mono
+PMONOZ = DejaVu Sans Mono
+
+[licences]
+DEJAVU = dejavu/dejavu-fonts-ttf-2.37/LICENSE
+
+[PSANS]
+style = regular
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = native
+
+[PSANSB]
+style = bold
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Bold.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = native
+
+[PSANSI]
+style = italic
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Oblique.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = auto
+
+[PSANSZ]
+style = bolditalic
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-BoldOblique.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = auto
+
+[PSERF]
+style = regular
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = native
+
+[PSERFB]
+style = bold
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Bold.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = native
+
+[PSERFI]
+style = italic
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Italic.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = auto
+
+[PSERFZ]
+style = bolditalic
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-BoldItalic.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = auto
+
+[PMONO]
+style = regular
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = native
+
+[PMONOB]
+style = bold
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Bold.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = native
+
+[PMONOI]
+style = italic
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Oblique.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = auto
+
+[PMONOZ]
+style = bolditalic
+licence = DEJAVU
+boxdraw = true
+source = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-BoldOblique.ttf
+sizes = 25 29 33 38 42 50 58 67 75 83 100
+hinting = auto
diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile
index 5c67a77a..e20a4986 100644
--- a/src/apps/kpunch/Makefile
+++ b/src/apps/kpunch/Makefile
@@ -49,7 +49,7 @@ BASCOMP = ../../../bin/host/bascomp
# Editor, dvxhelp/helpedit, installs beside the Help Viewer in dvxhelp).
# The program is bin/apps/kpunch//.app, being the last
# part of the directory.
-BASIC_APPS = iconed notepad imgview dvxhelp/helpedit:dvxhelp resedit basdemo widshow clock welcome fileMgr terminal software pkgbuild
+BASIC_APPS = iconed notepad imgview dvxhelp/helpedit:dvxhelp resedit basdemo widshow clock welcome fileMgr terminal software pkgbuild player recorder
basicDir = $(firstword $(subst :, ,$(1)))
basicName = $(notdir $(call basicDir,$(1)))
diff --git a/src/apps/kpunch/README.md b/src/apps/kpunch/README.md
index 4ffacb57..9731b783 100644
--- a/src/apps/kpunch/README.md
+++ b/src/apps/kpunch/README.md
@@ -16,7 +16,7 @@ the BASIC-based sample apps. Two flavours of app live side by side:
copy of `basstub.app`, which acts as the runtime host. See
`iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
`widshow/`, `dvxhelp/helpedit/`, `clock/`, `welcome/`, `fileMgr/`,
- `terminal/`.
+ `terminal/`, `player/`, `recorder/`.
The rest of this document covers writing a C app against the SDK.
For BASIC apps, see `dvxbasic/README.md`. A new app outside this tree
@@ -290,6 +290,56 @@ Options), `termPass.frm` (passphrase), `termProg.frm` (File Transfer),
start), with the conn.bas, xfer.bas, prefs.bas, fileops.bas, shell.bas,
commdlg.bas, and help.bas libraries; its help is `terminal.dhs`.
+### Media Player (player, BASIC)
+
+| | |
+|---|---|
+| File | `apps/kpunch/player/player.app` |
+| Type | BASIC (main-loop via basstub) |
+| Multi-instance | No (a second start hands its files over: NewArgs) |
+
+Plays music modules (MOD, S3M, XM, IT), MIDI files (MID, RMI), and WAV
+and VOC sounds from a list, through music.lib (music.bas): play, pause,
+stop, previous, next, a position slider that seeks once it stands
+still, a volume slider, Repeat Off / List / Track, and Shuffle (a
+shuffled play order with the track playing first). A 250 ms Timer shows
+the position and notices a track's end. Files named on its command line
+are added to the list and the first plays; ASSOC.INI opens those file
+types in it. The list and the settings are kept in PLAYER.INI in its
+settings folder. Built from `player.dbp` + `player.frm`, with the
+music.bas, prefs.bas, fileops.bas, commdlg.bas, and help.bas libraries;
+its help is `player.dhs`. Tested on the host by
+`src/test/music/playerTest.c` and on DOS by `player.scr`.
+
+### Sound Recorder (recorder, BASIC)
+
+| | |
+|---|---|
+| File | `apps/kpunch/recorder/recorder.app` |
+| Type | BASIC (main-loop via basstub) |
+| Multi-instance | No |
+
+Records sound from the card's microphone, line, or CD input into WAV
+files through audio.lib's recording (sound.bas RecordStart,
+RecordStop, RecordLength, RecordLevel): Record, Stop, Play (PlayWave),
+a level meter (a ProgressBar) and the length shown by a 100 ms Timer,
+the format the card chose. A recording goes into RECORDING.WAV in its
+settings folder until File > Save As copies it out (FILECOPY); New,
+Open, and closing ask about a recording not saved. Options picks the
+quality (telephone 8 kHz 8-bit mono, radio 22 kHz 8-bit mono, CD 44.1
+kHz 16-bit stereo) and the input (inputs the card lacks are greyed).
+The MIDI Keyboard input (when DVX.INI [audio] midiIn names the MIDI
+port) records MIDI instead: MidiInOpen, each message played as it
+comes on a live music (MusicMidiSend) and kept in a MIDI recording
+(MidiRecordAdd with MidiInTime), the Timer at 20 ms meanwhile, the
+level the last note's velocity; Stop writes RECORDING.MID
+(MidiRecordSave) and Play plays it with music.lib. Open takes WAV and
+MIDI files (MusicLoad tells which). Settings are kept in RECORDER.INI.
+Built from `recorder.dbp` + `recorder.frm`, with the sound.bas,
+music.bas, prefs.bas, fileops.bas, commdlg.bas, and help.bas
+libraries; its help is `recorder.dhs`. Tested on the host by
+`src/test/music/recorderTest.c` and on DOS by `record.scr`.
+
### DVX Demo (dvxdemo)
| | |
@@ -643,6 +693,16 @@ apps/kpunch/
termMain.bas start
terminal.dhs
ICON32.BMP
+ player/
+ player.dbp
+ player.frm
+ player.dhs
+ ICON32.BMP
+ recorder/
+ recorder.dbp
+ recorder.frm
+ recorder.dhs
+ ICON32.BMP
dvxbasic/ see dvxbasic/README.md
compiler/ lexer, parser, codegen, strip, obfuscate
diff --git a/src/apps/kpunch/basdemo/basdemo.dbp b/src/apps/kpunch/basdemo/basdemo.dbp
index 1f018641..83b526c4 100644
--- a/src/apps/kpunch/basdemo/basdemo.dbp
+++ b/src/apps/kpunch/basdemo/basdemo.dbp
@@ -12,7 +12,8 @@ File0 = ../../../include/basic/commdlg.bas
File1 = ../../../include/basic/fileops.bas
File2 = ../../../include/basic/help.bas
File3 = ../../../include/basic/font.bas
-Count = 4
+File4 = ../../../include/basic/print.bas
+Count = 5
[Classes]
File0 = shape.cls
diff --git a/src/apps/kpunch/basdemo/basdemo.dhs b/src/apps/kpunch/basdemo/basdemo.dhs
index 7ca728eb..e7f39983 100644
--- a/src/apps/kpunch/basdemo/basdemo.dhs
+++ b/src/apps/kpunch/basdemo/basdemo.dhs
@@ -75,6 +75,7 @@ The File I/O and INI examples create their files in the program's own data folde
Demos Timer... Open a window with a counter that ticks twice a second.
Demos Fonts... Open a window of text in several fonts, styles, and colors; a sample line whose font you choose with Choose Font; and a box to write styled text in, with Bold, Italic, and Underline buttons.
Demos Objects Make shapes from the Shape class module, keep them in a Collection, and list them with FOR EACH.
+ Demos Print Report... Print a page drawn with the Printer object: a title, a table of sales figures lined up with TextWidth, and a bar chart with dotted grid lines (DrawStyle) and filled bars (FillStyle and FillColor). The Print dialog chooses where it goes and how many copies; printing needs your printer's model set in the Control Panel.
Help Contents Open this help.
Help About... Show the program name and copyright.
.endtable
diff --git a/src/apps/kpunch/basdemo/basdemo.frm b/src/apps/kpunch/basdemo/basdemo.frm
index 38582d5a..4eff6621 100644
--- a/src/apps/kpunch/basdemo/basdemo.frm
+++ b/src/apps/kpunch/basdemo/basdemo.frm
@@ -48,6 +48,12 @@ Begin Form BasicDemo
Begin Menu mnuObjects
Caption = "&Objects"
End
+ Begin Menu mnuSepR2
+ Caption = "-"
+ End
+ Begin Menu mnuReport
+ Caption = "Print &Report..."
+ End
End
Begin Menu mnuHelp
@@ -206,6 +212,19 @@ CONST FONT_FORM_H = 380
CONST FONT_RICH_W = 360
CONST FONT_BIG = 20
CONST FONT_SERIF = "DejaVu Serif"
+CONST FONT_SANS = "DejaVu Sans"
+CONST REPORT_ROWS = 4
+CONST REPORT_LEFT = 1 ' the report's left and right edges, inches
+CONST REPORT_RIGHT = 7
+CONST REPORT_TOP = 0.75
+CONST REPORT_FIGURES = 4 ' where the table's figures end
+CONST REPORT_GAP = 0.25
+CONST REPORT_TITLE = 20 ' points
+CONST REPORT_TEXT = 11
+CONST REPORT_RULE_W = 3 ' printer dots
+CONST REPORT_CHART_H = 3 ' inches
+CONST REPORT_SCALE = 6000 ' sales at the top of the chart
+CONST REPORT_GRID = 1000 ' sales between the chart's grid lines
DIM SHARED gfxOpen AS BOOLEAN
DIM SHARED dynOpen AS BOOLEAN
@@ -1108,3 +1127,115 @@ SUB mnuObjects_Click
Say "TypeName(s) = " + TypeName(s) + ", s Is shapes(2): " + IIf(s Is shapes(2), "yes", "no")
LblStatus.Caption = "Objects demo complete."
END SUB
+
+
+SUB mnuReport_Click
+ ' Print Report: a page drawn with the Printer object -- a title, a
+ ' table of figures, and a bar chart -- on the printer chosen in the
+ ' Control Panel, where and as many times as the Print dialog says
+ DIM regions(1 TO REPORT_ROWS) AS STRING
+ DIM sales(1 TO REPORT_ROWS) AS LONG
+ DIM shades(1 TO REPORT_ROWS) AS LONG
+ DIM total AS LONG
+ DIM level AS LONG
+ DIM i AS INTEGER
+ DIM y AS SINGLE
+ DIM x AS SINGLE
+ DIM pitch AS SINGLE
+ DIM axis AS SINGLE
+
+ IF PrintCanPrintPictures() = 0 THEN
+ MsgBox "To print the report, choose your printer's model on the Control Panel's Printer page."
+ EXIT SUB
+ END IF
+
+ IF PrintChoose("Sales Report", 1) = 0 THEN
+ EXIT SUB
+ END IF
+
+ regions(1) = "North"
+ sales(1) = 4200
+ shades(1) = RGB(0, 90, 200)
+ regions(2) = "South"
+ sales(2) = 3150
+ shades(2) = RGB(220, 120, 0)
+ regions(3) = "East"
+ sales(3) = 5480
+ shades(3) = RGB(0, 150, 70)
+ regions(4) = "West"
+ sales(4) = 2630
+ shades(4) = RGB(170, 0, 120)
+
+ ' The title and a rule under it, measured in inches
+ Printer.ScaleMode = vbInches
+ Printer.FontName = FONT_SERIF
+ Printer.FontSize = REPORT_TITLE
+ Printer.FontBold = True
+ Printer.CurrentX = REPORT_LEFT
+ Printer.CurrentY = REPORT_TOP
+ Printer.Print "Quarterly Sales by Region"
+ Printer.FontBold = False
+ Printer.FontSize = REPORT_TEXT
+ Printer.CurrentX = REPORT_LEFT
+ Printer.Print "From the DVX BASIC Feature Tour"
+ y = Printer.CurrentY + REPORT_GAP / 2
+ Printer.DrawWidth = REPORT_RULE_W
+ Printer.Line (REPORT_LEFT, y)-(REPORT_RIGHT, y)
+ Printer.DrawWidth = 1
+
+ ' The table: each region and its sales, then the total
+ Printer.FontName = FONT_SANS
+ y = y + REPORT_GAP
+
+ FOR i = 1 TO REPORT_ROWS
+ ReportRow regions(i), sales(i), y, False
+ total = total + sales(i)
+ y = y + Printer.TextHeight("X")
+ NEXT
+
+ ReportRow "Total", total, y, True
+
+ ' The chart: dotted grid lines, the axis, and a bar for each region
+ ' in its color with its name under it
+ axis = y + Printer.TextHeight("X") + REPORT_GAP + REPORT_CHART_H
+ Printer.DrawStyle = vbDot
+
+ FOR level = REPORT_GRID TO REPORT_SCALE STEP REPORT_GRID
+ y = axis - level / REPORT_SCALE * REPORT_CHART_H
+ Printer.Line (REPORT_LEFT, y)-(REPORT_RIGHT, y)
+ NEXT
+
+ Printer.DrawStyle = vbSolid
+ Printer.Line (REPORT_LEFT, axis)-(REPORT_RIGHT, axis)
+ Printer.FillStyle = vbFSSolid
+ pitch = (REPORT_RIGHT - REPORT_LEFT) / REPORT_ROWS
+
+ FOR i = 1 TO REPORT_ROWS
+ x = REPORT_LEFT + (i - 1) * pitch + pitch / 4
+ Printer.FillColor = shades(i)
+ Printer.Line (x, axis - sales(i) / REPORT_SCALE * REPORT_CHART_H)-(x + pitch / 2, axis), vbBlack, B
+ Printer.CurrentX = x + pitch / 4 - Printer.TextWidth(regions(i)) / 2
+ Printer.CurrentY = axis + REPORT_GAP / 2
+ Printer.Print regions(i)
+ NEXT
+
+ Printer.FillStyle = vbFSTransparent
+ Printer.EndDoc
+ LblStatus.Caption = "Report sent to the printer."
+END SUB
+
+
+SUB ReportRow(label AS STRING, figure AS LONG, y AS SINGLE, bold AS BOOLEAN)
+ ' One line of the report's table: the label, and the figure ending
+ ' at REPORT_FIGURES
+ DIM text AS STRING
+
+ text = FORMAT$(figure, "#,##0")
+ Printer.FontBold = bold
+ Printer.CurrentX = REPORT_LEFT
+ Printer.CurrentY = y
+ Printer.Print label;
+ Printer.CurrentX = REPORT_FIGURES - Printer.TextWidth(text)
+ Printer.Print text
+ Printer.FontBold = False
+END SUB
diff --git a/src/apps/kpunch/cpanel/cpJoystick.c b/src/apps/kpunch/cpanel/cpJoystick.c
index 8c66d036..57028e3a 100644
--- a/src/apps/kpunch/cpanel/cpJoystick.c
+++ b/src/apps/kpunch/cpanel/cpJoystick.c
@@ -227,7 +227,7 @@ static WidgetT *addRow(WidgetT *parent, const char *label) {
WidgetT *lbl = wgtLabel(row, label);
row->spacing = wgtPixels(CPJ_GAP);
- lbl->prefW = wgtPixels(CPJ_LABEL_W);
+ lbl->minW = wgtPixels(CPJ_LABEL_W);
return row;
}
@@ -270,23 +270,23 @@ void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
WidgetT *detect = wgtButton(row, "Detect");
detect->onClick = onDetect;
- detect->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+ detect->minW = 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);
+ sCalBtn->minW = wgtPixels(CPJ_BTN_W);
sResetBtn = wgtButton(row, "Automatic");
sResetBtn->onClick = onReset;
- sResetBtn->prefW = wgtPixels(CPJ_BTN_W);
+ sResetBtn->minW = 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);
+ sZoneLbl->minW = wgtPixels(CPJ_ZONE_LBL_W);
row = addRow(page, "Game port:");
sModeDrop = wgtDropdown(row);
@@ -301,7 +301,7 @@ void cpJoystickBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
WidgetT *keys = wgtButton(row, "Keys...");
keys->onClick = onKeys;
- keys->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+ keys->minW = wgtPixels(CPJ_SMALL_BTN_W);
WidgetT *timer = wgtTimer(page, CPJ_TICK_MS, true);
@@ -447,12 +447,12 @@ static void onCalibrate(WidgetT *w) {
row->spacing = wgtPixels(CPJ_GAP);
sWiz.next = wgtButton(row, "Next");
sWiz.next->onClick = onWizNext;
- sWiz.next->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+ sWiz.next->minW = wgtPixels(CPJ_SMALL_BTN_W);
WidgetT *cancel = wgtButton(row, "Cancel");
cancel->onClick = onWizCancel;
- cancel->prefW = wgtPixels(CPJ_SMALL_BTN_W);
+ cancel->minW = wgtPixels(CPJ_SMALL_BTN_W);
WidgetT *timer = wgtTimer(root, CPJ_TICK_MS, true);
diff --git a/src/apps/kpunch/cpanel/cpNet.c b/src/apps/kpunch/cpanel/cpNet.c
index 9494f12d..8e982914 100644
--- a/src/apps/kpunch/cpanel/cpNet.c
+++ b/src/apps/kpunch/cpanel/cpNet.c
@@ -114,8 +114,8 @@ static WidgetT *addField(WidgetT *parent, const char *label, int32_t maxLen, con
WidgetT *in = wgtTextInput(row, maxLen);
row->spacing = wgtPixels(CN_GAP);
- lbl->prefW = wgtPixels(CN_LABEL_W);
- in->prefW = wgtChars(CN_FIELD_CHARS);
+ lbl->minW = wgtPixels(CN_LABEL_W);
+ in->minW = wgtChars(CN_FIELD_CHARS);
wgtSetText(in, text);
return in;
}
@@ -173,7 +173,7 @@ void cpNetBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
// Packet driver interrupt: "Find it" or one of 60h..80h
row = wgtHBox(page);
row->spacing = wgtPixels(CN_GAP);
- wgtLabel(row, "Packet driver:")->prefW = wgtPixels(CN_LABEL_W);
+ wgtLabel(row, "Packet driver:")->minW = wgtPixels(CN_LABEL_W);
snprintf(sVectorText[0], CN_VECTOR_TEXT, "%s", "Find it");
sVectorLabels[0] = sVectorText[0];
@@ -193,10 +193,10 @@ void cpNetBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
row->spacing = wgtPixels(CN_GAP);
btn = wgtButton(row, "Status...");
btn->onClick = onStatus;
- btn->prefW = wgtPixels(CN_BTN_W);
+ btn->minW = wgtPixels(CN_BTN_W);
btn = wgtButton(row, "Restart");
btn->onClick = onRestart;
- btn->prefW = wgtPixels(CN_BTN_W);
+ btn->minW = wgtPixels(CN_BTN_W);
}
diff --git a/src/apps/kpunch/cpanel/cpPrinter.c b/src/apps/kpunch/cpanel/cpPrinter.c
index c043d42e..f4faf38b 100644
--- a/src/apps/kpunch/cpanel/cpPrinter.c
+++ b/src/apps/kpunch/cpanel/cpPrinter.c
@@ -25,8 +25,9 @@
// The printing 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). Page Setup... opens the library's own dialog, which saves
-// the page setup at once; OK stores the model and port and keeps the
-// page setup as it is in DVX.INI then.
+// the page setup at once; OK stores the model and port (and a serial
+// port's speed and handshake, which only a COM port enables) and keeps
+// the page setup as it is in DVX.INI then.
#include "cpPrinter.h"
#include "print/print.h"
@@ -39,12 +40,15 @@
#include "textInput/textInpt.h"
#include
+#include
+#include
#include
#define CPR_LABEL_W 110
#define CPR_FIELD_CHARS 24
#define CPR_GAP 4
#define CPR_BTN_W 110
+#define CPR_BAUD_CHARS 8 // a speed's label, "38400" and its NUL
// The library's functions and tables (NULL when it is not installed)
typedef struct {
@@ -54,6 +58,10 @@ typedef struct {
__typeof__(printPageSetupDialog) *pageSetup;
__typeof__(printDestLabel) *destLabel;
__typeof__(printModelLabel) *modelLabel;
+ __typeof__(printDestIsSerial) *isSerial;
+ __typeof__(printBaudRate) *baudRate;
+ __typeof__(printHandshakeLabel) *handshakeLabel;
+ __typeof__(printSerialStore) *serialStore;
} PrintFnsT;
static PrintFnsT sPrint;
@@ -62,8 +70,13 @@ static WidgetT *sModel = NULL;
static WidgetT *sDest = NULL;
static WidgetT *sFile = NULL;
static WidgetT *sBrowse = NULL; // beside sFile
+static WidgetT *sBaud = NULL;
+static WidgetT *sShake = NULL;
static const char *sDestLabels[PrintDestCountE];
static const char *sModelLabels[PrintModelCountE];
+static const char *sShakeLabels[PrintHandshakeCountE];
+static const char *sBaudLabels[PRINT_BAUD_COUNT];
+static char sBaudText[PRINT_BAUD_COUNT][CPR_BAUD_CHARS];
// ============================================================
// Prototypes
@@ -84,7 +97,7 @@ static WidgetT *addRow(WidgetT *parent, const char *label) {
WidgetT *lbl = wgtLabel(row, label);
row->spacing = wgtPixels(CPR_GAP);
- lbl->prefW = wgtPixels(CPR_LABEL_W);
+ lbl->minW = wgtPixels(CPR_LABEL_W);
return row;
}
@@ -92,15 +105,19 @@ static WidgetT *addRow(WidgetT *parent, const char *label) {
void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
PrintSettingsT s;
- sAc = ac;
- sPrint.read = (__typeof__(sPrint.read))dlsym(RTLD_DEFAULT, DVX_SYM("printSettingsRead"));
- sPrint.problem = (__typeof__(sPrint.problem))dlsym(RTLD_DEFAULT, DVX_SYM("printDeviceProblem"));
- sPrint.store = (__typeof__(sPrint.store))dlsym(RTLD_DEFAULT, DVX_SYM("printDeviceStore"));
- sPrint.pageSetup = (__typeof__(sPrint.pageSetup))dlsym(RTLD_DEFAULT, DVX_SYM("printPageSetupDialog"));
- sPrint.destLabel = (__typeof__(sPrint.destLabel))dlsym(RTLD_DEFAULT, DVX_SYM("printDestLabel"));
- sPrint.modelLabel = (__typeof__(sPrint.modelLabel))dlsym(RTLD_DEFAULT, DVX_SYM("printModelLabel"));
+ sAc = ac;
+ sPrint.read = (__typeof__(sPrint.read))dlsym(RTLD_DEFAULT, DVX_SYM("printSettingsRead"));
+ sPrint.problem = (__typeof__(sPrint.problem))dlsym(RTLD_DEFAULT, DVX_SYM("printDeviceProblem"));
+ sPrint.store = (__typeof__(sPrint.store))dlsym(RTLD_DEFAULT, DVX_SYM("printDeviceStore"));
+ sPrint.pageSetup = (__typeof__(sPrint.pageSetup))dlsym(RTLD_DEFAULT, DVX_SYM("printPageSetupDialog"));
+ sPrint.destLabel = (__typeof__(sPrint.destLabel))dlsym(RTLD_DEFAULT, DVX_SYM("printDestLabel"));
+ sPrint.modelLabel = (__typeof__(sPrint.modelLabel))dlsym(RTLD_DEFAULT, DVX_SYM("printModelLabel"));
+ sPrint.isSerial = (__typeof__(sPrint.isSerial))dlsym(RTLD_DEFAULT, DVX_SYM("printDestIsSerial"));
+ sPrint.baudRate = (__typeof__(sPrint.baudRate))dlsym(RTLD_DEFAULT, DVX_SYM("printBaudRate"));
+ sPrint.handshakeLabel = (__typeof__(sPrint.handshakeLabel))dlsym(RTLD_DEFAULT, DVX_SYM("printHandshakeLabel"));
+ sPrint.serialStore = (__typeof__(sPrint.serialStore))dlsym(RTLD_DEFAULT, DVX_SYM("printSerialStore"));
- if (!sPrint.read || !sPrint.problem || !sPrint.store || !sPrint.pageSetup || !sPrint.destLabel || !sPrint.modelLabel) {
+ if (!sPrint.read || !sPrint.problem || !sPrint.store || !sPrint.pageSetup || !sPrint.destLabel || !sPrint.modelLabel || !sPrint.isSerial || !sPrint.baudRate || !sPrint.handshakeLabel || !sPrint.serialStore) {
memset(&sPrint, 0, sizeof(sPrint));
wgtLabel(page, "Printing is not installed.");
return;
@@ -116,6 +133,18 @@ void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
sModelLabels[i] = sPrint.modelLabel((PrintModelE)i);
}
+ int32_t baudAt = 0;
+
+ for (int32_t i = 0; i < PRINT_BAUD_COUNT; i++) {
+ snprintf(sBaudText[i], sizeof(sBaudText[i]), "%" PRId32, sPrint.baudRate(i));
+ sBaudLabels[i] = sBaudText[i];
+ baudAt = sPrint.baudRate(i) == s.baud ? i : baudAt;
+ }
+
+ for (int32_t i = 0; i < PrintHandshakeCountE; i++) {
+ sShakeLabels[i] = sPrint.handshakeLabel((PrintHandshakeE)i);
+ }
+
sModel = wgtDropdown(addRow(page, "Printer model:"));
wgtDropdownSetItems(sModel, sModelLabels, PrintModelCountE);
wgtDropdownSetSelected(sModel, s.model);
@@ -127,10 +156,16 @@ void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
WidgetT *fileRow = addRow(page, "File name:");
sFile = wgtTextInput(fileRow, PRINT_PATH_MAX - 1);
- sFile->prefW = wgtChars(CPR_FIELD_CHARS);
+ sFile->minW = wgtChars(CPR_FIELD_CHARS);
sBrowse = wgtButton(fileRow, "&Browse...");
sBrowse->onClick = onBrowse;
wgtSetText(sFile, s.filePath);
+ sBaud = wgtDropdown(addRow(page, "Speed (baud):"));
+ wgtDropdownSetItems(sBaud, sBaudLabels, PRINT_BAUD_COUNT);
+ wgtDropdownSetSelected(sBaud, baudAt);
+ sShake = wgtDropdown(addRow(page, "Handshake:"));
+ wgtDropdownSetItems(sShake, sShakeLabels, PrintHandshakeCountE);
+ wgtDropdownSetSelected(sShake, s.handshake);
onDest(sDest);
wgtLabel(page, "Text only works with any printer; choose your printer's");
wgtLabel(page, "model to print pictures too.");
@@ -138,7 +173,7 @@ void cpPrinterBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
WidgetT *btn = wgtButton(wgtHBox(page), "Page Setup...");
btn->onClick = onPageSetup;
- btn->prefW = wgtPixels(CPR_BTN_W);
+ btn->minW = wgtPixels(CPR_BTN_W);
}
@@ -159,6 +194,7 @@ void cpPrinterStore(PrefsHandleT *prefs) {
}
sPrint.store(prefs, (PrintModelE)wgtDropdownGetSelected(sModel), (PrintDestE)wgtDropdownGetSelected(sDest), wgtGetText(sFile));
+ sPrint.serialStore(prefs, sPrint.baudRate(wgtDropdownGetSelected(sBaud)), (PrintHandshakeE)wgtDropdownGetSelected(sShake));
}
@@ -177,12 +213,17 @@ static void onBrowse(WidgetT *w) {
}
-// The file name only matters when printing to a file.
+// The file name only matters when printing to a file, the speed and
+// handshake on a serial port.
static void onDest(WidgetT *w) {
- bool toFile = wgtDropdownGetSelected(w) == PrintDestFileE;
+ PrintDestE dest = (PrintDestE)wgtDropdownGetSelected(w);
+ bool toFile = dest == PrintDestFileE;
+ bool serial = sPrint.isSerial(dest);
wgtSetEnabled(sFile, toFile);
wgtSetEnabled(sBrowse, toFile);
+ wgtSetEnabled(sBaud, serial);
+ wgtSetEnabled(sShake, serial);
}
diff --git a/src/apps/kpunch/cpanel/cpSounds.c b/src/apps/kpunch/cpanel/cpSounds.c
index f3d7aea2..41330f65 100644
--- a/src/apps/kpunch/cpanel/cpSounds.c
+++ b/src/apps/kpunch/cpanel/cpSounds.c
@@ -26,11 +26,19 @@
// 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 MIDI instrument banks offered are the standard one and
-// those in the banks folders. The volume applies at once (Cancel puts
-// DVX.INI's back); the sounds, the driver, the MIDI choice, and the bank
-// are written by OK, and the driver, MIDI choice, and bank take effect
-// when DVX next starts.
+// first. Two rows of instrument banks: FM banks (OP2) for the FM chip
+// and the FM software synthesizer, and SoundFonts for wavetable music
+// (and a sound card's own banks: an UltraSound's patches, an AWE's ROM
+// bank, where its software installed them); each offers the standard one
+// and those in the banks folders, each shown by the name of the add-on
+// that installed it when the package library can say, and its Test plays
+// a short tune on the one selected. The volume applies at once (Cancel
+// puts DVX.INI's back); the sounds, the driver, the MIDI and modules
+// choices, and the banks are written by OK. The banks and the modules
+// choice reach the next music; the driver and the MIDI choice take
+// effect when DVX next starts. The music and package libraries are
+// looked up by name too, and the page does without them. The page fits
+// 640x480: the volume and the driver share a row.
#include "cpSounds.h"
#include "audio/audio.h"
@@ -43,7 +51,9 @@
#include "label/label.h"
#include "listBox/listBox.h"
#include "music/music.h"
+#include "pkg/pkgDb.h"
#include "slider/slider.h"
+#include "spacer/spacer.h"
#include "stb_ds_wrap.h"
#include
@@ -55,66 +65,137 @@
#define CPS_LABEL_W 100
#define CPS_GAP 4
+#define CPS_PAIR_GAP 12 // between two settings on one row
#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_DRV_ITEMS (AUDIO_DRIVERS_MAX + 2) // Automatic, the drivers, DVX.INI's own choice
#define PERCENT_MAX 100
#define CPS_NONE_LABEL "(None)"
#define CPS_BANK_LABEL "Standard"
-#define CPS_BANK_EXT ".op2"
+#define CPS_BANK_TITLE "Instruments"
+#define CPS_PATCHES "Gravis UltraSound patches"
+#define CPS_ROM_BANK "Sound Blaster AWE ROM instruments"
+// The Instruments Test: the same rising arpeggio on five General MIDI
+// instruments in turn (piano, guitar, violin, trumpet, flute)
+#define CPS_BANK_TUNE "T150 L8 I1 O3 CEG>C4 I25 O3 CEG>C4 I41 O4 CEG>C4 I57 O4 CEG>C4 I74 O5 CEG>C4"
// A dropdown's items: the values DVX.INI keeps and the labels shown
typedef struct {
char **values; // stb_ds, owned
- const char **labels; // stb_ds, parallel (into values, or a literal for "")
+ const char **labels; // stb_ds, parallel (into values or owned, or a literal for "")
+ char **owned; // stb_ds: labels made for items (choiceSetLabel)
} ChoicesT;
// The sound library's functions (NULL when it is not installed)
typedef struct {
- __typeof__(audioDeskFile) *file;
- __typeof__(audioDeskLabel) *label;
- __typeof__(audioDeskPlayFile) *playFile;
- __typeof__(audioDeskReload) *reload;
- __typeof__(audioDeskSetFile) *setFile;
- __typeof__(audioDriverNames) *driverNames;
- __typeof__(audioErrorText) *errorText;
- __typeof__(audioGetInfo) *getInfo;
- const PrefsChoiceT *midiChoices;
- __typeof__(audioPercentToVolume) *toVolume;
- __typeof__(audioSetMasterVolume) *setVolume;
- __typeof__(audioSettingDriver) *driver;
- __typeof__(audioSettingMidi) *midi;
- __typeof__(audioSettingsStore) *store;
- __typeof__(audioSettingVolume) *volume;
+ __typeof__(audioDeskFile) *file;
+ __typeof__(audioDeskLabel) *label;
+ __typeof__(audioDeskPlayFile) *playFile;
+ __typeof__(audioDeskReload) *reload;
+ __typeof__(audioDeskSetFile) *setFile;
+ __typeof__(audioDriverNames) *driverNames;
+ __typeof__(audioErrorText) *errorText;
+ __typeof__(audioGetInfo) *getInfo;
+ const PrefsChoiceT *midiChoices;
+ const PrefsChoiceT *midiInChoices;
+ const PrefsChoiceT *modulesChoices;
+ __typeof__(audioPercentToVolume) *toVolume;
+ __typeof__(audioSetMasterVolume) *setVolume;
+ __typeof__(audioSettingDriver) *driver;
+ __typeof__(audioSettingMidi) *midi;
+ __typeof__(audioSettingMidiIn) *midiIn;
+ __typeof__(audioSettingMidiInStore) *midiInStore;
+ __typeof__(audioSettingModules) *modules;
+ __typeof__(audioSettingsStore) *store;
+ __typeof__(audioSettingVolume) *volume;
} SndFnsT;
+// How the music library finds a sound card's own bank (musicPatchSetPath,
+// musicRomBankPath)
+typedef bool BankFindFnT(char *out, int32_t outSize);
+
+// The music library's functions, for the Instruments Test (NULL when it
+// is not installed: no Test)
+typedef struct {
+ __typeof__(musicErrorText) *errorText;
+ __typeof__(musicFree) *release;
+ __typeof__(musicLoadMacro) *loadMacro;
+ __typeof__(musicPatchSetPath) *patchSet;
+ __typeof__(musicPlay) *play;
+ __typeof__(musicRomBankPath) *romBank;
+ __typeof__(musicSetBank) *setBank;
+} MusFnsT;
+
+// The package library's functions, for the banks' names (NULL when it is
+// not installed: the names of their files)
+typedef struct {
+ __typeof__(pkgDbFree) *release;
+ __typeof__(pkgDbLoad) *load;
+ __typeof__(pkgDbOwner) *owner;
+} PkgFnsT;
+
+// A function looked up by name into a field of one of those
typedef struct {
const char *name;
size_t offset;
-} SndFnSymT;
+} FnSymT;
-#define SND_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(SndFnsT, field) }
+#define FN_SYM(type, field, fn) { DVX_SYM(#fn), offsetof(type, field) }
-static const SndFnSymT sSyms[] = {
- SND_FN_SYM(file, audioDeskFile),
- SND_FN_SYM(label, audioDeskLabel),
- SND_FN_SYM(playFile, audioDeskPlayFile),
- SND_FN_SYM(reload, audioDeskReload),
- SND_FN_SYM(setFile, audioDeskSetFile),
- SND_FN_SYM(driverNames, audioDriverNames),
- SND_FN_SYM(errorText, audioErrorText),
- SND_FN_SYM(getInfo, audioGetInfo),
- SND_FN_SYM(midiChoices, audioMidiChoices),
- SND_FN_SYM(toVolume, audioPercentToVolume),
- SND_FN_SYM(setVolume, audioSetMasterVolume),
- SND_FN_SYM(driver, audioSettingDriver),
- SND_FN_SYM(midi, audioSettingMidi),
- SND_FN_SYM(store, audioSettingsStore),
- SND_FN_SYM(volume, audioSettingVolume)
+static const FnSymT sSndSyms[] = {
+ FN_SYM(SndFnsT, file, audioDeskFile),
+ FN_SYM(SndFnsT, label, audioDeskLabel),
+ FN_SYM(SndFnsT, playFile, audioDeskPlayFile),
+ FN_SYM(SndFnsT, reload, audioDeskReload),
+ FN_SYM(SndFnsT, setFile, audioDeskSetFile),
+ FN_SYM(SndFnsT, driverNames, audioDriverNames),
+ FN_SYM(SndFnsT, errorText, audioErrorText),
+ FN_SYM(SndFnsT, getInfo, audioGetInfo),
+ FN_SYM(SndFnsT, midiChoices, audioMidiChoices),
+ FN_SYM(SndFnsT, midiInChoices, audioMidiInChoices),
+ FN_SYM(SndFnsT, modulesChoices, audioModulesChoices),
+ FN_SYM(SndFnsT, toVolume, audioPercentToVolume),
+ FN_SYM(SndFnsT, setVolume, audioSetMasterVolume),
+ FN_SYM(SndFnsT, driver, audioSettingDriver),
+ FN_SYM(SndFnsT, midi, audioSettingMidi),
+ FN_SYM(SndFnsT, midiIn, audioSettingMidiIn),
+ FN_SYM(SndFnsT, midiInStore, audioSettingMidiInStore),
+ FN_SYM(SndFnsT, modules, audioSettingModules),
+ FN_SYM(SndFnsT, store, audioSettingsStore),
+ FN_SYM(SndFnsT, volume, audioSettingVolume)
};
+static const FnSymT sMusSyms[] = {
+ FN_SYM(MusFnsT, errorText, musicErrorText),
+ FN_SYM(MusFnsT, release, musicFree),
+ FN_SYM(MusFnsT, loadMacro, musicLoadMacro),
+ FN_SYM(MusFnsT, patchSet, musicPatchSetPath),
+ FN_SYM(MusFnsT, play, musicPlay),
+ FN_SYM(MusFnsT, romBank, musicRomBankPath),
+ FN_SYM(MusFnsT, setBank, musicSetBank)
+};
+
+static const FnSymT sPkgSyms[] = {
+ FN_SYM(PkgFnsT, release, pkgDbFree),
+ FN_SYM(PkgFnsT, load, pkgDbLoad),
+ FN_SYM(PkgFnsT, owner, pkgDbOwner)
+};
+
+// A row of instrument banks: the files it lists (ext, and altExt when it
+// is not NULL), the DVX.INI [audio] key it sets (not set: the standard
+// one), the bank its standard one is as musicSetBank takes it ("" for the
+// bundled FM bank), and whether it offers the sound cards' own banks
+typedef struct {
+ const char *key;
+ const char *ext;
+ const char *altExt;
+ const char *standard;
+ bool cardBanks;
+ WidgetT *drop;
+ ChoicesT choices; // as stored ("" for the standard one)
+} BankRowT;
+
// The sound files listed from the sounds folder
static const char *sSoundExts[] = { ".wav", ".voc" };
@@ -123,6 +204,9 @@ static const char *sSoundExts[] = { ".wav", ".voc" };
// ============================================================
static SndFnsT sSnd;
+static MusFnsT sMus;
+static PkgFnsT sPkg;
+static MusicT *sBankTune = NULL; // an instruments Test playing
static AppContextT *sAc = NULL;
static PrefsHandleT *sPrefs = NULL;
static WidgetT *sEventList = NULL;
@@ -131,26 +215,36 @@ static WidgetT *sVolSldr = NULL;
static WidgetT *sVolLbl = NULL;
static WidgetT *sDrvDrop = NULL;
static WidgetT *sMidiDrop = NULL;
-static WidgetT *sBankDrop = NULL;
+static WidgetT *sMidiInDrop = NULL;
+static WidgetT *sModDrop = NULL;
static ChoicesT sFiles; // the sound files as stored ("" for none)
-static ChoicesT sBanks; // the MIDI banks as stored ("" for the standard one)
+static BankRowT sFmBanks = { MUSIC_INI_BANK, ".op2", NULL, "", false, NULL, { NULL, NULL, NULL } };
+static BankRowT sSoundFonts = { MUSIC_INI_SOUND_FONT, ".sf2", ".sbk", MUSIC_SOUND_FONT_DEFAULT, true, NULL, { NULL, NULL, NULL } };
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 const char *sMidiLabels[AUDIO_MIDI_CHOICE_COUNT];
+static const char *sMidiInLabels[AUDIO_MIDI_IN_CHOICE_COUNT];
+static const char *sModLabels[AUDIO_MODULES_CHOICE_COUNT];
static int32_t sChoice[SoundCountE];
// ============================================================
// Prototypes
// ============================================================
-static int32_t addBanks(PrefsHandleT *prefs);
+static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs);
+static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs);
static int32_t addDriver(const char *name, const char *label);
static int32_t addFile(const char *path);
+static void addFound(BankRowT *row, BankFindFnT *find, const char *label);
+static void addPairGap(WidgetT *row);
static WidgetT *addRow(WidgetT *parent, const char *label);
static void addSoundsDir(void);
+static void bankLabel(const char *name, const BankRowT *row, const PkgDbT *db, char *out, int32_t outSize);
+static bool bankOfRow(const BankRowT *row, const char *name);
static int32_t choiceAdd(ChoicesT *c, const char *value, const char *emptyLabel);
+static void choiceSetLabel(ChoicesT *c, int32_t i, const char *label);
static void choicesFree(ChoicesT *c);
static int compareNames(const void *a, const void *b);
void cpSoundsApply(void);
@@ -158,33 +252,72 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *pref
void cpSoundsFree(void);
void cpSoundsRevert(void);
void cpSoundsStore(PrefsHandleT *prefs);
+static void onBankTest(WidgetT *w);
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 bool resolveFns(const FnSymT *syms, int32_t count, void *fns, size_t size);
static void showVolume(void);
-// The MIDI instrument banks: the standard one, those in the banks folders
-// (by the names DVX.INI keeps), and DVX.INI's own choice when it is none
-// of them (so OK keeps it). Returns DVX.INI's choice's item.
-static int32_t addBanks(PrefsHandleT *prefs) {
+// A row of banks (its dropdown, and its Test when the music library is
+// there).
+static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs) {
+ int32_t sel = addBanks(row, prefs);
+ WidgetT *box = addRow(page, label);
+
+ row->drop = wgtDropdown(box);
+ row->drop->weight = WGT_WEIGHT_FILL;
+ wgtDropdownSetItems(row->drop, row->choices.labels, (int32_t)arrlen(row->choices.labels));
+ wgtDropdownSetSelected(row->drop, sel);
+
+ if (sMus.loadMacro) {
+ WidgetT *btn = wgtButton(box, "Test");
+
+ btn->onClick = onBankTest;
+ btn->userData = row;
+ btn->minW = wgtPixels(CPS_BTN_W);
+ }
+}
+
+
+// A row's banks: the standard one, those of its kind in the banks folders
+// (by the names DVX.INI keeps, shown by bankLabel; the standard one not
+// twice), the sound cards' own banks where the row offers them, and
+// DVX.INI's own choice when it is none of them (so OK keeps it). Returns
+// DVX.INI's choice's item.
+static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs) {
char **names = platformKindReadDir(DvxKindBanksE);
+ PkgDbT db;
+ bool haveDb = sPkg.load && sPkg.load(&db, "");
int32_t sel;
- choiceAdd(&sBanks, "", CPS_BANK_LABEL);
+ choiceAdd(&row->choices, "", CPS_BANK_LABEL);
qsort(names, arrlen(names), sizeof(names[0]), compareNames);
for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
- if (dvxHasExt(names[i], CPS_BANK_EXT)) {
- choiceAdd(&sBanks, names[i], CPS_BANK_LABEL);
+ if (bankOfRow(row, names[i]) && strcasecmp(names[i], row->standard) != 0) {
+ char label[DVX_MAX_PATH];
+
+ bankLabel(names[i], row, haveDb ? &db : NULL, label, sizeof(label));
+ choiceSetLabel(&row->choices, choiceAdd(&row->choices, names[i], CPS_BANK_LABEL), label);
}
}
+ if (row->cardBanks) {
+ addFound(row, sMus.patchSet, CPS_PATCHES);
+ addFound(row, sMus.romBank, CPS_ROM_BANK);
+ }
+
+ // Partly loaded when out of memory: freed whenever it was loaded at all
+ if (sPkg.load) {
+ sPkg.release(&db);
+ }
+
dvxReadDirFree(names);
- sel = choiceAdd(&sBanks, prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK, ""), CPS_BANK_LABEL);
+ sel = choiceAdd(&row->choices, prefsGetString(prefs, AUDIO_INI_SECTION, row->key, ""), CPS_BANK_LABEL);
return sel;
}
@@ -213,13 +346,34 @@ static int32_t addFile(const char *path) {
}
+// A sound card's own bank, when find (NULL without the music library)
+// finds it: an item by its path, shown as label.
+static void addFound(BankRowT *row, BankFindFnT *find, const char *label) {
+ char path[DVX_MAX_PATH];
+
+ if (find && find(path, sizeof(path))) {
+ choiceSetLabel(&row->choices, choiceAdd(&row->choices, path, CPS_BANK_LABEL), label);
+ }
+}
+
+
+// A fixed gap before the second setting on a row (what is left of the
+// row stays with the first).
+static void addPairGap(WidgetT *row) {
+ WidgetT *gap = wgtSpacer(row);
+
+ gap->weight = 0;
+ gap->minW = wgtPixels(CPS_PAIR_GAP);
+}
+
+
// "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);
+ lbl->minW = wgtPixels(CPS_LABEL_W);
return row;
}
@@ -242,6 +396,50 @@ static void addSoundsDir(void) {
}
+// How a bank is shown: as the name of the add-on that installed it (db,
+// when the package library is there), with the bank's own name after it
+// when that add-on has more than one of the row's; else as its name
+// without the extension.
+static void bankLabel(const char *name, const BankRowT *row, const PkgDbT *db, char *out, int32_t outSize) {
+ char title[DVX_MAX_PATH];
+ const PkgInstalledT *owner = NULL;
+ int32_t banks = 0;
+ const char *dot;
+
+ snprintf(title, sizeof(title), "%s", name);
+ dot = strrchr(platformPathBaseName(title), '.');
+
+ if (dot) {
+ title[dot - title] = '\0';
+ }
+
+ if (db) {
+ char path[DVX_MAX_PATH];
+
+ snprintf(path, sizeof(path), DVX_SHARED_DIR DVX_PATH_SEP DVX_BANKS_DIR DVX_PATH_SEP "%s", name);
+ owner = sPkg.owner(db, path, NULL);
+ }
+
+ for (int32_t i = 0; owner && i < (int32_t)arrlen(owner->manifest.files); i++) {
+ banks += bankOfRow(row, owner->manifest.files[i].name) ? 1 : 0;
+ }
+
+ if (!owner) {
+ snprintf(out, (size_t)outSize, "%s", title);
+ } else if (banks > 1) {
+ snprintf(out, (size_t)outSize, "%s: %s", owner->manifest.name, platformPathBaseName(title));
+ } else {
+ snprintf(out, (size_t)outSize, "%s", owner->manifest.name);
+ }
+}
+
+
+// name is one of the row's kind of bank (by its extension).
+static bool bankOfRow(const BankRowT *row, const char *name) {
+ return dvxHasExt(name, row->ext) || (row->altExt && dvxHasExt(name, row->altExt));
+}
+
+
// The item of value, added when it is not there yet (told apart ignoring
// case: DOS names have none); "" shows as emptyLabel. Out of memory: the
// first item.
@@ -266,15 +464,34 @@ static int32_t choiceAdd(ChoicesT *c, const char *value, const char *emptyLabel)
}
+// Show item i as label (a copy, freed with the choices).
+static void choiceSetLabel(ChoicesT *c, int32_t i, const char *label) {
+ char *copy = strdup(label);
+
+ if (copy && i >= 0 && i < (int32_t)arrlen(c->labels)) {
+ arrput(c->owned, copy);
+ c->labels[i] = copy;
+ } else {
+ free(copy);
+ }
+}
+
+
static void choicesFree(ChoicesT *c) {
for (int32_t i = 0; i < (int32_t)arrlen(c->values); i++) {
free(c->values[i]);
}
+ for (int32_t i = 0; i < (int32_t)arrlen(c->owned); i++) {
+ free(c->owned[i]);
+ }
+
arrfree(c->values);
arrfree(c->labels);
+ arrfree(c->owned);
c->values = NULL;
c->labels = NULL;
+ c->owned = NULL;
}
@@ -294,11 +511,14 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
sAc = ac;
sPrefs = prefs;
- if (!resolveLibrary()) {
+ if (!resolveFns(sSndSyms, DVX_ARRAY_LEN(sSndSyms), &sSnd, sizeof(sSnd))) {
wgtLabel(page, "Sound is not installed.");
return;
}
+ resolveFns(sMusSyms, DVX_ARRAY_LEN(sMusSyms), &sMus, sizeof(sMus));
+ resolveFns(sPkgSyms, DVX_ARRAY_LEN(sPkgSyms), &sPkg, sizeof(sPkg));
+
addFile("");
addSoundsDir();
@@ -309,7 +529,6 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
wgtLabel(page, "Events:");
sEventList = wgtListBox(page);
- sEventList->prefH = wgtPixels(CPS_LIST_H);
sEventList->onChange = onEvent;
wgtListBoxSetItems(sEventList, sEventLabels, SoundCountE);
@@ -323,19 +542,10 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
WidgetT *btn = wgtButton(row, "Browse...");
btn->onClick = onBrowse;
- btn->prefW = wgtPixels(CPS_BTN_W);
+ btn->minW = 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, sSnd.volume(prefs));
- showVolume();
+ btn->minW = wgtPixels(CPS_BTN_W);
// Driver: "Automatic", every installed driver, and DVX.INI's own
// choice even when that driver is missing (so OK keeps it).
@@ -359,20 +569,42 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
drvSel = addDriver(want, NULL);
}
- sDrvDrop = wgtDropdown(addRow(page, "Sound driver:"));
+ // The volume and the driver share a row (the page fits 640x480)
+ row = addRow(page, "Volume:");
+ sVolSldr = wgtSlider(row, 0, PERCENT_MAX);
+ sVolSldr->weight = WGT_WEIGHT_FILL;
+ sVolSldr->onChange = onVolume;
+ sVolLbl = wgtLabel(row, "");
+ sVolLbl->minW = wgtPixels(CPS_VOL_LBL_W);
+ wgtSliderSetValue(sVolSldr, sSnd.volume(prefs));
+ showVolume();
+ addPairGap(row);
+ wgtLabel(row, "Sound driver:");
+ sDrvDrop = wgtDropdown(row);
wgtDropdownSetItems(sDrvDrop, sDrvLabels, sDrvCount);
wgtDropdownSetSelected(sDrvDrop, drvSel);
+ // Where MIDI music plays and where MIDI comes in share a row (the page
+ // has no height to spare)
prefsChoiceLabels(sSnd.midiChoices, AUDIO_MIDI_CHOICE_COUNT, sMidiLabels);
- sMidiDrop = wgtDropdown(addRow(page, "MIDI music:"));
+ row = addRow(page, "MIDI music:");
+ sMidiDrop = wgtDropdown(row);
wgtDropdownSetItems(sMidiDrop, sMidiLabels, AUDIO_MIDI_CHOICE_COUNT);
wgtDropdownSetSelected(sMidiDrop, sSnd.midi(prefs));
+ addPairGap(row);
+ wgtLabel(row, "MIDI in:");
+ prefsChoiceLabels(sSnd.midiInChoices, AUDIO_MIDI_IN_CHOICE_COUNT, sMidiInLabels);
+ sMidiInDrop = wgtDropdown(row);
+ wgtDropdownSetItems(sMidiInDrop, sMidiInLabels, AUDIO_MIDI_IN_CHOICE_COUNT);
+ wgtDropdownSetSelected(sMidiInDrop, sSnd.midiIn(prefs));
- int32_t bankSel = addBanks(prefs);
+ prefsChoiceLabels(sSnd.modulesChoices, AUDIO_MODULES_CHOICE_COUNT, sModLabels);
+ sModDrop = wgtDropdown(addRow(page, "Modules:"));
+ wgtDropdownSetItems(sModDrop, sModLabels, AUDIO_MODULES_CHOICE_COUNT);
+ wgtDropdownSetSelected(sModDrop, sSnd.modules(prefs));
- sBankDrop = wgtDropdown(addRow(page, "Instruments:"));
- wgtDropdownSetItems(sBankDrop, sBanks.labels, (int32_t)arrlen(sBanks.labels));
- wgtDropdownSetSelected(sBankDrop, bankSel);
+ addBankRow(page, "Instruments:", &sFmBanks, prefs);
+ addBankRow(page, "SoundFont:", &sSoundFonts, prefs);
AudioInfoT info;
char text[CPS_TEXT_BUF];
@@ -388,8 +620,14 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
void cpSoundsFree(void) {
+ if (sMus.release) {
+ sMus.release(sBankTune);
+ }
+
+ sBankTune = NULL;
choicesFree(&sFiles);
- choicesFree(&sBanks);
+ choicesFree(&sFmBanks.choices);
+ choicesFree(&sSoundFonts.choices);
}
@@ -409,16 +647,48 @@ void cpSoundsStore(PrefsHandleT *prefs) {
sSnd.setFile(prefs, (DvxSoundE)i, sFiles.values[sChoice[i]]);
}
- // The standard bank is no setting at all
- const char *bank = sBanks.values[wgtDropdownGetSelected(sBankDrop)];
+ // A standard bank is no setting at all
+ const BankRowT *rows[] = { &sFmBanks, &sSoundFonts };
- if (bank[0]) {
- prefsSetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK, bank);
- } else {
- prefsRemove(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK);
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(rows); i++) {
+ const char *bank = rows[i]->choices.values[wgtDropdownGetSelected(rows[i]->drop)];
+
+ if (bank[0]) {
+ prefsSetString(prefs, AUDIO_INI_SECTION, rows[i]->key, bank);
+ } else {
+ prefsRemove(prefs, AUDIO_INI_SECTION, rows[i]->key);
+ }
}
- sSnd.store(prefs, sDrvNames[wgtDropdownGetSelected(sDrvDrop)], wgtDropdownGetSelected(sMidiDrop), wgtSliderGetValue(sVolSldr));
+ sSnd.store(prefs, sDrvNames[wgtDropdownGetSelected(sDrvDrop)], wgtDropdownGetSelected(sMidiDrop), wgtDropdownGetSelected(sModDrop), wgtSliderGetValue(sVolSldr));
+ sSnd.midiInStore(prefs, wgtDropdownGetSelected(sMidiInDrop));
+}
+
+
+// Play CPS_BANK_TUNE on the bank selected in the button's row (stored or
+// not), in place of the tune a press before started.
+static void onBankTest(WidgetT *w) {
+ const BankRowT *row = (const BankRowT *)w->userData;
+ const char *bank = row->choices.values[wgtDropdownGetSelected(row->drop)];
+ int32_t rc;
+
+ sMus.release(sBankTune);
+ sBankTune = NULL;
+ rc = sMus.loadMacro(CPS_BANK_TUNE, &sBankTune);
+
+ if (rc == MUSIC_OK) {
+ rc = sMus.setBank(sBankTune, bank[0] ? bank : row->standard);
+ }
+
+ if (rc == MUSIC_OK) {
+ rc = sMus.play(sBankTune, false);
+ }
+
+ if (rc != MUSIC_OK) {
+ sMus.release(sBankTune);
+ sBankTune = NULL;
+ dvxMessageBox(sAc, CPS_BANK_TITLE, sMus.errorText(rc), MB_OK | MB_ICONWARNING);
+ }
}
@@ -493,16 +763,18 @@ static void onVolume(WidgetT *w) {
}
-static bool resolveLibrary(void) {
- for (int32_t i = 0; i < DVX_ARRAY_LEN(sSyms); i++) {
- void *fn = dlsym(RTLD_DEFAULT, sSyms[i].name);
+// Fill fns (size bytes) from the functions syms names; false, with fns
+// cleared, when the library is not there (any one is missing).
+static bool resolveFns(const FnSymT *syms, int32_t count, void *fns, size_t size) {
+ for (int32_t i = 0; i < count; i++) {
+ void *fn = dlsym(RTLD_DEFAULT, syms[i].name);
if (!fn) {
- memset(&sSnd, 0, sizeof(sSnd));
+ memset(fns, 0, size);
return false;
}
- memcpy((char *)&sSnd + sSyms[i].offset, &fn, sizeof(fn));
+ memcpy((char *)fns + syms[i].offset, &fn, sizeof(fn));
}
return true;
diff --git a/src/apps/kpunch/cpanel/cpTypes.c b/src/apps/kpunch/cpanel/cpTypes.c
index 064814b6..dbe289d3 100644
--- a/src/apps/kpunch/cpanel/cpTypes.c
+++ b/src/apps/kpunch/cpanel/cpTypes.c
@@ -146,17 +146,17 @@ void cpTypesBuild(WidgetT *page) {
WidgetT *addBtn = wgtButton(btnRow, "&Add...");
- addBtn->prefW = wgtPixels(CT_BTN_W);
+ addBtn->minW = wgtPixels(CT_BTN_W);
addBtn->onClick = onAdd;
WidgetT *changeBtn = wgtButton(btnRow, "C&hange...");
- changeBtn->prefW = wgtPixels(CT_BTN_W);
+ changeBtn->minW = wgtPixels(CT_BTN_W);
changeBtn->onClick = onChange;
WidgetT *removeBtn = wgtButton(btnRow, "&Remove");
- removeBtn->prefW = wgtPixels(CT_BTN_W);
+ removeBtn->minW = wgtPixels(CT_BTN_W);
removeBtn->onClick = onRemove;
}
@@ -211,6 +211,7 @@ static void onBrowse(WidgetT *w) {
TypeDlgT *td = (TypeDlgT *)w->userData;
const FileFilterT filters[] = {
{ "Programs (*.app)" },
+ { SHELL_DOS_FILTER },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
diff --git a/src/apps/kpunch/cpanel/cpanel.c b/src/apps/kpunch/cpanel/cpanel.c
index 4bbbc7f6..13593820 100644
--- a/src/apps/kpunch/cpanel/cpanel.c
+++ b/src/apps/kpunch/cpanel/cpanel.c
@@ -25,7 +25,7 @@
// System configuration app with these tabs:
// Mouse -- scroll direction, wheel speed, double-click speed, acceleration, cursor speed
// Colors -- every system color, theme load/save
-// Desktop -- wallpaper image (stretch mode)
+// Desktop -- wallpaper image (stretch mode), the screen saver
// Video -- resolution and color depth
// File Types -- which program opens each kind of document (cpTypes.c)
// Network -- TCP/IP settings and the network's state (cpNet.c)
@@ -50,10 +50,13 @@
#include "listBox/listBox.h"
#include "slider/slider.h"
#include "spacer/spacer.h"
+#include "spinner/spinner.h"
#include "tabControl/tabCtrl.h"
#include "dvxWm.h"
#include "dvxPlat.h"
#include "shellApp.h"
+#include "shellProg.h"
+#include "shellSaver.h"
#include "cpJoystick.h"
#include "cpNet.h"
#include "cpPrinter.h"
@@ -94,6 +97,7 @@
#define CP_REVERT_PREFIX "The old mode comes back in "
#define CP_REVERT_SUFFIX " seconds."
#define CP_WPAPER_BTN_W 90
+#define CP_SAVER_NONE "(None)"
#define CP_LABEL_BUF 48
#define CP_FILTER_BUF 128
#define CP_ENTRY_NAME_MAX 64
@@ -171,6 +175,11 @@ static WidgetT *sWpaperList = NULL;
static WidgetT *sWpModeDrop = NULL;
static FileEntryT *sWpaperEntries = NULL; // stb_ds dynamic array
static const char **sWpaperLabels = NULL; // stb_ds dynamic array
+static WidgetT *sSaverDrop = NULL;
+static WidgetT *sSaverWait = NULL;
+static WidgetT *sSaverTest = NULL;
+static FileEntryT *sSaverEntries = NULL; // stb_ds dynamic array: (None), then the savers (path: the name SAVERS knows)
+static const char **sSaverLabels = NULL; // stb_ds dynamic array
// Video tab
static const VideoModeInfoT *sVideoModes = NULL; // points into ctx, not owned
@@ -190,12 +199,15 @@ static WidgetT *sThemeList = NULL;
// Prototypes
// ============================================================
+static void addGap(WidgetT *page);
static void applyMouseConfig(void);
void appShutdown(void);
static void buildColorsTab(WidgetT *page);
static void buildDesktopTab(WidgetT *page);
static void buildMouseTab(WidgetT *page);
static void buildVideoTab(WidgetT *page);
+static void entriesClear(FileEntryT **entries, const char ***labels);
+static void entriesLabels(const FileEntryT *entries, const char ***labels);
static int entryNameCompare(const void *a, const void *b);
static void onAccelChange(WidgetT *w);
static void onApplyTheme(WidgetT *w);
@@ -212,6 +224,8 @@ static void onDblClickTest(WidgetT *w);
static void onMenu(WindowT *win, int32_t menuId);
static void onOk(WidgetT *w);
static void onResetColors(WidgetT *w);
+static void onSaverChange(WidgetT *w);
+static void onSaverTest(WidgetT *w);
static void onSaveTheme(WidgetT *w);
static void onSmoothText(WidgetT *w);
static void onSpeedSlider(WidgetT *w);
@@ -223,8 +237,10 @@ static void onWallpaperMode(WidgetT *w);
static void onWheelChange(WidgetT *w);
static void onWheelStepSlider(WidgetT *w);
static void restoreSnapshot(void);
+static int32_t saverIndex(void);
static void saveSnapshot(void);
static void scanKindEntries(DvxKindE kind, const char *const *exts, int32_t extCount, bool stripExt, FileEntryT **entries, const char ***labels);
+static void scanSavers(void);
static void scanThemes(void);
static void scanWallpapers(void);
static void setIntLabel(WidgetT *lbl, const char *prefix, int32_t val, const char *suffix);
@@ -249,6 +265,15 @@ SHELL_APP_DESCRIPTOR(
);
+// A small fixed gap between a page's rows.
+static void addGap(WidgetT *page) {
+ WidgetT *gap = wgtSpacer(page);
+
+ gap->weight = 0;
+ gap->minH = wgtPixels(CP_SPACING_SMALL);
+}
+
+
static void applyMouseConfig(void) {
int32_t dirIdx = wgtDropdownGetSelected(sWheelDrop);
int32_t dbl = wgtSliderGetValue(sDblClickSldr);
@@ -286,6 +311,10 @@ void appShutdown(void) {
sWpaperEntries = NULL;
arrfree(sWpaperLabels);
sWpaperLabels = NULL;
+ arrfree(sSaverEntries);
+ sSaverEntries = NULL;
+ arrfree(sSaverLabels);
+ sSaverLabels = NULL;
arrfree(sVideoLabels);
sVideoLabels = NULL;
arrfree(sLabelBufs);
@@ -340,19 +369,19 @@ static void buildColorsTab(WidgetT *page) {
WidgetT *loadBtn = wgtButton(themeRow, "Apply");
loadBtn->onClick = onApplyTheme;
- loadBtn->prefW = wgtPixels(CP_THEME_BTN_W);
+ loadBtn->minW = wgtPixels(CP_THEME_BTN_W);
WidgetT *browseBtn = wgtButton(themeRow, "Load...");
browseBtn->onClick = onBrowseTheme;
- browseBtn->prefW = wgtPixels(CP_THEME_BTN_W);
+ browseBtn->minW = wgtPixels(CP_THEME_BTN_W);
WidgetT *saveBtn = wgtButton(themeRow, "Save As...");
saveBtn->onClick = onSaveTheme;
- saveBtn->prefW = wgtPixels(CP_SAVE_BTN_W);
+ saveBtn->minW = wgtPixels(CP_SAVE_BTN_W);
WidgetT *resetBtn = wgtButton(themeRow, "Reset");
resetBtn->onClick = onResetColors;
- resetBtn->prefW = wgtPixels(CP_THEME_BTN_W);
+ resetBtn->minW = wgtPixels(CP_THEME_BTN_W);
// Right side: the selected color and its values, in a non-weighted
// inner box so they stay at natural size; the outer box absorbs extra
@@ -394,15 +423,15 @@ static void buildDesktopTab(WidgetT *page) {
WidgetT *applyBtn = wgtButton(btnRow, "Apply");
applyBtn->onClick = onApplyWallpaper;
- applyBtn->prefW = wgtPixels(CP_WPAPER_BTN_W);
+ applyBtn->minW = wgtPixels(CP_WPAPER_BTN_W);
WidgetT *chooseBtn = wgtButton(btnRow, "Browse...");
chooseBtn->onClick = onChooseWallpaper;
- chooseBtn->prefW = wgtPixels(CP_WPAPER_BTN_W);
+ chooseBtn->minW = wgtPixels(CP_WPAPER_BTN_W);
WidgetT *clearBtn = wgtButton(btnRow, "Clear");
clearBtn->onClick = onClearWallpaper;
- clearBtn->prefW = wgtPixels(CP_WPAPER_BTN_W);
+ clearBtn->minW = wgtPixels(CP_WPAPER_BTN_W);
wgtSpacer(btnRow)->weight = WGT_WEIGHT_FILL;
wgtLabel(btnRow, "Mode:");
@@ -415,6 +444,29 @@ static void buildDesktopTab(WidgetT *page) {
sWpModeDrop->onChange = onWallpaperMode;
wgtDropdownSetItems(sWpModeDrop, modeItems, PREFS_WALLPAPER_CHOICE_COUNT);
wgtDropdownSetSelected(sWpModeDrop, (int32_t)sAc->wallpaperMode);
+
+ // The screen saver: (None) or one of the savers by its name, the
+ // wait, and Test (shellSaver.h). Live like the rest.
+ WidgetT *saverRow = wgtHBox(page);
+ saverRow->spacing = wgtPixels(CP_SPACING);
+ wgtLabel(saverRow, "Screen sa&ver:");
+
+ scanSavers();
+ sSaverDrop = wgtDropdown(saverRow);
+ sSaverDrop->weight = WGT_WEIGHT_FILL;
+ sSaverDrop->onChange = onSaverChange;
+ wgtDropdownSetItems(sSaverDrop, sSaverLabels, arrlen(sSaverLabels));
+ wgtDropdownSetSelected(sSaverDrop, saverIndex());
+
+ wgtLabel(saverRow, "&Wait:");
+ sSaverWait = wgtSpinner(saverRow, SHELL_SAVER_WAIT_MIN, SHELL_SAVER_WAIT_MAX, 1);
+ sSaverWait->onChange = onSaverChange;
+ wgtSpinnerSetValue(sSaverWait, shellSaverWait());
+ wgtLabel(saverRow, "minutes");
+
+ sSaverTest = wgtButton(saverRow, "&Test");
+ sSaverTest->onClick = onSaverTest;
+ wgtSetEnabled(sSaverTest, shellSaverName()[0] != '\0');
}
@@ -436,7 +488,7 @@ static void buildMouseTab(WidgetT *page) {
wgtDropdownSetItems(sWheelDrop, wheelItems, PREFS_WHEEL_DIR_CHOICE_COUNT);
wgtDropdownSetSelected(sWheelDrop, prefsChoiceIndexOfValue(prefsWheelDirChoices, PREFS_WHEEL_DIR_CHOICE_COUNT, sAc->wheelDirection));
- wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
+ addGap(page);
// Wheel speed (lines per notch)
wgtLabel(page, "Scroll Speed:");
@@ -451,11 +503,11 @@ static void buildMouseTab(WidgetT *page) {
wgtSliderSetValue(sWheelStepSldr, sAc->wheelStep);
wgtLabel(wheelStepRow, "Fast ");
- sWheelStepLbl = wgtLabel(wheelStepRow, "");
- sWheelStepLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
+ sWheelStepLbl = wgtLabel(wheelStepRow, "");
+ sWheelStepLbl->minW = wgtPixels(CP_DBLCLICK_LBL_W);
updateWheelStepLabel();
- wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
+ addGap(page);
// Double-click speed
wgtLabel(page, "Double-Click Speed:");
@@ -470,11 +522,11 @@ static void buildMouseTab(WidgetT *page) {
wgtSliderSetValue(sDblClickSldr, sAc->dblClickMs);
wgtLabel(dblRow, "Slow ");
- sDblClickLbl = wgtLabel(dblRow, "");
- sDblClickLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
+ sDblClickLbl = wgtLabel(dblRow, "");
+ sDblClickLbl->minW = wgtPixels(CP_DBLCLICK_LBL_W);
updateDblClickLabel();
- wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
+ addGap(page);
// Acceleration
WidgetT *accelRow = wgtHBox(page);
@@ -491,7 +543,7 @@ static void buildMouseTab(WidgetT *page) {
wgtDropdownSetItems(sAccelDrop, accelItems, PREFS_ACCEL_CHOICE_COUNT);
wgtDropdownSetSelected(sAccelDrop, prefsChoiceIndexOfValue(prefsAccelChoices, PREFS_ACCEL_CHOICE_COUNT, sAc->mouseAccel));
- wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
+ addGap(page);
// Speed (mickey-to-pixel ratio)
wgtLabel(page, "Cursor Speed:");
@@ -508,11 +560,11 @@ static void buildMouseTab(WidgetT *page) {
// INI 'speed' key use); convert to the inverted slider position.
wgtSliderSetValue(sSpeedSldr, CP_SPEED_INVERT - sAc->mouseMickeys);
wgtLabel(speedRow, "Fast ");
- sSpeedLbl = wgtLabel(speedRow, "");
- sSpeedLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
+ sSpeedLbl = wgtLabel(speedRow, "");
+ sSpeedLbl->minW = wgtPixels(CP_DBLCLICK_LBL_W);
updateSpeedLabel();
- wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);
+ addGap(page);
// Double-click test area
WidgetT *testRow = wgtHBox(page);
@@ -520,7 +572,7 @@ static void buildMouseTab(WidgetT *page) {
WidgetT *testBtn = wgtButton(testRow, "Test Area");
testBtn->onDblClick = onDblClickTest;
- testBtn->prefW = wgtPixels(CP_TEST_BTN_W);
+ testBtn->minW = wgtPixels(CP_TEST_BTN_W);
sDblTestLbl = wgtLabel(testRow, "Double-click the button to test");
}
@@ -581,7 +633,7 @@ static void buildVideoTab(WidgetT *page) {
WidgetT *applyBtn = wgtButton(page, "Apply Mode");
applyBtn->onClick = onVideoApply;
- applyBtn->prefW = wgtPixels(CP_APPLY_BTN_W);
+ applyBtn->maxW = wgtPixels(CP_APPLY_BTN_W);
sSmoothChk = wgtCheckbox(page, "Smooth the edges of text (more than 256 colors)");
wgtCheckboxSetChecked(sSmoothChk, sAc->fontSmoothing);
@@ -589,6 +641,22 @@ static void buildVideoTab(WidgetT *page) {
}
+// A list's entries and labels emptied (kept allocated for the next scan).
+static void entriesClear(FileEntryT **entries, const char ***labels) {
+ arrsetlen(*entries, 0);
+ arrsetlen(*labels, 0);
+}
+
+
+// A list's labels: each entry's display name, in order (once the entries
+// are all there: the labels point into them).
+static void entriesLabels(const FileEntryT *entries, const char ***labels) {
+ for (int32_t i = 0; i < arrlen(entries); i++) {
+ arrput(*labels, entries[i].name);
+ }
+}
+
+
// qsort: entries by display name, case ignored (scanKindEntries)
static int entryNameCompare(const void *a, const void *b) {
return strcasecmp(((const FileEntryT *)a)->name, ((const FileEntryT *)b)->name);
@@ -789,6 +857,7 @@ static void onOk(WidgetT *w) {
dvxMouseToPrefs(sAc, sPrefs);
dvxColorsToPrefs(sAc, sPrefs);
dvxWallpaperToPrefs(sAc, sPrefs);
+ shellSaverToPrefs(sPrefs);
dvxFontsToPrefs(sAc, sPrefs);
prefsSetVideoMode(sPrefs, sAc->display.width, sAc->display.height, sAc->display.format.bitsPerPixel);
@@ -816,6 +885,21 @@ static void onResetColors(WidgetT *w) {
}
+static void onSaverChange(WidgetT *w) {
+ int32_t sel = wgtDropdownGetSelected(sSaverDrop);
+
+ (void)w;
+ shellSaverSet(sel > 0 && sel < arrlen(sSaverEntries) ? sSaverEntries[sel].path : "", wgtSpinnerGetValue(sSaverWait));
+ wgtSetEnabled(sSaverTest, shellSaverName()[0] != '\0');
+}
+
+
+static void onSaverTest(WidgetT *w) {
+ (void)w;
+ shellSaverStart();
+}
+
+
static void onSaveTheme(WidgetT *w) {
(void)w;
@@ -932,11 +1016,11 @@ static void onVideoApply(WidgetT *w) {
btnRow->spacing = wgtPixels(CP_SPACING);
WidgetT *yesBtn = wgtButton(btnRow, "&Keep");
- yesBtn->prefW = wgtPixels(CP_CONFIRM_BTN_W);
+ yesBtn->minW = wgtPixels(CP_CONFIRM_BTN_W);
yesBtn->onClick = onVideoConfirmYes;
WidgetT *noBtn = wgtButton(btnRow, "&Revert");
- noBtn->prefW = wgtPixels(CP_CONFIRM_BTN_W);
+ noBtn->minW = wgtPixels(CP_CONFIRM_BTN_W);
noBtn->onClick = onVideoConfirmNo;
dvxFitWindow(sAc, confirmWin);
@@ -1029,10 +1113,23 @@ static void restoreSnapshot(void) {
}
dvxWallpaperFromPrefs(sAc, sSnapshot);
+ shellSaverFromPrefs(sSnapshot);
dvxFontsFromPrefs(sAc, sSnapshot);
}
+// The list entry of the saver chosen; 0 ((None)) when there is none.
+static int32_t saverIndex(void) {
+ for (int32_t i = 1; i < arrlen(sSaverEntries); i++) {
+ if (strcasecmp(sSaverEntries[i].path, shellSaverName()) == 0) {
+ return i;
+ }
+ }
+
+ return 0;
+}
+
+
// The state Cancel goes back to, written like DVX.INI but kept in memory.
static void saveSnapshot(void) {
prefsClose(sSnapshot);
@@ -1042,6 +1139,7 @@ static void saveSnapshot(void) {
dvxColorsToPrefs(sAc, sSnapshot);
dvxMouseToPrefs(sAc, sSnapshot);
dvxWallpaperToPrefs(sAc, sSnapshot);
+ shellSaverToPrefs(sSnapshot);
dvxFontsToPrefs(sAc, sSnapshot);
prefsSetVideoMode(sSnapshot, sAc->display.width, sAc->display.height, sAc->display.format.bitsPerPixel);
}
@@ -1055,8 +1153,7 @@ static void saveSnapshot(void) {
// buffer are shortened for display only; the path always uses the
// original filename.
static void scanKindEntries(DvxKindE kind, const char *const *exts, int32_t extCount, bool stripExt, FileEntryT **entries, const char ***labels) {
- arrsetlen(*entries, 0);
- arrsetlen(*labels, 0);
+ entriesClear(entries, labels);
char **names = platformKindReadDir(kind);
int32_t n = (int32_t)arrlen(names);
@@ -1101,10 +1198,43 @@ static void scanKindEntries(DvxKindE kind, const char *const *exts, int32_t extC
qsort(*entries, (size_t)arrlen(*entries), sizeof((*entries)[0]), entryNameCompare);
}
- // Build label array now that the entry array is stable
- for (int32_t i = 0; i < arrlen(*entries); i++) {
- arrput(*labels, (*entries)[i].name);
+ entriesLabels(*entries, labels);
+}
+
+
+// (None), then every program in the SAVERS folders by its name (its
+// "name" resource), in file order; a saver chosen that is not in them
+// (named by a path) comes last.
+static void scanSavers(void) {
+ char **names = platformKindReadDir(DvxKindSaversE);
+ FileEntryT entry = { .name = CP_SAVER_NONE };
+
+ entriesClear(&sSaverEntries, &sSaverLabels);
+ arrput(sSaverEntries, entry);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
+ char path[DVX_MAX_PATH];
+ ShellProgramT info;
+
+ if (!shellIsProgram(names[i]) || !platformKindFind(path, sizeof(path), DvxKindSaversE, names[i])) {
+ continue;
+ }
+
+ shellProgramInfo(path, &info);
+ snprintf(entry.name, sizeof(entry.name), "%s", info.name);
+ snprintf(entry.path, sizeof(entry.path), "%s", names[i]);
+ arrput(sSaverEntries, entry);
}
+
+ dvxReadDirFree(names);
+
+ if (shellSaverName()[0] && saverIndex() == 0) {
+ snprintf(entry.name, sizeof(entry.name), "%s", shellSaverName());
+ snprintf(entry.path, sizeof(entry.path), "%s", shellSaverName());
+ arrput(sSaverEntries, entry);
+ }
+
+ entriesLabels(sSaverEntries, &sSaverLabels);
}
@@ -1236,11 +1366,11 @@ int32_t appMain(DxeAppContextT *ctx) {
WidgetT *okBtn = wgtButton(btnRow, "OK");
okBtn->onClick = onOk;
- okBtn->prefW = wgtPixels(CP_OK_CANCEL_BTN_W);
+ okBtn->minW = wgtPixels(CP_OK_CANCEL_BTN_W);
WidgetT *cancelBtn = wgtButton(btnRow, "Cancel");
cancelBtn->onClick = onCancel;
- cancelBtn->prefW = wgtPixels(CP_OK_CANCEL_BTN_W);
+ cancelBtn->minW = wgtPixels(CP_OK_CANCEL_BTN_W);
dvxFitWindow(sAc, sWin);
return 0;
diff --git a/src/apps/kpunch/cpanel/cpanel.dhs b/src/apps/kpunch/cpanel/cpanel.dhs
index 3b48cd80..8f1454cf 100644
--- a/src/apps/kpunch/cpanel/cpanel.dhs
+++ b/src/apps/kpunch/cpanel/cpanel.dhs
@@ -34,11 +34,11 @@
.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, 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.
+The Control Panel changes how DVX looks and works: the mouse, the screen colors, the desktop picture and the screen saver, 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
-.link cpanel.desktop Desktop: the picture behind the windows
+.link cpanel.desktop Desktop: the picture behind the windows, and the screen saver
.link cpanel.video Video: the screen resolution, number of colors, and text smoothing
.link cpanel.types File Types: which program opens each kind of document
.link cpanel.network Network: connecting DVX to a network, and its status
@@ -129,11 +129,13 @@ A theme is a saved set of all the colors. The row under the list works with them
Remember to click OK if you want to keep the colors after you close the Control Panel.
.topic cpanel.desktop
-.title Desktop Picture
+.title Desktop Picture and Screen Saver
.toc 0 Desktop
.index Wallpaper
.index Desktop Picture
.index Background
+.index Screen Saver
+.index Screensaver
.h1 Desktop Picture
@@ -158,6 +160,29 @@ The Mode list chooses how the picture fills the screen:
If a picture cannot be loaded, the Control Panel says so and the desktop stays as it was.
+.h2 Screen Saver
+
+A screen saver fills the screen with a moving picture when the computer has not been used for a while, and goes away when you press a key or move the mouse. The key or click that wakes the screen does nothing else, so it cannot change your work by accident.
+
+.list
+.item Screen saver: choose one of the screen savers, or (None) for no screen saver.
+.item Wait: the number of minutes, from 1 to 60, with no key pressed and no mouse movement before the screen saver starts.
+.item Test starts the screen saver now, so you can see it. Press a key or move the mouse to end it.
+.endlist
+
+The screen saver does not start while a message or a dialog box is waiting for an answer, or while a program (such as a game) is using the whole screen.
+
+DVX comes with four screen savers:
+
+.table
+ Screen saver What it shows
+ ------------ -------------
+ Blank Screen A black screen.
+ Marquee DVX's name drifting around the screen in changing colors.
+ Mystify Two bouncing shapes that leave trails of their outlines.
+ Starfield Stars flying toward you out of the middle of the screen.
+.endtable
+
.topic cpanel.video
.title Screen Resolution and Colors
.toc 0 Video
@@ -208,7 +233,7 @@ When you open a document (from the Program Manager, for example), DVX starts the
.table
Button What it does
------ ------------
- Add... Set up a new extension: type it (such as TXT), pick the program with Browse..., and click OK. An extension that is already listed gets the new program.
+ Add... Set up a new extension: type it (such as TXT), pick the program with Browse..., and click OK. An extension that is already listed gets the new program. The program can be a DOS program (a .EXE, .COM, or .BAT file): opening a document then runs it with DVX stepped aside, as the Program Manager's help describes under DOS Programs.
Change... Pick a different program for the selected extension. Double-clicking an extension does the same.
Remove Forget the selected extension. The program itself is not removed.
.endtable
@@ -282,8 +307,10 @@ The Printer tab tells DVX about your printer. Every program prints to the printe
.table
Setting What it does
------- ------------
- Printer model Text only works with any printer, but prints only text. Choose Epson 9-pin, Epson 24-pin, or HP LaserJet if your printer is one of those or works like one (most dot matrix printers work like an Epson, most laser printers like a LaserJet); then pictures can be printed too.
- Connected to The printer port the printer is plugged into: LPT1, LPT2, or LPT3. Choose File to print into a file instead.
+ Printer model Text only works with any printer, but prints only text. Choose one of the others if your printer is one of those or works like one (most dot matrix printers work like an Epson, most laser and ink jet printers like a LaserJet); then pictures can be printed too. HP LaserJet III or DeskJet prints pictures sharper and faster on laser and ink jet printers made since about 1990; choose HP LaserJet for an older LaserJet (the LaserJet II or Plus). For color, choose Epson 9-pin color or Epson 24-pin color for a dot matrix printer with a color ribbon, HP DeskJet 500C for a DeskJet that holds one color cartridge, or HP DeskJet 550C and later for one that holds a black and a color cartridge. Choose PostScript or PostScript (color) for a PostScript printer (often an Apple LaserWriter or a printer with a PostScript option); text prints on it too, and pages that programs draw are drawn by the printer itself, as sharp as it can print. Choose Apple ImageWriter II, or Apple ImageWriter II (color) with the four-color ribbon, for an ImageWriter II on a serial port. Pictures print in shades of grey on the models without color.
+ Connected to The port the printer is plugged into: LPT1, LPT2, or LPT3 for a parallel printer, COM1 to COM4 for a serial printer (such as the ImageWriter II). Choose File to print into a file instead.
+ Speed (baud) For a serial printer: how fast the port sends, to match the printer's own setting (the ImageWriter II's DIP switches offer 300 to 9600; 9600 is usual).
+ Handshake For a serial printer: how it tells the computer to wait. Hardware (DSR, Apple) suits Apple's ImageWriter cable; Hardware (CTS) a cable that wires the printer's ready line to CTS; XON / XOFF a printer set to send those characters.
File name The file printouts go into when Connected to is File: type it, or click Browse... to pick it. Each printout replaces it.
.endtable
@@ -347,6 +374,9 @@ Every change works at once, so you can see it in the picture. Like the other tab
.index Volume
.index Sound Driver
.index MIDI Music
+.index Modules
+.index SoundFont
+.index Instruments
.h1 Sounds
@@ -360,12 +390,16 @@ The Sounds tab chooses the sound DVX makes for each event, such as starting, end
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.
- Instruments The instrument sounds MIDI music uses on the music chip and the built-in synthesizer: Standard (the set that comes with DVX), or another instrument bank installed with an add-on package or put in the BANKS folder of your user folder.
+ Sound driver The sound hardware to use. Automatic finds it by itself (the usual choice); wss is a Windows Sound System card, sb a Sound Blaster, gus a Gravis UltraSound, lpt a small sound device plugged into the printer port (a Covox Speech Thing, a Disney Sound Source, or a Stereo-on-1), speaker the PC speaker, null no sound at all.
+ MIDI music Where MIDI music plays: Automatic (the recorded instruments of a Gravis UltraSound or Sound Blaster AWE card when it has them, else the music chip on the sound card, else DVX's own synthesizer that sounds much the same as the chip), a MIDI port with an external synthesizer, the FM synthesizer chip, the built-in synthesizer, or the built-in SoundFont synthesizer (recorded instruments played by the computer itself, on any sound card; it needs more memory and a faster computer).
+ MIDI in Where MIDI input comes from, for programs that read a MIDI keyboard (the Sound Recorder records one): None, or the MIDI port (the MPU-401 port on the sound card, or one like it).
+ Modules Where music modules (MOD, S3M, XM, and IT files) play: Wavetable synthesizer when free (on the voices of a Gravis UltraSound or Sound Blaster AWE card, which takes the computer little time, when no other music plays there and the module fits in the card's memory; otherwise mixed by the computer), or Mixed in software (always mixed by the computer, which takes much more of a slow computer's time but plays every effect a module has).
+ Instruments The instrument sounds MIDI music uses on the music chip and the built-in synthesizer: Standard (the set that comes with DVX), or another instrument bank. A bank installed with an add-on package (Software offers some) is shown by the add-on's name; one you put in the BANKS folder of your user folder, by its file's name.
+ SoundFont The recorded instruments MIDI music uses on a Gravis UltraSound or a Sound Blaster AWE card, and in the built-in SoundFont synthesizer: Standard (DVX's General MIDI SoundFont, an add-on package Software offers), a SoundFont you put in the BANKS folder of your user folder, Gravis UltraSound patches (the card's own instruments, when its software is installed), or Sound Blaster AWE ROM instruments (the instruments built into the card, when Creative's software is installed). Without the Standard SoundFont, the card's own instruments are used when it has them.
+ Test Beside Instruments and SoundFont: plays a short tune on the instruments shown, a few in turn, so you can hear them before you choose.
.endtable
-A new sound driver, MIDI music choice, or set of instruments takes effect when DVX next starts.
+A new set of instruments, SoundFont, or Modules choice is used by the next music that starts. A new sound driver, MIDI music, or MIDI in choice takes effect when DVX next starts.
.h2 The Events
diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile
index 2df422e5..68e1b008 100644
--- a/src/apps/kpunch/dvxbasic/Makefile
+++ b/src/apps/kpunch/dvxbasic/Makefile
@@ -72,7 +72,7 @@ RES_SRCS = $(LIBDVX)/dvxResource.c $(LIBDVX)/dvxFontFmt.c $(TOOLSDIR)/dvxRes
CLI_SRCS = basBuild.c $(COMP_SRCS) $(VM_LINK_SRCS) $(RES_SRCS) $(SUPPORT_SRCS)
# Runtime library objects (VM + values + form runtime + serialization)
-RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/formDesign.o $(OBJDIR)/formCtl.o $(OBJDIR)/frmParser.o $(OBJDIR)/frmModel.o $(OBJDIR)/frmModelBas.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o $(OBJDIR)/basRunHost.o
+RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/formPrinter.o $(OBJDIR)/formDesign.o $(OBJDIR)/formCtl.o $(OBJDIR)/frmParser.o $(OBJDIR)/frmModel.o $(OBJDIR)/frmModelBas.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o $(OBJDIR)/basRunHost.o
RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
@@ -311,7 +311,7 @@ clean: host-basrt-clean
# of the host build.
HOSTRT_DIR = $(HOST_OBJROOT)/basrt
HOSTRT_TARGET = $(HOST_OBJROOT)/basrt.a
-HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/formDesign.c formrt/formCtl.c formrt/frmParser.c formrt/frmModel.c formrt/frmModelBas.c formrt/basNativeHost.c formrt/basRunHost.c
+HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/formPrinter.c formrt/formDesign.c formrt/formCtl.c formrt/frmParser.c formrt/frmModel.c formrt/frmModelBas.c formrt/basNativeHost.c formrt/basRunHost.c
HOSTRT_OBJS = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS)))
HOSTRT_CFLAGS = $(HOST_CFLAGS) -Wno-stringop-truncation $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks
diff --git a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
index f6c10a10..ea9c0bf8 100644
--- a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
+++ b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
@@ -116,7 +116,7 @@ A UserControl's header (the Begin UserControl block of a .ctl) is read and chang
.h2 Running a Program (basRunHost.h, in basrt.lib)
-basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, and whether runtime errors show their own box. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
+basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, and whether it is a standalone program (the stub's own: the runtime owns the module, frees it with itself, and is ended through the shell's kill hook, shellRegisterAppKill, when the program is ended by force; the IDE and basdev end their runtimes themselves). basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.
diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
index 3e048704..1809eee0 100644
--- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h
+++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
@@ -230,6 +230,13 @@ typedef struct {
#define BAS_SCALE_HIMETRIC 8 // vbHimetric
#define BAS_SCALE_COUNT 9
+// The Printer object's Orientation and ColorMode (VB's constants)
+#define BAS_PRINTER_OBJ_NAME "Printer"
+#define BAS_PRINTER_PORTRAIT 1 // vbPRORPortrait (the only one)
+#define BAS_PRINTER_LANDSCAPE 2 // vbPRORLandscape
+#define BAS_PRINTER_MONO 1 // vbPRCMMonochrome
+#define BAS_PRINTER_COLOR 2 // vbPRCMColor
+
// PaintPicture's raster operations (VB's vb* opcode constants, Windows
// ternary raster ops): bits 16..23 hold the operation's truth table over
// the pattern (FillColor), the source and the destination pixels.
diff --git a/src/apps/kpunch/dvxbasic/compiler/compiler.c b/src/apps/kpunch/dvxbasic/compiler/compiler.c
index 938d0d85..949e54be 100644
--- a/src/apps/kpunch/dvxbasic/compiler/compiler.c
+++ b/src/apps/kpunch/dvxbasic/compiler/compiler.c
@@ -34,6 +34,7 @@
#include "dvxTypes.h"
#include "textHelp.h"
#include "../runtime/dates.h"
+#include "../runtime/gfx.h"
#include "thirdparty/stb_ds_wrap.h"
#include
@@ -200,6 +201,29 @@ static const PredefConstT sPredefConsts[] = {
{ "vbMillimeters", BAS_SCALE_MM },
{ "vbCentimeters", BAS_SCALE_CM },
{ "vbHimetric", BAS_SCALE_HIMETRIC },
+
+ // FillStyle and DrawStyle
+ { "vbFSSolid", BAS_GFX_FILL_SOLID },
+ { "vbFSTransparent", BAS_GFX_FILL_TRANSPARENT },
+ { "vbHorizontalLine", BAS_GFX_FILL_HORIZONTAL },
+ { "vbVerticalLine", BAS_GFX_FILL_VERTICAL },
+ { "vbUpwardDiagonal", BAS_GFX_FILL_UP_DIAGONAL },
+ { "vbDownwardDiagonal", BAS_GFX_FILL_DOWN_DIAGONAL },
+ { "vbCross", BAS_GFX_FILL_CROSS },
+ { "vbDiagonalCross", BAS_GFX_FILL_DIAGONAL_CROSS },
+ { "vbSolid", BAS_GFX_DRAW_SOLID },
+ { "vbDash", BAS_GFX_DRAW_DASH },
+ { "vbDot", BAS_GFX_DRAW_DOT },
+ { "vbDashDot", BAS_GFX_DRAW_DASH_DOT },
+ { "vbDashDotDot", BAS_GFX_DRAW_DASH_DOT_DOT },
+ { "vbInvisible", BAS_GFX_DRAW_INVISIBLE },
+ { "vbInsideSolid", BAS_GFX_DRAW_INSIDE_SOLID },
+
+ // The Printer object's Orientation and ColorMode
+ { "vbPRORPortrait", BAS_PRINTER_PORTRAIT },
+ { "vbPRORLandscape", BAS_PRINTER_LANDSCAPE },
+ { "vbPRCMMonochrome", BAS_PRINTER_MONO },
+ { "vbPRCMColor", BAS_PRINTER_COLOR },
{ "vbPicTypeNone", BAS_PIC_TYPE_NONE },
{ "vbPicTypeBitmap", BAS_PIC_TYPE_BITMAP },
{ "vbSrcCopy", BAS_ROP_SRCCOPY },
diff --git a/src/apps/kpunch/dvxbasic/ctrlover.dhs b/src/apps/kpunch/dvxbasic/ctrlover.dhs
index f3450096..187b187d 100644
--- a/src/apps/kpunch/dvxbasic/ctrlover.dhs
+++ b/src/apps/kpunch/dvxbasic/ctrlover.dhs
@@ -212,6 +212,7 @@ End Sub
.index CurrentX
.index CurrentY
.index DrawWidth
+.index DrawStyle
.index FillColor
.index FillStyle
.index FontScale
@@ -382,7 +383,8 @@ End Sub
CurrentX Single 0 Where the next drawing or printing starts.
CurrentY Single 0
DrawWidth Integer 1 Pen width in pixels.
- FillStyle Integer 1 How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour.
+ DrawStyle Integer 0 The pen's lines and outlines: 0 solid, 1 dashes, 2 dots, 3 dash-dot, 4 dash-dot-dot (a pen 1 pixel wide only; a wider one draws solid), 5 none at all (fills are still drawn), 6 solid. Other values are an error. The constants vbSolid to vbInsideSolid name them.
+ FillStyle Integer 1 How boxes (B) and circles are filled with FillColor: 0 solid, 1 (the default) not at all, 2 horizontal lines, 3 vertical lines, 4 upward diagonals, 5 downward diagonals, 6 a cross hatch, 7 a diagonal cross hatch (the lines 8 pixels apart, the gaps left as they are). Other values are an error. BF fills solid in the line's colour. The constants vbFSSolid to vbDiagonalCross name them.
FillColor Long &H000000 Colour boxes and circles are filled with.
FontScale Integer 1 How many times as large Print writes its text (1 to 8): each dot of the font becomes a FontScale x FontScale block, for titles and banners. TextWidth and TextHeight follow it; PrintAccel does not.
AutoRedraw Boolean False True: no Paint events (see above).
diff --git a/src/apps/kpunch/dvxbasic/form.dhs b/src/apps/kpunch/dvxbasic/form.dhs
index 15439b35..ed669675 100644
--- a/src/apps/kpunch/dvxbasic/form.dhs
+++ b/src/apps/kpunch/dvxbasic/form.dhs
@@ -201,7 +201,7 @@ End Sub
.h2 Drawing
-Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle, and so on.
+Forms have Visual Basic's drawing methods and properties (Cls, PSet, Line, Circle, Point, Print, PaintPicture, DrawFocusRect, DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, Scale, CurrentX, CurrentY, ForeColor, BackColor, DrawWidth, DrawStyle, FillColor, FillStyle, FontScale, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight): see Drawing on Forms and PictureBoxes. In the form's own code use Me.Line, Me.Circle, and so on.
.h2 Example
diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.c b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c
index feebf42f..a4b0f811 100644
--- a/src/apps/kpunch/dvxbasic/formrt/basRunHost.c
+++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c
@@ -24,11 +24,14 @@
#include "basRunHost.h"
#include "../basRes.h"
+#include "../runtime/serialize.h"
#include "dvxPlat.h"
#include "dvxWm.h"
+#include "shellApp.h"
#include "stb_ds_wrap.h"
+#include
#include
#include
#include
@@ -37,8 +40,19 @@
#define RUN_HOST_ERR_FORMRT "Out of memory creating the form runtime."
#define RUN_HOST_ERR_OTHER "Runtime error"
+// A standalone program's runtime, its module, and the program it runs for
+typedef struct {
+ BasFormRtT *rt;
+ BasModuleT *module;
+ int32_t appId;
+} HostRecT;
+
+static HostRecT *sHosts = NULL; // stb_ds
+static bool sKillHooked = false; // basRunHostAppKilled given to the shell
+
// Prototypes (alphabetical)
bool basRunHostAlive(const BasFormRtT *rt);
+static void basRunHostAppKilled(int32_t appId);
BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunSpecT *spec, const char **outError);
void basRunHostDestroy(BasFormRtT *rt);
void basRunHostLoadForms(BasFormRtT *rt, const char *startupForm);
@@ -52,6 +66,19 @@ bool basRunHostAlive(const BasFormRtT *rt) {
}
+// The shell's kill hook: a program ended by force has its runtimes ended
+// as its own end would (its windows are still there, none of its code
+// runs).
+static void basRunHostAppKilled(int32_t appId) {
+ for (ptrdiff_t i = arrlen(sHosts) - 1; i >= 0; i--) {
+ if (i < arrlen(sHosts) && sHosts[i].appId == appId) {
+ dvxLog("BASIC: '%s' ended by force; its runtime freed", sHosts[i].rt->title);
+ basRunHostDestroy(sHosts[i].rt);
+ }
+ }
+}
+
+
BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunSpecT *spec, const char **outError) {
BasVmT *vm = basVmCreate();
@@ -115,15 +142,41 @@ BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunS
snprintf(rt->helpFile, sizeof(rt->helpFile), "%s", spec->helpFile);
}
+ if (spec->standalone) {
+ HostRecT rec = { rt, module, ctx->currentAppId };
+
+ arrput(sHosts, rec);
+
+ // The shell is looked up at run time (the host tests have none)
+ if (!sKillHooked) {
+ void (*reg)(ShellAppCleanupFnT fn) = dlsym(NULL, DVX_SYM("shellRegisterAppKill"));
+
+ sKillHooked = true;
+
+ if (reg) {
+ reg(basRunHostAppKilled);
+ }
+ }
+ }
+
return rt;
}
void basRunHostDestroy(BasFormRtT *rt) {
- BasVmT *vm = rt->vm;
+ BasVmT *vm = rt->vm;
+ BasModuleT *module = NULL;
+
+ for (ptrdiff_t i = arrlen(sHosts) - 1; i >= 0; i--) {
+ if (sHosts[i].rt == rt) {
+ module = sHosts[i].module;
+ arrdel(sHosts, i);
+ }
+ }
basFormRtDestroy(rt);
basVmDestroy(vm);
+ basModuleFree(module);
}
diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.h b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h
index c6547728..e7bcac7d 100644
--- a/src/apps/kpunch/dvxbasic/formrt/basRunHost.h
+++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h
@@ -53,6 +53,7 @@ typedef struct {
BasDoEventsFnT doEventsFn; // DoEvents and the pause loop of a breakpoint in a handler
void *doEventsCtx;
bool suppressErrorDialog; // the host reports runtime errors itself
+ bool standalone; // the .app's own program (basstub): see basRunHostCreate
} BasRunSpecT;
// A form's caption saved while " [break]" is appended to it.
@@ -69,13 +70,18 @@ typedef struct {
bool basRunHostAlive(const BasFormRtT *rt);
// A VM running module (borrowed: it must outlive the runtime) and its
-// form runtime, set up from spec. Returns the runtime (rt->vm is the
+// form runtime, set up from spec. A standalone program's runtime
+// (spec->standalone, once it is made) owns the module, freed with it, and
+// is ended as the program's own end would when the program is ended by
+// force (the shell's kill hook); the IDE and basdev end theirs
+// themselves. Returns the runtime (rt->vm is the
// VM), or NULL with *outError set when memory runs out. A module the VM
// refuses (too many globals) leaves rt->vm->module NULL; running it then
// fails with the reason in basVmGetError.
BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunSpecT *spec, const char **outError);
-// Destroy the runtime made by basRunHostCreate and its VM.
+// Destroy the runtime made by basRunHostCreate and its VM (and a
+// standalone program's module).
void basRunHostDestroy(BasFormRtT *rt);
// Load every registered form and show the startup form (named, else the
diff --git a/src/apps/kpunch/dvxbasic/formrt/formGfx.c b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
index 1ea69746..f1512b27 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formGfx.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
@@ -83,9 +83,7 @@
#define DRAW_GREEN_SHIFT 8
#define DRAW_BYTE_MASK 0xFF
#define DRAW_ASPECT 1.0 // forms and PictureBoxes have square pixels
-#define DRAW_PAINT_MIN_ARGS 3 // PaintPicture picture, x, y
-#define DRAW_PAINT_SIZE_ARGS 5 // ... , width, height
-#define DRAW_PAINT_SRC_ARGS 9 // ... , srcX, srcY, srcWidth, srcHeight
+#define DRAW_RGB_BYTES 3
#define DRAW_PAINT_ROP_ARGS 10 // ... , opcode (a raster operation)
#define DRAW_ROP_COMBOS 8 // pattern / source / destination bit combinations a raster op's truth table covers
#define DRAW_ROP_PATTERN_BIT 4 // in a combination: the pattern bit
@@ -163,6 +161,12 @@ struct BasScreenWinT {
uint8_t deskPal[BAS_SCREEN_PAL_ENTRIES * BAS_SCREEN_PAL_CHANNELS]; // 256-colour desktop palette the table was made for
};
+// drawPrint's text: the canvas's drawing state and font
+typedef struct {
+ BasDrawT *d;
+ const BitmapFontT *font;
+} CanvasTextT;
+
// DrawFocusRect's dots: where and in which pixel value
typedef struct {
WidgetT *canvas;
@@ -175,6 +179,9 @@ struct BasDrawT {
WidgetT *canvas; // the PictureBox, or the form's drawing area (NULL until a form draws)
AppContextT *ctx;
int32_t scaleMode; // VB ScaleMode (BAS_SCALE_*)
+ double twipsX; // twips in a pixel across and down (the screen: DRAW_TWIPS_PER_PIXEL; a printer's dots)
+ double twipsY;
+ bool paper; // a surface with no canvas (the Printer): no AutoRedraw, BackColor or FontScale
int32_t fontScale; // FontScale: Print's text this many times as large (in the object's font, drawFont)
bool autoRedraw; // True: no Paint events
bool autoRedrawSet; // the program set AutoRedraw (False then clears to BackColor before Paint)
@@ -216,6 +223,7 @@ typedef enum {
DRAW_P_BACKCOLOR,
DRAW_P_CURRENTX,
DRAW_P_CURRENTY,
+ DRAW_P_DRAWSTYLE,
DRAW_P_DRAWWIDTH,
DRAW_P_FILLCOLOR,
DRAW_P_FILLSTYLE,
@@ -260,6 +268,7 @@ static const char *const sDrawProps[DRAW_P_COUNT] = {
[DRAW_P_BACKCOLOR] = BAS_PROP_BACKCOLOR,
[DRAW_P_CURRENTX] = "CurrentX",
[DRAW_P_CURRENTY] = "CurrentY",
+ [DRAW_P_DRAWSTYLE] = "DrawStyle",
[DRAW_P_DRAWWIDTH] = "DrawWidth",
[DRAW_P_FILLCOLOR] = "FillColor",
[DRAW_P_FILLSTYLE] = "FillStyle",
@@ -277,16 +286,20 @@ static const char *const sDrawProps[DRAW_P_COUNT] = {
// ============================================================
static bool canvasSize(WidgetT *canvas, int32_t *w, int32_t *h);
+static int32_t canvasText(void *ctx, const char *text, int32_t len, int32_t x, int32_t y);
+static bool drawCanvasOnly(const BasDrawT *d, int32_t prop);
static int32_t drawCls(BasDrawT *d);
static int32_t drawFind(const char *const *names, int32_t count, const char *name);
static void drawFocusDot(void *ctx, int32_t x, int32_t y);
static int32_t drawFocusRectOn(BasDrawT *d, const BasValueT *args, int32_t argc);
static const BitmapFontT *drawFont(const BasControlT *ctrl);
static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas);
+static bool drawGetProp(BasDrawT *d, DrawPropE prop, BasValueT *out);
static bool drawIsSurface(const BasControlT *ctrl);
static bool drawMeasure(BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result);
static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, uint32_t opcode, const BasValueT *args, int32_t argc, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy);
static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc);
+static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale);
static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across);
static void drawPrint(BasDrawT *d, const BitmapFontT *font, BasValueT value, int32_t mode);
static uint32_t drawRasterOp(uint32_t rop, uint32_t pattern, uint32_t source, uint32_t dest);
@@ -294,11 +307,13 @@ static int32_t drawScale(BasDrawT *d, const BasValueT *args);
static void drawScaleApply(BasDrawT *d);
static int32_t drawScaleArg(const BasValueT *args, int32_t argc, int32_t idx, int32_t omitted);
static bool drawScaleLength(BasDrawT *d, const BasValueT *args, int32_t argc, bool across, double *out);
+static bool drawScaleProp(int32_t prop);
+static void drawScaleXY(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result);
+static bool drawSetProp(BasFormRtT *rt, BasDrawT *d, DrawPropE prop, BasValueT value);
static bool drawSetScaleProp(BasFormRtT *rt, BasDrawT *d, int32_t prop, BasValueT value);
static void drawSync(BasDrawT *d);
static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc);
static double drawToPixelLength(BasDrawT *d, double length, int32_t scale, bool across);
-static void drawToPixels(const BasGfxT *g, double x, double y, double w, double h, int32_t *px, int32_t *py, int32_t *pw, int32_t *ph);
static uint32_t readPixel(const uint8_t *src, int32_t bytesPerPixel);
static void screenBlinkTick(WidgetT *w);
static void screenExpandRow(const BasScreenWinT *sw, uint8_t *dst, const uint8_t *src, int32_t count, int32_t bytesPerPixel);
@@ -329,6 +344,11 @@ void basFormRtDrawClear(BasFormRtT *rt, BasControlT *ctrl) {
}
+void basFormRtDrawDelete(BasDrawT *d) {
+ free(d);
+}
+
+
void basFormRtDrawFree(BasControlT *ctrl) {
free(ctrl->draw);
ctrl->draw = NULL;
@@ -349,70 +369,12 @@ bool basFormRtDrawGetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, B
return true;
}
- switch ((DrawPropE)prop) {
- case DRAW_P_AUTOREDRAW:
- *out = basValBool(d->autoRedraw);
- break;
+ return drawGetProp(d, (DrawPropE)prop, out);
+}
- case DRAW_P_BACKCOLOR:
- *out = basValLong((int32_t)d->gfx.back);
- break;
- case DRAW_P_CURRENTX:
- *out = basValDouble(d->gfx.lastX);
- break;
-
- case DRAW_P_CURRENTY:
- *out = basValDouble(d->gfx.lastY);
- break;
-
- case DRAW_P_DRAWWIDTH:
- *out = basValInteger((int16_t)d->gfx.drawWidth);
- break;
-
- case DRAW_P_FILLCOLOR:
- *out = basValLong((int32_t)d->gfx.fillColor);
- break;
-
- case DRAW_P_FILLSTYLE:
- *out = basValInteger((int16_t)d->gfx.fillStyle);
- break;
-
- case DRAW_P_FONTSCALE:
- *out = basValInteger((int16_t)d->fontScale);
- break;
-
- case DRAW_P_FORECOLOR:
- *out = basValLong((int32_t)d->gfx.fore);
- break;
-
- case DRAW_P_SCALEHEIGHT:
- drawSync(d);
- *out = basValDouble(d->gfx.world ? d->gfx.worldHeight : d->gfx.surf.height);
- break;
-
- case DRAW_P_SCALELEFT:
- *out = basValDouble(d->gfx.world ? d->gfx.worldLeft : 0.0);
- break;
-
- case DRAW_P_SCALEMODE:
- *out = basValInteger((int16_t)d->scaleMode);
- break;
-
- case DRAW_P_SCALETOP:
- *out = basValDouble(d->gfx.world ? d->gfx.worldTop : 0.0);
- break;
-
- case DRAW_P_SCALEWIDTH:
- drawSync(d);
- *out = basValDouble(d->gfx.world ? d->gfx.worldWidth : d->gfx.surf.width);
- break;
-
- default:
- return false;
- }
-
- return true;
+BasGfxT *basFormRtDrawGfx(BasDrawT *d) {
+ return &d->gfx;
}
@@ -456,15 +418,7 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
// ScaleX / ScaleY need no drawing area either
if (method == DRAW_M_SCALEX || method == DRAW_M_SCALEY) {
- BasDrawT *scaled = drawFor(rt, ctrl, false);
- double length = 0.0;
-
- if (!scaled || !drawScaleLength(scaled, args, argc, method == DRAW_M_SCALEX, &length)) {
- basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nScaleX and ScaleY take a length, then optionally the scale it is in (vbHimetric when left out) and the scale wanted (the object's ScaleMode when left out); the scales are 0 (vbUser) to 8 (vbHimetric).", name);
- return true;
- }
-
- *result = basValDouble(length);
+ drawScaleXY(rt, drawFor(rt, ctrl, false), (DrawMethodE)method, args, argc, result);
return true;
}
@@ -542,11 +496,139 @@ bool basFormRtDrawMethod(BasFormRtT *rt, BasControlT *ctrl, const char *name, Ba
}
+// A surface with no canvas: ForeColor black, BackColor white, square
+// shapes on paper (the aspect from the dots' size), CurrentX / CurrentY 0,
+// the ScaleMode's units over it.
+BasDrawT *basFormRtDrawNew(AppContextT *ctx, const BasGfxSurfaceT *surf, double twipsX, double twipsY, int32_t scaleMode) {
+ BasDrawT *d = (BasDrawT *)calloc(1, sizeof(BasDrawT));
+
+ if (!d) {
+ return NULL;
+ }
+
+ d->ctx = ctx;
+ d->paper = true;
+ d->twipsX = twipsX;
+ d->twipsY = twipsY;
+ d->scaleMode = scaleMode;
+ d->fontScale = DRAW_FONT_SCALE_NORMAL;
+ basGfxInit(&d->gfx, surf, DRAW_DEFAULT_FORE, DRAW_DEFAULT_BACK, twipsX / twipsY);
+ d->gfx.lastX = 0.0;
+ d->gfx.lastY = 0.0;
+ drawScaleApply(d);
+ return d;
+}
+
+
+// Columns and lines count charW x charH pixel cells from the left edge,
+// CurrentX / CurrentY kept in the scale; text moves CurrentX by the width
+// emit drew it at.
+// The picture's pixels as R, G, B bytes (malloc'd), *scale as
+// drawPictureOf gives it. NULL, a runtime error raised, when there is no
+// picture or no memory.
+uint8_t *basFormRtDrawPictureRgb(BasFormRtT *rt, BasFormT *form, BasValueT pic, int32_t *w, int32_t *h, const BasGfxT **scale) {
+ uint8_t *pixels = NULL;
+ WidgetT *src = NULL;
+ int32_t pitch = 0;
+ int32_t bpp = rt->ctx->display.format.bytesPerPixel;
+
+ if (!drawPictureOf(rt, form, pic, &src, &pixels, w, h, &pitch, scale)) {
+ return NULL;
+ }
+
+ uint8_t *rgb = (uint8_t *)malloc((size_t)*w * (size_t)*h * DRAW_RGB_BYTES);
+
+ for (int32_t y = 0; rgb && y < *h; y++) {
+ for (int32_t x = 0; x < *w; x++) {
+ uint8_t *p = rgb + ((size_t)y * (size_t)*w + (size_t)x) * DRAW_RGB_BYTES;
+ uint32_t pixel = pixels ? readPixel(pixels + y * pitch + x * bpp, bpp) : wgtCanvasGetPixel(src, x, y);
+
+ unpackColor(&rt->ctx->display, pixel, &p[0], &p[1], &p[2]);
+ }
+ }
+
+ dvxFreeImage(pixels);
+
+ if (!rgb) {
+ basFormRtRuntimeError(rt, "Out of memory", "There is not enough memory for the picture.");
+ }
+
+ return rgb;
+}
+
+
double basFormRtDrawPixelsToUnits(double pixels, int32_t scale, bool across) {
return pixels * DRAW_TWIPS_PER_PIXEL / sTwipsPerUnit[scale][across ? DRAW_ACROSS : DRAW_DOWN];
}
+void basFormRtDrawPrintItem(BasDrawT *d, int32_t charW, int32_t charH, BasValueT value, int32_t mode, BasDrawTextFnT emit, void *ctx) {
+ BasGfxT *g = &d->gfx;
+ double left = g->world ? g->worldLeft : 0.0;
+ double px = basGfxToPixelX(g, g->lastX);
+ int32_t cellW = charW > 0 ? charW : 1;
+ int32_t column = (int32_t)(px / cellW);
+
+ switch (mode) {
+ case BAS_OBJ_PRINT_NEWLINE:
+ g->lastX = left;
+ g->lastY += basGfxToUnitsY(g, charH);
+ return;
+
+ case BAS_OBJ_PRINT_ZONE:
+ g->lastX = left + basGfxToUnitsX(g, (double)(column / BAS_PRINT_ZONE_WIDTH + 1) * BAS_PRINT_ZONE_WIDTH * cellW);
+ return;
+
+ case BAS_OBJ_PRINT_TAB: {
+ // The 1-based column made 0-based in 64 bits (TAB(INT32_MIN))
+ int64_t target = (int64_t)basValToInt32(value) - 1;
+
+ if (target < column) {
+ g->lastY += basGfxToUnitsY(g, charH);
+ }
+
+ g->lastX = left + basGfxToUnitsX(g, (double)(target < 0 ? 0 : target) * cellW);
+ return;
+ }
+
+ case BAS_OBJ_PRINT_SPC:
+ g->lastX += basGfxToUnitsX(g, (double)basValToInt32(value) * cellW);
+ return;
+
+ default:
+ break;
+ }
+
+ BasStringT *s = basValPrintString(value);
+ int32_t w = emit(ctx, s->data, s->len, basGfxRound(px), basGfxRound(basGfxToPixelY(g, g->lastY)));
+
+ g->lastX += basGfxToUnitsX(g, (double)w);
+ basStringUnref(s);
+}
+
+
+bool basFormRtDrawScaleCall(BasFormRtT *rt, BasDrawT *d, const char *name, BasValueT *args, int32_t argc, BasValueT *result) {
+ int32_t method = drawFind(sDrawMethods, DRAW_M_COUNT, name);
+
+ *result = basValInteger(0);
+
+ if (method == DRAW_M_SCALEX || method == DRAW_M_SCALEY) {
+ drawScaleXY(rt, d, (DrawMethodE)method, args, argc, result);
+ return true;
+ }
+
+ if (method != DRAW_M_SCALE) {
+ return false;
+ }
+
+ if (argc != BAS_WINDOW_ARGS || drawScale(d, args) != 0) {
+ basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nScale takes two corners, (x1, y1)-(x2, y2), that are not on one line; Scale alone goes back to the ScaleMode.", name);
+ }
+
+ return true;
+}
+
+
bool basFormRtDrawSetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, BasValueT value) {
int32_t prop = drawIsSurface(ctrl) ? drawFind(sDrawProps, DRAW_P_COUNT, name) : -1;
@@ -557,85 +639,44 @@ bool basFormRtDrawSetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, B
// Setting BackColor clears the drawing to it (VB) and the scale
// follows the drawing area's size, so they need it; the others are
// only remembered.
- bool scaleProp = prop == DRAW_P_SCALEHEIGHT || prop == DRAW_P_SCALELEFT || prop == DRAW_P_SCALEMODE || prop == DRAW_P_SCALETOP || prop == DRAW_P_SCALEWIDTH;
- BasDrawT *d = drawFor(rt, ctrl, prop == DRAW_P_BACKCOLOR || scaleProp);
+ BasDrawT *d = drawFor(rt, ctrl, prop == DRAW_P_BACKCOLOR || drawScaleProp(prop));
if (!d) {
return true;
}
- if (scaleProp) {
- return drawSetScaleProp(rt, d, prop, value);
+ return drawSetProp(rt, d, (DrawPropE)prop, value);
+}
+
+
+bool basFormRtDrawStateGet(BasDrawT *d, const char *name, BasValueT *out) {
+ int32_t prop = drawFind(sDrawProps, DRAW_P_COUNT, name);
+
+ return prop >= 0 && !drawCanvasOnly(d, prop) && drawGetProp(d, (DrawPropE)prop, out);
+}
+
+
+bool basFormRtDrawStateSet(BasFormRtT *rt, BasDrawT *d, const char *name, BasValueT value) {
+ int32_t prop = drawFind(sDrawProps, DRAW_P_COUNT, name);
+
+ return prop >= 0 && !drawCanvasOnly(d, prop) && drawSetProp(rt, d, (DrawPropE)prop, value);
+}
+
+
+// A rectangle in a scale -> pixels (no scale: the values are pixels).
+void basFormRtDrawToPixels(const BasGfxT *g, double x, double y, double w, double h, int32_t *px, int32_t *py, int32_t *pw, int32_t *ph) {
+ if (!g) {
+ *px = basGfxRound(x);
+ *py = basGfxRound(y);
+ *pw = basGfxRound(w);
+ *ph = basGfxRound(h);
+ return;
}
- switch ((DrawPropE)prop) {
- case DRAW_P_AUTOREDRAW:
- d->autoRedraw = basValIsTruthy(value);
- d->autoRedrawSet = true;
- break;
-
- case DRAW_P_BACKCOLOR:
- d->gfx.back = (uint32_t)basValToInt32(value);
-
- if (d->canvas) {
- drawSync(d);
- basGfxClear(&d->gfx, value);
- }
-
- break;
-
- case DRAW_P_CURRENTX:
- d->gfx.lastX = basValToNumber(value);
- break;
-
- case DRAW_P_CURRENTY:
- d->gfx.lastY = basValToNumber(value);
- break;
-
- case DRAW_P_DRAWWIDTH: {
- int32_t w = basValToInt32(value);
-
- d->gfx.drawWidth = w < 1 ? 1 : w;
- break;
- }
-
- case DRAW_P_FILLCOLOR:
- d->gfx.fillColor = (uint32_t)basValToInt32(value);
- break;
-
- case DRAW_P_FILLSTYLE: {
- int32_t style = basValToInt32(value);
-
- if (style < BAS_GFX_FILL_SOLID || style > BAS_GFX_FILL_MAX) {
- basFormRtRuntimeError(rt, "Invalid property value", "FillStyle must be 0 (solid), 1 (transparent) or a hatch pattern 2 to 7.");
- break;
- }
-
- d->gfx.fillStyle = style;
- break;
- }
-
- case DRAW_P_FONTSCALE: {
- int32_t scale = basValToInt32(value);
-
- if (scale < DRAW_FONT_SCALE_NORMAL || scale > DRAW_FONT_SCALE_MAX) {
- basFormRtRuntimeError(rt, "Invalid property value", "FontScale must be 1 (the normal size) to 8.");
- break;
- }
-
- d->fontScale = scale;
- break;
- }
-
- case DRAW_P_FORECOLOR:
- d->gfx.fore = (uint32_t)basValToInt32(value);
- break;
-
- default:
- return false;
- }
-
- return true;
+ *px = basGfxRound(basGfxToPixelX(g, x));
+ *py = basGfxRound(basGfxToPixelY(g, y));
+ *pw = basGfxRound(basGfxToPixelX(g, x + w)) - *px;
+ *ph = basGfxRound(basGfxToPixelY(g, y + h)) - *py;
}
@@ -721,6 +762,23 @@ static bool canvasSize(WidgetT *canvas, int32_t *w, int32_t *h) {
}
+// A canvas's Print text: in ForeColor, FontScale times as large; its
+// width in pixels.
+static int32_t canvasText(void *ctx, const char *text, int32_t len, int32_t x, int32_t y) {
+ CanvasTextT *t = (CanvasTextT *)ctx;
+
+ wgtCanvasSetPenColor(t->d->canvas, basGfxPixel(&t->d->gfx, t->d->gfx.fore));
+ wgtCanvasDrawTextScaled(t->d->canvas, x, y, text, t->d->fontScale);
+ return textWidthN(t->font, text, len) * t->d->fontScale;
+}
+
+
+// The properties a surface without a canvas (the Printer) lacks.
+static bool drawCanvasOnly(const BasDrawT *d, int32_t prop) {
+ return d->paper && (prop == DRAW_P_AUTOREDRAW || prop == DRAW_P_BACKCOLOR || prop == DRAW_P_FONTSCALE);
+}
+
+
// Cls: the drawing cleared to BackColor, CurrentX / CurrentY back to 0.
// 0 or a BASIC error.
static int32_t drawCls(BasDrawT *d) {
@@ -762,7 +820,7 @@ static int32_t drawFocusRectOn(BasDrawT *d, const BasValueT *args, int32_t argc)
uint32_t color = (argc == DRAW_FOCUS_COLOR_ARGS) ? (uint32_t)basValToNumber(args[DRAW_FOCUS_COLOR_ARG]) : d->gfx.fore;
DrawFocusDotT fd;
- drawToPixels(&d->gfx, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), basValToNumber(args[3]), &px, &py, &pw, &ph);
+ basFormRtDrawToPixels(&d->gfx, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), basValToNumber(args[3]), &px, &py, &pw, &ph);
// A negative width or height counts from the other edge
if (pw < 0) {
@@ -825,6 +883,8 @@ static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas) {
d->gfx.lastY = 0.0;
d->canvas = isForm ? NULL : ctrl->widget;
d->scaleMode = BAS_SCALE_PIXELS;
+ d->twipsX = DRAW_TWIPS_PER_PIXEL;
+ d->twipsY = DRAW_TWIPS_PER_PIXEL;
d->fontScale = DRAW_FONT_SCALE_NORMAL;
ctrl->draw = d;
}
@@ -845,6 +905,80 @@ static BasDrawT *drawFor(BasFormRtT *rt, BasControlT *ctrl, bool needCanvas) {
}
+// A drawing property's value (the switch of basFormRtDrawGetProp and
+// basFormRtDrawStateGet). False when prop is none of them.
+static bool drawGetProp(BasDrawT *d, DrawPropE prop, BasValueT *out) {
+ switch (prop) {
+ case DRAW_P_AUTOREDRAW:
+ *out = basValBool(d->autoRedraw);
+ break;
+
+ case DRAW_P_BACKCOLOR:
+ *out = basValLong((int32_t)d->gfx.back);
+ break;
+
+ case DRAW_P_CURRENTX:
+ *out = basValDouble(d->gfx.lastX);
+ break;
+
+ case DRAW_P_CURRENTY:
+ *out = basValDouble(d->gfx.lastY);
+ break;
+
+ case DRAW_P_DRAWSTYLE:
+ *out = basValInteger((int16_t)d->gfx.drawStyle);
+ break;
+
+ case DRAW_P_DRAWWIDTH:
+ *out = basValInteger((int16_t)d->gfx.drawWidth);
+ break;
+
+ case DRAW_P_FILLCOLOR:
+ *out = basValLong((int32_t)d->gfx.fillColor);
+ break;
+
+ case DRAW_P_FILLSTYLE:
+ *out = basValInteger((int16_t)d->gfx.fillStyle);
+ break;
+
+ case DRAW_P_FONTSCALE:
+ *out = basValInteger((int16_t)d->fontScale);
+ break;
+
+ case DRAW_P_FORECOLOR:
+ *out = basValLong((int32_t)d->gfx.fore);
+ break;
+
+ case DRAW_P_SCALEHEIGHT:
+ drawSync(d);
+ *out = basValDouble(d->gfx.world ? d->gfx.worldHeight : d->gfx.surf.height);
+ break;
+
+ case DRAW_P_SCALELEFT:
+ *out = basValDouble(d->gfx.world ? d->gfx.worldLeft : 0.0);
+ break;
+
+ case DRAW_P_SCALEMODE:
+ *out = basValInteger((int16_t)d->scaleMode);
+ break;
+
+ case DRAW_P_SCALETOP:
+ *out = basValDouble(d->gfx.world ? d->gfx.worldTop : 0.0);
+ break;
+
+ case DRAW_P_SCALEWIDTH:
+ drawSync(d);
+ *out = basValDouble(d->gfx.world ? d->gfx.worldWidth : d->gfx.surf.width);
+ break;
+
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+
// Forms and PictureBoxes take the drawing members.
static bool drawIsSurface(const BasControlT *ctrl) {
if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
@@ -984,43 +1118,25 @@ static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint
// the FillColor pattern by the raster operation opcode (vbSrcCopy when
// left out; vbSrcAnd, vbSrcPaint, vbSrcInvert ... -- any of Windows' 256).
static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc) {
- uint8_t *pixels = NULL;
- WidgetT *src = NULL;
- int32_t srcW = 0;
- int32_t srcH = 0;
- int32_t pitch = 0;
- int32_t bpp = rt->ctx->display.format.bytesPerPixel;
+ uint8_t *pixels = NULL;
+ WidgetT *src = NULL;
+ const BasGfxT *srcScale = NULL;
+ int32_t srcW = 0;
+ int32_t srcH = 0;
+ int32_t pitch = 0;
+ int32_t bpp = rt->ctx->display.format.bytesPerPixel;
if (argc < DRAW_PAINT_MIN_ARGS) {
basFormRtRuntimeError(rt, "PaintPicture needs a picture, x, and y", "Usage: PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]");
return;
}
- // The picture: a PictureBox (a Control variable, or its name), else an
- // image file (named, or a LOADPICTURE Picture object).
- const char *path = basFormRtPicturePath(rt, args[0]);
- BasStringT *name = path ? basStringNew(path, (int32_t)strlen(path)) : (args[0].type == BAS_TYPE_OBJECT ? NULL : basValFormatString(args[0]));
- void *ref = name ? basFormRtFindCtrl(rt, ctrl->form, name->data) : args[0].objVal;
- BasControlT *pic = basFormRtResolveCtrl(rt, ref);
-
- src = pic && drawIsSurface(pic) && (!pic->form || pic != &pic->form->formCtrl) ? pic->widget : NULL;
-
- if (!src && name) {
- pixels = dvxLoadImage(rt->ctx, name->data, &srcW, &srcH, &pitch);
- }
-
- if (name) {
- basStringUnref(name);
- }
-
- if (!pixels && (!src || !canvasSize(src, &srcW, &srcH))) {
- basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG, or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture.");
+ if (!drawPictureOf(rt, ctrl->form, args[0], &src, &pixels, &srcW, &srcH, &pitch, &srcScale)) {
return;
}
// The destination in this object's scale, the source rectangle in the
// source PictureBox's (an image file's in pixels)
- const BasGfxT *srcScale = src && pic->draw ? &pic->draw->gfx : NULL;
int32_t sx = 0;
int32_t sy = 0;
int32_t sw = srcW;
@@ -1031,10 +1147,10 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas
int32_t dh = 0;
if (argc >= DRAW_PAINT_SRC_ARGS) {
- drawToPixels(srcScale, basValToNumber(args[5]), basValToNumber(args[6]), basValToNumber(args[7]), basValToNumber(args[8]), &sx, &sy, &sw, &sh);
+ basFormRtDrawToPixels(srcScale, basValToNumber(args[5]), basValToNumber(args[6]), basValToNumber(args[7]), basValToNumber(args[8]), &sx, &sy, &sw, &sh);
}
- drawToPixels(&d->gfx, basValToNumber(args[1]), basValToNumber(args[2]), argc >= DRAW_PAINT_SIZE_ARGS ? basValToNumber(args[3]) : 0.0, argc >= DRAW_PAINT_SIZE_ARGS ? basValToNumber(args[4]) : 0.0, &dx, &dy, &dw, &dh);
+ basFormRtDrawToPixels(&d->gfx, basValToNumber(args[1]), basValToNumber(args[2]), argc >= DRAW_PAINT_SIZE_ARGS ? basValToNumber(args[3]) : 0.0, argc >= DRAW_PAINT_SIZE_ARGS ? basValToNumber(args[4]) : 0.0, &dx, &dy, &dw, &dh);
if (argc < DRAW_PAINT_SIZE_ARGS) {
dw = sw;
@@ -1085,6 +1201,38 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas
}
+// PaintPicture's picture: a PictureBox (a Control variable, or its name,
+// looked up from form first), else an image file (named, or a LOADPICTURE
+// Picture object) loaded into *pixels in the display format (free with
+// dvxFreeImage). *scale is the PictureBox's scale (NULL: pixels). False,
+// a runtime error raised, when there is none.
+static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale) {
+ const char *path = basFormRtPicturePath(rt, v);
+ BasStringT *name = path ? basStringNew(path, (int32_t)strlen(path)) : (v.type == BAS_TYPE_OBJECT ? NULL : basValFormatString(v));
+ void *ref = name ? basFormRtFindCtrl(rt, form, name->data) : v.objVal;
+ BasControlT *pic = basFormRtResolveCtrl(rt, ref);
+
+ *pixels = NULL;
+ *src = pic && drawIsSurface(pic) && (!pic->form || pic != &pic->form->formCtrl) ? pic->widget : NULL;
+ *scale = *src && pic->draw ? &pic->draw->gfx : NULL;
+
+ if (!*src && name) {
+ *pixels = dvxLoadImage(rt->ctx, name->data, w, h, pitch);
+ }
+
+ if (name) {
+ basStringUnref(name);
+ }
+
+ if (!*pixels && (!*src || !canvasSize(*src, w, h))) {
+ basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG, or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture.");
+ return false;
+ }
+
+ return true;
+}
+
+
// A length in the object's scale as whole pixels along one axis (across
// or down), measured from 0 so the scale's origin does not shift it. No
// scale (g NULL): the length is pixels.
@@ -1094,61 +1242,21 @@ static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across) {
int32_t pw = 0;
int32_t ph = 0;
- drawToPixels(g, 0.0, 0.0, length, length, &px, &py, &pw, &ph);
+ basFormRtDrawToPixels(g, 0.0, 0.0, length, length, &px, &py, &pw, &ph);
return across ? pw : ph;
}
-// One object Print item at CurrentX / CurrentY in ForeColor (font
-// FontScale times as large, transparent background). Columns, zones and
-// lines are counted in (scaled) characters of the font -- its charWidth
-// across, its line height down -- from the left edge, CurrentX /
-// CurrentY kept in the scale; text moves CurrentX by its width.
+// One object Print item on a canvas at CurrentX / CurrentY in ForeColor
+// (font FontScale times as large, transparent background); columns and
+// lines in its (scaled) characters (basFormRtDrawPrintItem).
static void drawPrint(BasDrawT *d, const BitmapFontT *font, BasValueT value, int32_t mode) {
- BasGfxT *g = &d->gfx;
- int32_t charW = font->charWidth * d->fontScale;
- int32_t charH = font->charHeight * d->fontScale;
- double left = g->world ? g->worldLeft : 0.0;
- double px = basGfxToPixelX(g, g->lastX);
- int32_t column = (int32_t)(px / charW);
+ CanvasTextT t;
- switch (mode) {
- case BAS_OBJ_PRINT_NEWLINE:
- g->lastX = left;
- g->lastY += basGfxToUnitsY(g, charH);
- return;
-
- case BAS_OBJ_PRINT_ZONE:
- g->lastX = left + basGfxToUnitsX(g, (double)(column / BAS_PRINT_ZONE_WIDTH + 1) * BAS_PRINT_ZONE_WIDTH * charW);
- return;
-
- case BAS_OBJ_PRINT_TAB: {
- // The 1-based column made 0-based in 64 bits (TAB(INT32_MIN))
- int64_t target = (int64_t)basValToInt32(value) - 1;
-
- if (target < column) {
- g->lastY += basGfxToUnitsY(g, charH);
- }
-
- g->lastX = left + basGfxToUnitsX(g, (double)(target < 0 ? 0 : target) * charW);
- return;
- }
-
- case BAS_OBJ_PRINT_SPC:
- g->lastX += basGfxToUnitsX(g, (double)basValToInt32(value) * charW);
- return;
-
- default:
- break;
- }
-
- BasStringT *s = basValPrintString(value);
-
- wgtCanvasSetPenColor(d->canvas, basGfxPixel(g, g->fore));
- wgtCanvasDrawTextScaled(d->canvas, basGfxRound(px), basGfxRound(basGfxToPixelY(g, g->lastY)), s->data, d->fontScale);
- g->lastX += basGfxToUnitsX(g, (double)textWidthN(font, s->data, s->len) * d->fontScale);
- basStringUnref(s);
+ t.d = d;
+ t.font = font;
+ basFormRtDrawPrintItem(d, font->charWidth * d->fontScale, font->charHeight * d->fontScale, value, mode, canvasText, &t);
}
@@ -1210,7 +1318,7 @@ static void drawScaleApply(BasDrawT *d) {
const double *twips = sTwipsPerUnit[d->scaleMode];
- basGfxSetScale(g, 0.0, 0.0, g->surf.width * DRAW_TWIPS_PER_PIXEL / twips[DRAW_ACROSS], g->surf.height * DRAW_TWIPS_PER_PIXEL / twips[DRAW_DOWN]);
+ basGfxSetScale(g, 0.0, 0.0, g->surf.width * d->twipsX / twips[DRAW_ACROSS], g->surf.height * d->twipsY / twips[DRAW_DOWN]);
}
@@ -1250,6 +1358,116 @@ static bool drawScaleLength(BasDrawT *d, const BasValueT *args, int32_t argc, bo
}
+// True for the scale properties (they follow the drawing area's size).
+static bool drawScaleProp(int32_t prop) {
+ return prop == DRAW_P_SCALEHEIGHT || prop == DRAW_P_SCALELEFT || prop == DRAW_P_SCALEMODE || prop == DRAW_P_SCALETOP || prop == DRAW_P_SCALEWIDTH;
+}
+
+
+// ScaleX / ScaleY into *result: a bad argument is a runtime error.
+static void drawScaleXY(BasFormRtT *rt, BasDrawT *d, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result) {
+ double length = 0.0;
+
+ if (!d || !drawScaleLength(d, args, argc, method == DRAW_M_SCALEX, &length)) {
+ basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nScaleX and ScaleY take a length, then optionally the scale it is in (vbHimetric when left out) and the scale wanted (the object's ScaleMode when left out); the scales are 0 (vbUser) to 8 (vbHimetric).", sDrawMethods[method]);
+ return;
+ }
+
+ *result = basValDouble(length);
+}
+
+
+// Set a drawing property (basFormRtDrawSetProp and basFormRtDrawStateSet):
+// the scale ones through drawSetScaleProp. False when prop is none of
+// them.
+static bool drawSetProp(BasFormRtT *rt, BasDrawT *d, DrawPropE prop, BasValueT value) {
+ if (drawScaleProp(prop)) {
+ return drawSetScaleProp(rt, d, prop, value);
+ }
+
+ switch (prop) {
+ case DRAW_P_AUTOREDRAW:
+ d->autoRedraw = basValIsTruthy(value);
+ d->autoRedrawSet = true;
+ break;
+
+ case DRAW_P_BACKCOLOR:
+ d->gfx.back = (uint32_t)basValToInt32(value);
+
+ if (d->canvas) {
+ drawSync(d);
+ basGfxClear(&d->gfx, value);
+ }
+
+ break;
+
+ case DRAW_P_CURRENTX:
+ d->gfx.lastX = basValToNumber(value);
+ break;
+
+ case DRAW_P_CURRENTY:
+ d->gfx.lastY = basValToNumber(value);
+ break;
+
+ case DRAW_P_DRAWSTYLE: {
+ int32_t style = basValToInt32(value);
+
+ if (style < BAS_GFX_DRAW_SOLID || style > BAS_GFX_DRAW_MAX) {
+ basFormRtRuntimeError(rt, "Invalid property value", "DrawStyle must be 0 (solid), 1 (dash), 2 (dot), 3 (dash-dot), 4 (dash-dot-dot), 5 (invisible), or 6 (inside solid).");
+ break;
+ }
+
+ d->gfx.drawStyle = style;
+ break;
+ }
+
+ case DRAW_P_DRAWWIDTH: {
+ int32_t w = basValToInt32(value);
+
+ d->gfx.drawWidth = w < 1 ? 1 : w;
+ break;
+ }
+
+ case DRAW_P_FILLCOLOR:
+ d->gfx.fillColor = (uint32_t)basValToInt32(value);
+ break;
+
+ case DRAW_P_FILLSTYLE: {
+ int32_t style = basValToInt32(value);
+
+ if (style < BAS_GFX_FILL_SOLID || style > BAS_GFX_FILL_MAX) {
+ basFormRtRuntimeError(rt, "Invalid property value", "FillStyle must be 0 (solid), 1 (transparent) or a hatch pattern 2 to 7.");
+ break;
+ }
+
+ d->gfx.fillStyle = style;
+ break;
+ }
+
+ case DRAW_P_FONTSCALE: {
+ int32_t scale = basValToInt32(value);
+
+ if (scale < DRAW_FONT_SCALE_NORMAL || scale > DRAW_FONT_SCALE_MAX) {
+ basFormRtRuntimeError(rt, "Invalid property value", "FontScale must be 1 (the normal size) to 8.");
+ break;
+ }
+
+ d->fontScale = scale;
+ break;
+ }
+
+ case DRAW_P_FORECOLOR:
+ d->gfx.fore = (uint32_t)basValToInt32(value);
+ break;
+
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+
// ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight. Setting one
// of the last four makes the scale a user scale (ScaleMode 0) from the
// current one; ScaleWidth and ScaleHeight may not be 0.
@@ -1384,7 +1602,7 @@ static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, const BitmapFontT *font,
int32_t visible = basValToInt32(args[4]);
int32_t total = basValToInt32(args[5]);
- drawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), length, length, &px, &py, &pw, &ph);
+ basFormRtDrawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), length, length, &px, &py, &pw, &ph);
// A control's bar (ListBox's, WGT_SB_W wide) or a popup list's
if (control && across) {
@@ -1408,7 +1626,7 @@ static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, const BitmapFontT *font,
}
// DrawScrollArrow's size is a length across; DrawArrow's is glyph rows
- drawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), 0.0, &px, &py, &pw, &ph);
+ basFormRtDrawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), 0.0, &px, &py, &pw, &ph);
if (method == DRAW_M_DRAWSCROLLARROW) {
drawScrollbarArrowButton(&disp, ops, cs, px, py, pw, (ScrollArrowDirE)dir);
@@ -1433,7 +1651,7 @@ static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, const BitmapFontT *font,
bool noFill = argc > DRAW_RECT_FACE_ARG && basValToNumber(args[DRAW_RECT_FACE_ARG]) == DRAW_FACE_NONE;
uint32_t face = (argc > DRAW_RECT_FACE_ARG) ? surfToPixel(d, (uint32_t)basValToNumber(args[DRAW_RECT_FACE_ARG])) : cs->windowFace;
- drawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), basValToNumber(args[3]), &px, &py, &pw, &ph);
+ basFormRtDrawToPixels(g, basValToNumber(args[0]), basValToNumber(args[1]), basValToNumber(args[2]), basValToNumber(args[3]), &px, &py, &pw, &ph);
if (method == DRAW_M_DRAWBUTTONFACE) {
drawPressableBevel(&disp, ops, px, py, pw, ph, down, face, cs);
@@ -1452,7 +1670,6 @@ static int32_t drawThemed(BasFormRtT *rt, BasDrawT *d, const BitmapFontT *font,
}
-// A rectangle in a scale -> pixels (no scale: the values are pixels).
// A length in scale (BAS_SCALE_*) across or down as pixels: vbUser is the
// object's current scale (a unit ScaleMode or a user scale).
static double drawToPixelLength(BasDrawT *d, double length, int32_t scale, bool across) {
@@ -1460,23 +1677,7 @@ static double drawToPixelLength(BasDrawT *d, double length, int32_t scale, bool
return length / (across ? basGfxToUnitsX(&d->gfx, 1.0) : basGfxToUnitsY(&d->gfx, 1.0));
}
- return length / basFormRtDrawPixelsToUnits(1.0, scale, across);
-}
-
-
-static void drawToPixels(const BasGfxT *g, double x, double y, double w, double h, int32_t *px, int32_t *py, int32_t *pw, int32_t *ph) {
- if (!g) {
- *px = basGfxRound(x);
- *py = basGfxRound(y);
- *pw = basGfxRound(w);
- *ph = basGfxRound(h);
- return;
- }
-
- *px = basGfxRound(basGfxToPixelX(g, x));
- *py = basGfxRound(basGfxToPixelY(g, y));
- *pw = basGfxRound(basGfxToPixelX(g, x + w)) - *px;
- *ph = basGfxRound(basGfxToPixelY(g, y + h)) - *py;
+ return length * sTwipsPerUnit[scale][across ? DRAW_ACROSS : DRAW_DOWN] / (across ? d->twipsX : d->twipsY);
}
diff --git a/src/apps/kpunch/dvxbasic/formrt/formPrinter.c b/src/apps/kpunch/dvxbasic/formrt/formPrinter.c
new file mode 100644
index 00000000..0248db9b
--- /dev/null
+++ b/src/apps/kpunch/dvxbasic/formrt/formPrinter.c
@@ -0,0 +1,1253 @@
+// 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.
+
+// formPrinter.c -- DVX BASIC form runtime: VB's Printer object
+//
+// Printer draws pages for the printer chosen in the Control Panel,
+// through the printing library (print.lib, looked up by name as the
+// other printing calls are, so a program runs where it is not
+// installed). Its members, as VB has them:
+// drawing Line, Circle, PSet, Print (; , TAB SPC), PaintPicture;
+// TextWidth, TextHeight; Scale, ScaleX, ScaleY
+// pages NewPage, EndDoc, KillDoc; Page
+// properties CurrentX / Y, ForeColor, FillColor, FillStyle, DrawWidth,
+// DrawStyle,
+// ScaleMode / Left / Top / Width / Height, FontName,
+// FontSize (points), FontBold, FontItalic, FontUnderline,
+// FontStrikethru, Copies, Orientation (portrait), and to
+// read Width, Height (twips), ColorMode, TwipsPerPixelX / Y,
+// DeviceName, Port
+// The page is the printable area (8 by 10 inches) in the printer's dots;
+// ScaleMode starts at twips, as VB's Printer.
+//
+// A page is drawn by recording: each Line, Circle, PSet, Print, and
+// PaintPicture is done at once on a measuring surface (which moves
+// CurrentX / CurrentY and finds the rows it touches) and kept, with the
+// drawing state it was done in. NewPage and EndDoc print the page: the
+// library asks for it a band at a time, and every kept call whose rows
+// meet the band is done again into the band. On a PostScript printer
+// (a vector page) the kept calls are done again once, as the printer's
+// own lines, shapes, text (its Helvetica, Times, or Courier, which
+// TextWidth then measures), and pictures. The document (the print
+// job) opens with the first page printed, at the destination and copies
+// of the program's last PrintChoose when it made one, and stays open to
+// EndDoc; KillDoc throws the page away and ends it. A program that ends
+// with a page drawn or a document open has EndDoc done for it (VB prints
+// it).
+
+#include "formrt.h"
+#include "../../../../libs/kpunch/print/print.h"
+#include "dvxPlat.h"
+#include "dvxText.h"
+#include "thirdparty/stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define PR_TWIPS_PER_INCH 1440.0
+#define PR_SCREEN_TWIPS 15.0 // a screen pixel (a picture's own size)
+#define PR_POINT_TENTHS 10.0 // FontSize in points; the fonts take tenths
+#define PR_FONT_SIZE_MAX 1000.0 // points
+#define PR_DEFAULT_FONT FONT_FALLBACK_FAMILY
+#define PR_DEFAULT_TENTHS 120 // 12 points
+#define PR_DEFAULT_TITLE "BASIC program"
+#define PR_DIGIT "0" // Print's columns: the font's digit width
+#define PR_RGB_BYTES 3
+#define PR_RED_SHIFT 16
+#define PR_GREEN_SHIFT 8
+#define PR_BYTE_MASK 0xFF
+#define PR_WHITE 0xFFFFFF
+#define PR_UNSET (-1)
+#define PR_STANDIN_MODEL PrintModelLaserJet300E // a text-only printer's page still has a size: a 300 dpi one's
+
+// The print library's calls (NULL until looked up; any missing: printing
+// is not installed)
+typedef struct {
+ __typeof__(printSettingsLoad) *settingsLoad;
+ __typeof__(printModelPage) *modelPage;
+ __typeof__(printModelLabel) *modelLabel;
+ __typeof__(printDestLabel) *destLabel;
+ __typeof__(printBasChoice) *basChoice;
+ __typeof__(printUiDocOpen) *docOpen;
+ __typeof__(printUiDocClose) *docClose;
+ __typeof__(printDocClose) *docAbandon;
+ __typeof__(printDocPage) *docPage;
+ __typeof__(printFontOpen) *fontOpen;
+ __typeof__(printFontClose) *fontClose;
+ __typeof__(printFontWidth) *fontWidth;
+ __typeof__(printFontHeight) *fontHeight;
+ __typeof__(printFontDraw) *fontDraw;
+ __typeof__(printVecBegin) *vecBegin;
+ __typeof__(printVecEnd) *vecEnd;
+ __typeof__(printVecLine) *vecLine;
+ __typeof__(printVecRect) *vecRect;
+ __typeof__(printVecEllipse) *vecEllipse;
+ __typeof__(printVecEllipseFill) *vecEllipseFill;
+ __typeof__(printVecDot) *vecDot;
+ __typeof__(printVecText) *vecText;
+ __typeof__(printVecImage) *vecImage;
+} PrApiT;
+
+// The kinds of drawing a page keeps
+typedef enum {
+ PrOpLineE,
+ PrOpCircleE,
+ PrOpPsetE,
+ PrOpTextE,
+ PrOpPictureE
+} PrOpKindE;
+
+// One kept drawing call and the rows it touches
+typedef struct {
+ PrOpKindE kind;
+ int32_t top;
+ int32_t bottom;
+ union {
+ struct {
+ int32_t state; // states[]: the drawing state it was done in
+ double lastX; // CurrentX / Y then
+ double lastY;
+ double args[BAS_GFX_CIRCLE_ARGS]; // its argument block (the largest)
+ int32_t argc;
+ } shape;
+ struct {
+ int32_t font; // fonts[]
+ int32_t x; // the top of its line, in dots
+ int32_t y;
+ uint32_t color; // 0xRRGGBB
+ char *text; // malloc'd
+ int32_t len;
+ } text;
+ struct {
+ uint8_t *rgb; // malloc'd, w x h
+ int32_t w;
+ int32_t h;
+ int32_t sx; // the part of it drawn
+ int32_t sy;
+ int32_t sw;
+ int32_t sh;
+ int32_t dx; // where on the page
+ int32_t dy;
+ int32_t dw;
+ int32_t dh;
+ } picture;
+ } u;
+} PrOpT;
+
+// A font text was printed in, opened at the page's resolution
+typedef struct {
+ char family[FONT_FAMILY_MAX];
+ int32_t tenths;
+ uint32_t style;
+ PrintFontT *font;
+} PrFontT;
+
+// The Printer's state (one a runtime)
+struct BasPrinterT {
+ const PrApiT *api; // the print library's calls (prGet found them all)
+ BasDrawT *draw; // the page's drawing state, on the measuring surface
+ PrintPageInfoT page;
+ bool pageKnown; // page holds the printer's page (else the model prints text only)
+ bool graphics; // the model prints pictures
+ PrintDocT *doc; // the document, once a page went out
+ int32_t docCopies; // each page printed this many times
+ int32_t pageNum; // Page: the page being drawn
+ int32_t copies; // Copies set by the program (0: PrintChoose's, else 1)
+ char fontName[FONT_FAMILY_MAX];
+ int32_t fontTenths;
+ uint32_t fontStyle;
+ int32_t font; // fonts[] of the font now; PR_UNSET: open it on use
+ PrOpT *ops; // stb_ds: the page's drawing
+ BasGfxT *states; // stb_ds: the states ops were done in
+ PrFontT *fonts; // stb_ds: the fonts the document's text is in
+ int32_t touchTop; // rows the call being done touches (measuring surface)
+ int32_t touchBottom;
+ bool stateChanged; // the drawing state changed since states[] last kept it
+};
+
+// The Printer's own methods and properties (the rest are the drawing
+// state's: basFormRtDrawState* and basFormRtDrawScaleCall)
+typedef enum {
+ PrMCircleE,
+ PrMEndDocE,
+ PrMKillDocE,
+ PrMLineE,
+ PrMNewPageE,
+ PrMPaintPictureE,
+ PrMPrintE,
+ PrMPsetE,
+ PrMTextHeightE,
+ PrMTextWidthE,
+ PrMCountE
+} PrMethodE;
+
+typedef enum {
+ PrPColorModeE,
+ PrPCopiesE,
+ PrPDeviceNameE,
+ PrPFontBoldE,
+ PrPFontItalicE,
+ PrPFontNameE,
+ PrPFontSizeE,
+ PrPFontStrikethruE,
+ PrPFontUnderlineE,
+ PrPHeightE,
+ PrPOrientationE,
+ PrPPageE,
+ PrPPortE,
+ PrPTwipsPerPixelXE,
+ PrPTwipsPerPixelYE,
+ PrPWidthE,
+ PrPCountE
+} PrPropE;
+
+// printFontDraw / a shape's spans into one band
+typedef struct {
+ uint8_t *rgb;
+ int32_t width;
+ int32_t y0;
+ int32_t rows;
+} PrBandT;
+
+static const char *const sMethods[PrMCountE] = {
+ [PrMCircleE] = "Circle",
+ [PrMEndDocE] = "EndDoc",
+ [PrMKillDocE] = "KillDoc",
+ [PrMLineE] = "Line",
+ [PrMNewPageE] = "NewPage",
+ [PrMPaintPictureE] = "PaintPicture",
+ [PrMPrintE] = "Print",
+ [PrMPsetE] = "PSet",
+ [PrMTextHeightE] = "TextHeight",
+ [PrMTextWidthE] = "TextWidth"
+};
+
+static const char *const sProps[PrPCountE] = {
+ [PrPColorModeE] = "ColorMode",
+ [PrPCopiesE] = "Copies",
+ [PrPDeviceNameE] = "DeviceName",
+ [PrPFontBoldE] = "FontBold",
+ [PrPFontItalicE] = "FontItalic",
+ [PrPFontNameE] = "FontName",
+ [PrPFontSizeE] = "FontSize",
+ [PrPFontStrikethruE] = "FontStrikethru",
+ [PrPFontUnderlineE] = "FontUnderline",
+ [PrPHeightE] = "Height",
+ [PrPOrientationE] = "Orientation",
+ [PrPPageE] = "Page",
+ [PrPPortE] = "Port",
+ [PrPTwipsPerPixelXE] = "TwipsPerPixelX",
+ [PrPTwipsPerPixelYE] = "TwipsPerPixelY",
+ [PrPWidthE] = "Width"
+};
+
+// The font style bit of each Font property, indexed by PrPropE (0: none)
+static const uint32_t sFontBits[PrPCountE] = {
+ [PrPFontBoldE] = FONT_STYLE_BOLD,
+ [PrPFontItalicE] = FONT_STYLE_ITALIC,
+ [PrPFontStrikethruE] = FONT_STYLE_STRIKE,
+ [PrPFontUnderlineE] = FONT_STYLE_UNDERLINE
+};
+
+// The printer's fill for each FillStyle that fills (transparent never
+// does)
+static const PrintFillE sFills[BAS_GFX_FILL_MAX + 1] = {
+ [BAS_GFX_FILL_SOLID] = PrintFillSolidE,
+ [BAS_GFX_FILL_TRANSPARENT] = PrintFillSolidE,
+ [BAS_GFX_FILL_HORIZONTAL] = PrintFillHorizontalE,
+ [BAS_GFX_FILL_VERTICAL] = PrintFillVerticalE,
+ [BAS_GFX_FILL_UP_DIAGONAL] = PrintFillUpDiagonalE,
+ [BAS_GFX_FILL_DOWN_DIAGONAL] = PrintFillDownDiagonalE,
+ [BAS_GFX_FILL_CROSS] = PrintFillCrossE,
+ [BAS_GFX_FILL_DIAGONAL_CROSS] = PrintFillDiagonalCrossE
+};
+
+// The drawing properties a new page keeps from the old one when the
+// printer (so the page's size) changes between documents
+static const char *const sKeptProps[] = { "ForeColor", "FillColor", "FillStyle", "DrawWidth", "DrawStyle", "ScaleMode" };
+
+static const uint8_t sPrinterObj = 0; // the object BASIC holds: its address
+static PrApiT sApi;
+static bool sApiTried;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void basFormRtPrinterEnd(BasFormRtT *rt);
+BasValueT basFormRtPrinterGetProp(BasFormRtT *rt, const char *name);
+bool basFormRtPrinterIs(const void *ref);
+BasValueT basFormRtPrinterMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc);
+void *basFormRtPrinterRef(void);
+void basFormRtPrinterSetProp(BasFormRtT *rt, const char *name, BasValueT value);
+static const PrApiT *prApi(void);
+static void prBandSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel);
+static void prEndDoc(BasFormRtT *rt, BasPrinterT *p);
+static int32_t prFind(const char *const *names, int32_t count, const char *name);
+static int32_t prFont(BasPrinterT *p);
+static void prFontsClose(BasPrinterT *p);
+static BasPrinterT *prGet(BasFormRtT *rt);
+static void prHome(BasPrinterT *p);
+static void prKillDoc(BasPrinterT *p);
+static uint32_t prMeasureGet(const void *ctx, int32_t x, int32_t y);
+static void prMeasureSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel);
+static void prNewPage(BasFormRtT *rt, BasPrinterT *p);
+static bool prPage(BasFormRtT *rt, BasPrinterT *p, bool drawing);
+static bool prPageBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows);
+static void prPageClear(BasPrinterT *p);
+static bool prPageSame(const PrintPageInfoT *a, const PrintPageInfoT *b);
+static void prPicture(BasFormRtT *rt, BasPrinterT *p, BasValueT *args, int32_t argc);
+static void prPictureBand(const PrOpT *op, const PrBandT *b);
+static bool prPrintPage(BasFormRtT *rt, BasPrinterT *p);
+static void prShape(BasFormRtT *rt, BasPrinterT *p, PrOpKindE kind, BasValueT *args, int32_t argc);
+static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf);
+static int32_t prShapeDo(BasGfxT *g, PrOpKindE kind, const BasValueT *args);
+static int32_t prText(void *ctx, const char *text, int32_t len, int32_t x, int32_t y);
+static double prTwipsX(const BasPrinterT *p);
+static double prTwipsY(const BasPrinterT *p);
+static void prVecDot(void *ctx, int32_t x, int32_t y, uint32_t pixel, int32_t width);
+static void prVecEllipse(void *ctx, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, int32_t width, uint16_t style, bool arc, double start, double end);
+static void prVecEllipseFill(void *ctx, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, int32_t fillStyle);
+static void prVecLine(void *ctx, double x0, double y0, double x1, double y1, uint32_t pixel, int32_t width, uint16_t style);
+static bool prVecPage(BasPrinterT *p);
+static void prVecRect(void *ctx, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, int32_t fillStyle);
+
+// A vector page's shapes go to the printer whole
+static const BasGfxVectorT sPrVec = { prVecLine, prVecRect, prVecEllipse, prVecEllipseFill, prVecDot };
+
+
+void basFormRtPrinterEnd(BasFormRtT *rt) {
+ BasPrinterT *p = rt->printer;
+
+ if (!p) {
+ return;
+ }
+
+ prEndDoc(rt, p);
+ basFormRtDrawDelete(p->draw);
+ free(p);
+ rt->printer = NULL;
+}
+
+
+BasValueT basFormRtPrinterGetProp(BasFormRtT *rt, const char *name) {
+ BasValueT v = basValInteger(0);
+ BasPrinterT *p = prGet(rt);
+ int32_t prop;
+
+ if (!p || !prPage(rt, p, false)) {
+ return v;
+ }
+
+ prop = prFind(sProps, PrPCountE, name);
+
+ switch ((PrPropE)prop) {
+ case PrPColorModeE:
+ return basValInteger(p->page.color ? BAS_PRINTER_COLOR : BAS_PRINTER_MONO);
+
+ case PrPCopiesE:
+ return basValInteger((int16_t)(p->copies > 0 ? p->copies : 1));
+
+ case PrPDeviceNameE:
+ case PrPPortE: {
+ PrintSettingsT st;
+
+ p->api->settingsLoad(&st);
+ return basValStringFromC(prop == PrPDeviceNameE ? p->api->modelLabel(st.model) : p->api->destLabel(st.dest));
+ }
+
+ case PrPFontBoldE:
+ case PrPFontItalicE:
+ case PrPFontStrikethruE:
+ case PrPFontUnderlineE:
+ return basValBool((p->fontStyle & sFontBits[prop]) != 0);
+
+ case PrPFontNameE:
+ return basValStringFromC(p->fontName);
+
+ case PrPFontSizeE:
+ return basValSingle((float)(p->fontTenths / PR_POINT_TENTHS));
+
+ case PrPHeightE:
+ return basValLong(basGfxRound(p->page.heightDots * prTwipsY(p)));
+
+ case PrPOrientationE:
+ return basValInteger(BAS_PRINTER_PORTRAIT);
+
+ case PrPPageE:
+ return basValLong(p->pageNum);
+
+ case PrPTwipsPerPixelXE:
+ return basValSingle((float)prTwipsX(p));
+
+ case PrPTwipsPerPixelYE:
+ return basValSingle((float)prTwipsY(p));
+
+ case PrPWidthE:
+ return basValLong(basGfxRound(p->page.widthDots * prTwipsX(p)));
+
+ default:
+ break;
+ }
+
+ if (!basFormRtDrawStateGet(p->draw, name, &v)) {
+ basFormRtRuntimeError(rt, "Printer has no such property", "Property: %s", name);
+ }
+
+ return v;
+}
+
+
+bool basFormRtPrinterIs(const void *ref) {
+ return ref == &sPrinterObj;
+}
+
+
+BasValueT basFormRtPrinterMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc) {
+ BasValueT result = basValInteger(0);
+ BasPrinterT *p = prGet(rt);
+ int32_t f;
+
+ if (!p) {
+ return result;
+ }
+
+ switch ((PrMethodE)prFind(sMethods, PrMCountE, name)) {
+ case PrMCircleE:
+ prShape(rt, p, PrOpCircleE, args, argc);
+ break;
+
+ case PrMEndDocE:
+ prEndDoc(rt, p);
+ break;
+
+ case PrMKillDocE:
+ prKillDoc(p);
+ break;
+
+ case PrMLineE:
+ prShape(rt, p, PrOpLineE, args, argc);
+ break;
+
+ case PrMNewPageE:
+ prNewPage(rt, p);
+ break;
+
+ case PrMPaintPictureE:
+ prPicture(rt, p, args, argc);
+ break;
+
+ case PrMPrintE:
+ if (argc != BAS_OBJ_PRINT_ARGS) {
+ basFormRtRuntimeError(rt, "Illegal function call", "Method: Print");
+ break;
+ }
+
+ f = prPage(rt, p, true) ? prFont(p) : PR_UNSET;
+
+ if (f != PR_UNSET) {
+ PrintFontT *font = p->fonts[f].font;
+
+ basFormRtDrawPrintItem(p->draw, p->api->fontWidth(font, PR_DIGIT, 1), p->api->fontHeight(font), args[0], basValToInt32(args[1]), prText, p);
+ }
+
+ break;
+
+ case PrMPsetE:
+ prShape(rt, p, PrOpPsetE, args, argc);
+ break;
+
+ case PrMTextHeightE:
+ case PrMTextWidthE:
+ if (argc != 1) {
+ basFormRtRuntimeError(rt, "Illegal function call", "Method: %s\nTextWidth and TextHeight take the text to measure.", name);
+ break;
+ }
+
+ f = prPage(rt, p, false) ? prFont(p) : PR_UNSET;
+
+ if (f != PR_UNSET) {
+ BasStringT *str = basValFormatString(args[0]);
+ PrintFontT *font = p->fonts[f].font;
+ BasGfxT *g = basFormRtDrawGfx(p->draw);
+ bool wide = strcasecmp(name, sMethods[PrMTextWidthE]) == 0;
+
+ result = basValDouble(fabs(wide ? basGfxToUnitsX(g, p->api->fontWidth(font, str->data, str->len)) : basGfxToUnitsY(g, p->api->fontHeight(font))));
+ basStringUnref(str);
+ }
+
+ break;
+
+ default:
+ if (!prPage(rt, p, false)) {
+ break;
+ }
+
+ p->stateChanged = true;
+
+ if (!basFormRtDrawScaleCall(rt, p->draw, name, args, argc, &result)) {
+ basFormRtRuntimeError(rt, "Printer has no such method", "Method: %s", name);
+ }
+
+ break;
+ }
+
+ return result;
+}
+
+
+void *basFormRtPrinterRef(void) {
+ return (void *)&sPrinterObj;
+}
+
+
+void basFormRtPrinterSetProp(BasFormRtT *rt, const char *name, BasValueT value) {
+ BasPrinterT *p = prGet(rt);
+ int32_t prop = prFind(sProps, PrPCountE, name);
+
+ if (!p || !prPage(rt, p, false)) {
+ return;
+ }
+
+ switch ((PrPropE)prop) {
+ case PrPCopiesE: {
+ int32_t n = basValToInt32(value);
+
+ if (n < 1 || n > PRINT_COPIES_MAX) {
+ basFormRtRuntimeError(rt, "Invalid property value", "Copies must be 1 to %d.", PRINT_COPIES_MAX);
+ return;
+ }
+
+ p->copies = n;
+ return;
+ }
+
+ case PrPFontBoldE:
+ case PrPFontItalicE:
+ case PrPFontStrikethruE:
+ case PrPFontUnderlineE:
+ p->fontStyle = basValIsTruthy(value) ? p->fontStyle | sFontBits[prop] : p->fontStyle & ~sFontBits[prop];
+ p->font = PR_UNSET;
+ return;
+
+ case PrPFontNameE: {
+ BasStringT *str = basValFormatString(value);
+
+ snprintf(p->fontName, sizeof(p->fontName), "%s", str->data);
+ basStringUnref(str);
+ p->font = PR_UNSET;
+ return;
+ }
+
+ case PrPFontSizeE: {
+ double points = basValToNumber(value);
+
+ if (!(points > 0.0 && points <= PR_FONT_SIZE_MAX)) {
+ basFormRtRuntimeError(rt, "Invalid property value", "FontSize is in points, above 0 and at most %d.", (int)PR_FONT_SIZE_MAX);
+ return;
+ }
+
+ p->fontTenths = basGfxRound(points * PR_POINT_TENTHS);
+ p->fontTenths = p->fontTenths < 1 ? 1 : p->fontTenths;
+ p->font = PR_UNSET;
+ return;
+ }
+
+ case PrPOrientationE:
+ if (basValToInt32(value) != BAS_PRINTER_PORTRAIT) {
+ basFormRtRuntimeError(rt, "Invalid property value", "The Printer prints portrait pages only (vbPRORPortrait).");
+ }
+
+ return;
+
+ case PrPColorModeE:
+ case PrPDeviceNameE:
+ case PrPHeightE:
+ case PrPPageE:
+ case PrPPortE:
+ case PrPTwipsPerPixelXE:
+ case PrPTwipsPerPixelYE:
+ case PrPWidthE:
+ basFormRtRuntimeError(rt, "Property is read-only", "Property: Printer.%s", name);
+ return;
+
+ default:
+ break;
+ }
+
+ p->stateChanged = true;
+
+ if (!basFormRtDrawStateSet(rt, p->draw, name, value)) {
+ basFormRtRuntimeError(rt, "Printer has no such property", "Property: %s", name);
+ }
+}
+
+
+// The print library's calls, looked up once; NULL when any is missing.
+static const PrApiT *prApi(void) {
+ if (!sApiTried) {
+ sApiTried = true;
+ sApi.settingsLoad = (__typeof__(sApi.settingsLoad))dlsym(NULL, DVX_SYM("printSettingsLoad"));
+ sApi.modelPage = (__typeof__(sApi.modelPage))dlsym(NULL, DVX_SYM("printModelPage"));
+ sApi.modelLabel = (__typeof__(sApi.modelLabel))dlsym(NULL, DVX_SYM("printModelLabel"));
+ sApi.destLabel = (__typeof__(sApi.destLabel))dlsym(NULL, DVX_SYM("printDestLabel"));
+ sApi.basChoice = (__typeof__(sApi.basChoice))dlsym(NULL, DVX_SYM("printBasChoice"));
+ sApi.docOpen = (__typeof__(sApi.docOpen))dlsym(NULL, DVX_SYM("printUiDocOpen"));
+ sApi.docClose = (__typeof__(sApi.docClose))dlsym(NULL, DVX_SYM("printUiDocClose"));
+ sApi.docAbandon = (__typeof__(sApi.docAbandon))dlsym(NULL, DVX_SYM("printDocClose"));
+ sApi.docPage = (__typeof__(sApi.docPage))dlsym(NULL, DVX_SYM("printDocPage"));
+ sApi.fontOpen = (__typeof__(sApi.fontOpen))dlsym(NULL, DVX_SYM("printFontOpen"));
+ sApi.fontClose = (__typeof__(sApi.fontClose))dlsym(NULL, DVX_SYM("printFontClose"));
+ sApi.fontWidth = (__typeof__(sApi.fontWidth))dlsym(NULL, DVX_SYM("printFontWidth"));
+ sApi.fontHeight = (__typeof__(sApi.fontHeight))dlsym(NULL, DVX_SYM("printFontHeight"));
+ sApi.fontDraw = (__typeof__(sApi.fontDraw))dlsym(NULL, DVX_SYM("printFontDraw"));
+ sApi.vecBegin = (__typeof__(sApi.vecBegin))dlsym(NULL, DVX_SYM("printVecBegin"));
+ sApi.vecEnd = (__typeof__(sApi.vecEnd))dlsym(NULL, DVX_SYM("printVecEnd"));
+ sApi.vecLine = (__typeof__(sApi.vecLine))dlsym(NULL, DVX_SYM("printVecLine"));
+ sApi.vecRect = (__typeof__(sApi.vecRect))dlsym(NULL, DVX_SYM("printVecRect"));
+ sApi.vecEllipse = (__typeof__(sApi.vecEllipse))dlsym(NULL, DVX_SYM("printVecEllipse"));
+ sApi.vecEllipseFill = (__typeof__(sApi.vecEllipseFill))dlsym(NULL, DVX_SYM("printVecEllipseFill"));
+ sApi.vecDot = (__typeof__(sApi.vecDot))dlsym(NULL, DVX_SYM("printVecDot"));
+ sApi.vecText = (__typeof__(sApi.vecText))dlsym(NULL, DVX_SYM("printVecText"));
+ sApi.vecImage = (__typeof__(sApi.vecImage))dlsym(NULL, DVX_SYM("printVecImage"));
+ }
+
+ bool all = sApi.settingsLoad && sApi.modelPage && sApi.modelLabel && sApi.destLabel && sApi.basChoice && sApi.docOpen && sApi.docClose && sApi.docAbandon && sApi.docPage && sApi.fontOpen && sApi.fontClose && sApi.fontWidth && sApi.fontHeight && sApi.fontDraw;
+
+ all = all && sApi.vecBegin && sApi.vecEnd && sApi.vecLine && sApi.vecRect && sApi.vecEllipse && sApi.vecEllipseFill && sApi.vecDot && sApi.vecText && sApi.vecImage;
+
+ return all ? &sApi : NULL;
+}
+
+
+// A span of a kept shape: the part of it in the band, in its color.
+static void prBandSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel) {
+ const PrBandT *b = (const PrBandT *)ctx;
+ uint32_t rgb = wgtBasicColor(pixel);
+ uint8_t c[PR_RGB_BYTES];
+
+ if (y < b->y0 || y >= b->y0 + b->rows) {
+ return;
+ }
+
+ c[0] = (uint8_t)((rgb >> PR_RED_SHIFT) & PR_BYTE_MASK);
+ c[1] = (uint8_t)((rgb >> PR_GREEN_SHIFT) & PR_BYTE_MASK);
+ c[2] = (uint8_t)(rgb & PR_BYTE_MASK);
+ x0 = x0 < 0 ? 0 : x0;
+ x1 = x1 >= b->width ? b->width - 1 : x1;
+
+ for (uint8_t *dst = b->rgb + ((int64_t)(y - b->y0) * b->width + x0) * PR_RGB_BYTES; x0 <= x1; x0++, dst += PR_RGB_BYTES) {
+ memcpy(dst, c, PR_RGB_BYTES);
+ }
+}
+
+
+// EndDoc: the page drawn printed, the document ended (a failure told
+// to the user), Page back to 1.
+static void prEndDoc(BasFormRtT *rt, BasPrinterT *p) {
+ if (arrlen(p->ops) > 0) {
+ prPrintPage(rt, p);
+ }
+
+ if (p->doc) {
+ p->api->docClose(rt->ctx, p->doc);
+ p->doc = NULL;
+ }
+
+ prPageClear(p);
+ prFontsClose(p);
+ p->pageNum = 1;
+ prHome(p);
+}
+
+
+// The index of name (case ignored) in names, -1 when it is not there.
+static int32_t prFind(const char *const *names, int32_t count, const char *name) {
+ for (int32_t i = 0; i < count; i++) {
+ if (strcasecmp(names[i], name) == 0) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+
+// The font text is in now (FontName, FontSize, the style): fonts[] index,
+// opened at the page's resolution on first use; PR_UNSET when it cannot
+// be opened.
+static int32_t prFont(BasPrinterT *p) {
+ if (p->font != PR_UNSET) {
+ return p->font;
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(p->fonts); i++) {
+ if (p->fonts[i].tenths == p->fontTenths && p->fonts[i].style == p->fontStyle && strcasecmp(p->fonts[i].family, p->fontName) == 0) {
+ p->font = i;
+ return i;
+ }
+ }
+
+ PrFontT f;
+
+ memset(&f, 0, sizeof(f));
+ snprintf(f.family, sizeof(f.family), "%s", p->fontName);
+ f.tenths = p->fontTenths;
+ f.style = p->fontStyle;
+ f.font = p->api->fontOpen(&p->page, f.family, f.tenths, f.style);
+
+ if (!f.font) {
+ return PR_UNSET;
+ }
+
+ arrput(p->fonts, f);
+ p->font = (int32_t)arrlen(p->fonts) - 1;
+ return p->font;
+}
+
+
+// The document's fonts given up (no kept text uses them).
+static void prFontsClose(BasPrinterT *p) {
+ for (int32_t i = 0; i < (int32_t)arrlen(p->fonts); i++) {
+ p->api->fontClose(p->fonts[i].font);
+ }
+
+ arrfree(p->fonts);
+ p->font = PR_UNSET;
+}
+
+
+// The runtime's Printer, made on first use; NULL (a runtime error raised)
+// when printing is not installed.
+static BasPrinterT *prGet(BasFormRtT *rt) {
+ const PrApiT *api = prApi();
+
+ if (!api) {
+ basFormRtRuntimeError(rt, "Printing is not installed", "The Printer object needs the printing library (print.lib).");
+ return NULL;
+ }
+
+ if (!rt->printer) {
+ BasPrinterT *p = (BasPrinterT *)calloc(1, sizeof(BasPrinterT));
+
+ if (!p) {
+ basFormRtRuntimeError(rt, "Out of memory", "There is not enough memory for the Printer.");
+ return NULL;
+ }
+
+ p->api = api;
+ p->pageNum = 1;
+ p->fontTenths = PR_DEFAULT_TENTHS;
+ p->font = PR_UNSET;
+ snprintf(p->fontName, sizeof(p->fontName), "%s", PR_DEFAULT_FONT);
+ rt->printer = p;
+ }
+
+ return rt->printer;
+}
+
+
+// CurrentX / CurrentY to the page's top-left corner.
+static void prHome(BasPrinterT *p) {
+ if (!p->draw) {
+ return;
+ }
+
+ BasGfxT *g = basFormRtDrawGfx(p->draw);
+
+ g->lastX = g->world ? g->worldLeft : 0.0;
+ g->lastY = g->world ? g->worldTop : 0.0;
+}
+
+
+// KillDoc: the page thrown away, a document ended as it is (pages
+// already printed stay printed), Page back to 1.
+static void prKillDoc(BasPrinterT *p) {
+ prPageClear(p);
+
+ if (p->doc) {
+ p->api->docAbandon(p->doc);
+ p->doc = NULL;
+ }
+
+ prFontsClose(p);
+ p->pageNum = 1;
+ prHome(p);
+}
+
+
+// The measuring surface is paper: white.
+static uint32_t prMeasureGet(const void *ctx, int32_t x, int32_t y) {
+ (void)ctx;
+ (void)x;
+ (void)y;
+ return PR_WHITE;
+}
+
+
+// The measuring surface draws nothing: it finds the rows a call touches.
+static void prMeasureSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel) {
+ BasPrinterT *p = (BasPrinterT *)ctx;
+
+ (void)x0;
+ (void)x1;
+ (void)pixel;
+ p->touchTop = y < p->touchTop ? y : p->touchTop;
+ p->touchBottom = y > p->touchBottom ? y : p->touchBottom;
+}
+
+
+// NewPage: the page printed (a blank one too), the next one started.
+static void prNewPage(BasFormRtT *rt, BasPrinterT *p) {
+ if (!prPage(rt, p, true)) {
+ return;
+ }
+
+ prPrintPage(rt, p);
+ p->pageNum++;
+ prHome(p);
+}
+
+
+// The page to draw on: between documents the printer's page is read
+// again (the user may have chosen another printer), the drawing state
+// kept (all of it while the page is the same); drawing needs a model that
+// prints pictures. False, a runtime
+// error raised, when it cannot be had.
+static bool prPage(BasFormRtT *rt, BasPrinterT *p, bool drawing) {
+ const PrApiT *api = p->api;
+
+ if (!p->draw || (!p->doc && arrlen(p->ops) == 0)) {
+ PrintSettingsT st;
+ PrintPageInfoT page;
+
+ api->settingsLoad(&st);
+ p->graphics = api->modelPage(st.model, &page);
+
+ if (!p->graphics) {
+ api->modelPage(PR_STANDIN_MODEL, &page);
+ }
+
+ if (!p->draw || !prPageSame(&page, &p->page)) {
+ BasGfxSurfaceT surf;
+
+ memset(&surf, 0, sizeof(surf));
+ surf.ctx = p;
+ surf.width = page.widthDots;
+ surf.height = page.heightDots;
+ surf.span = prMeasureSpan;
+ surf.get = prMeasureGet;
+
+ BasDrawT *d = basFormRtDrawNew(rt->ctx, &surf, PR_TWIPS_PER_INCH / page.dpiX, PR_TWIPS_PER_INCH / page.dpiY, BAS_SCALE_TWIPS);
+
+ if (!d) {
+ basFormRtRuntimeError(rt, "Out of memory", "There is not enough memory for the Printer's page.");
+ return false;
+ }
+
+ for (int32_t i = 0; p->draw && i < DVX_ARRAY_LEN(sKeptProps); i++) {
+ BasValueT v;
+
+ if (basFormRtDrawStateGet(p->draw, sKeptProps[i], &v)) {
+ basFormRtDrawStateSet(rt, d, sKeptProps[i], v);
+ }
+ }
+
+ basFormRtDrawDelete(p->draw);
+ prFontsClose(p);
+ p->draw = d;
+ p->page = page;
+ p->stateChanged = true;
+ }
+ }
+
+ if (drawing && !p->graphics) {
+ basFormRtRuntimeError(rt, "The printer prints text only", "The printer model chosen in the Control Panel prints text only. To print with the Printer object, choose your printer's model on the Control Panel's Printer page.");
+ return false;
+ }
+
+ return true;
+}
+
+
+// One band of the page: every kept call whose rows meet it, done again
+// into the band in the order it was made.
+static bool prPageBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows) {
+ BasPrinterT *p = (BasPrinterT *)ctx;
+ PrBandT b = { rgb, width, y0, rows };
+ BasGfxSurfaceT surf = basFormRtDrawGfx(p->draw)->surf;
+ int32_t last = y0 + rows - 1;
+
+ surf.ctx = &b;
+ surf.span = prBandSpan;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(p->ops); i++) {
+ const PrOpT *op = &p->ops[i];
+
+ if (op->bottom < y0 || op->top > last) {
+ continue;
+ }
+
+ if (op->kind == PrOpTextE) {
+ p->api->fontDraw(p->fonts[op->u.text.font].font, op->u.text.x, op->u.text.y, op->u.text.text, op->u.text.len, op->u.text.color, rgb, width, y0, rows);
+ } else if (op->kind == PrOpPictureE) {
+ prPictureBand(op, &b);
+ } else {
+ prShapeAgain(p, op, &surf);
+ }
+ }
+
+ return true;
+}
+
+
+// The page's kept drawing thrown away.
+static void prPageClear(BasPrinterT *p) {
+ for (int32_t i = 0; i < (int32_t)arrlen(p->ops); i++) {
+ if (p->ops[i].kind == PrOpTextE) {
+ free(p->ops[i].u.text.text);
+ } else if (p->ops[i].kind == PrOpPictureE) {
+ free(p->ops[i].u.picture.rgb);
+ }
+ }
+
+ arrfree(p->ops);
+ arrfree(p->states);
+}
+
+
+// Whether two pages are the same size at the same resolution, in color or
+// not, drawn in dots or by the printer.
+static bool prPageSame(const PrintPageInfoT *a, const PrintPageInfoT *b) {
+ return a->dpiX == b->dpiX && a->dpiY == b->dpiY && a->widthDots == b->widthDots && a->heightDots == b->heightDots && a->color == b->color && a->vector == b->vector;
+}
+
+
+// PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth,
+// srcHeight]]: the picture (or the part of it) stretched to the
+// rectangle, nearest pixel; without a width and height it prints at its
+// size on the screen (15 twips a pixel). It is copied as it is (a
+// raster operation after the source rectangle is not used on paper).
+static void prPicture(BasFormRtT *rt, BasPrinterT *p, BasValueT *args, int32_t argc) {
+ const BasGfxT *srcScale = NULL;
+ int32_t w = 0;
+ int32_t h = 0;
+
+ if (argc < DRAW_PAINT_MIN_ARGS) {
+ basFormRtRuntimeError(rt, "PaintPicture needs a picture, x, and y", "Usage: Printer.PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight]]");
+ return;
+ }
+
+ if (!prPage(rt, p, true)) {
+ return;
+ }
+
+ uint8_t *rgb = basFormRtDrawPictureRgb(rt, NULL, args[0], &w, &h, &srcScale);
+
+ if (!rgb) {
+ return;
+ }
+
+ BasGfxT *g = basFormRtDrawGfx(p->draw);
+ PrOpT op;
+
+ memset(&op, 0, sizeof(op));
+ op.kind = PrOpPictureE;
+ op.u.picture.rgb = rgb;
+ op.u.picture.w = w;
+ op.u.picture.h = h;
+ op.u.picture.sw = w;
+ op.u.picture.sh = h;
+
+ if (argc >= DRAW_PAINT_SRC_ARGS) {
+ basFormRtDrawToPixels(srcScale, basValToNumber(args[5]), basValToNumber(args[6]), basValToNumber(args[7]), basValToNumber(args[8]), &op.u.picture.sx, &op.u.picture.sy, &op.u.picture.sw, &op.u.picture.sh);
+ }
+
+ basFormRtDrawToPixels(g, basValToNumber(args[1]), basValToNumber(args[2]), argc >= DRAW_PAINT_SIZE_ARGS ? basValToNumber(args[3]) : 0.0, argc >= DRAW_PAINT_SIZE_ARGS ? basValToNumber(args[4]) : 0.0, &op.u.picture.dx, &op.u.picture.dy, &op.u.picture.dw, &op.u.picture.dh);
+
+ if (argc < DRAW_PAINT_SIZE_ARGS) {
+ op.u.picture.dw = basGfxRound(op.u.picture.sw * PR_SCREEN_TWIPS / prTwipsX(p));
+ op.u.picture.dh = basGfxRound(op.u.picture.sh * PR_SCREEN_TWIPS / prTwipsY(p));
+ }
+
+ // The source rectangle inside the picture
+ int32_t sx = op.u.picture.sx < 0 ? 0 : op.u.picture.sx;
+ int32_t sy = op.u.picture.sy < 0 ? 0 : op.u.picture.sy;
+
+ op.u.picture.sw = (sx + op.u.picture.sw > w ? w - sx : op.u.picture.sw);
+ op.u.picture.sh = (sy + op.u.picture.sh > h ? h - sy : op.u.picture.sh);
+ op.u.picture.sx = sx;
+ op.u.picture.sy = sy;
+ op.top = op.u.picture.dy < 0 ? 0 : op.u.picture.dy;
+ op.bottom = op.u.picture.dy + op.u.picture.dh - 1;
+
+ if (op.u.picture.sw <= 0 || op.u.picture.sh <= 0 || op.u.picture.dw <= 0 || op.u.picture.dh <= 0 || op.bottom < op.top) {
+ free(rgb);
+ return;
+ }
+
+ arrput(p->ops, op);
+}
+
+
+// A kept picture's rows in the band.
+static void prPictureBand(const PrOpT *op, const PrBandT *b) {
+ int32_t top = op->top > b->y0 ? op->top : b->y0;
+ int32_t bottom = op->bottom < b->y0 + b->rows - 1 ? op->bottom : b->y0 + b->rows - 1;
+ int32_t x0 = op->u.picture.dx < 0 ? 0 : op->u.picture.dx;
+ int32_t x1 = op->u.picture.dx + op->u.picture.dw < b->width ? op->u.picture.dx + op->u.picture.dw : b->width;
+
+ for (int32_t y = top; y <= bottom; y++) {
+ int32_t ry = op->u.picture.sy + (int32_t)((int64_t)(y - op->u.picture.dy) * op->u.picture.sh / op->u.picture.dh);
+ uint8_t *dst = b->rgb + (int64_t)(y - b->y0) * b->width * PR_RGB_BYTES;
+
+ for (int32_t x = x0; x < x1; x++) {
+ int32_t rx = op->u.picture.sx + (int32_t)((int64_t)(x - op->u.picture.dx) * op->u.picture.sw / op->u.picture.dw);
+
+ memcpy(dst + (int64_t)x * PR_RGB_BYTES, op->u.picture.rgb + ((int64_t)ry * op->u.picture.w + rx) * PR_RGB_BYTES, PR_RGB_BYTES);
+ }
+ }
+}
+
+
+// The page out to the printer, Copies times, the document opened first
+// (where the program's last PrintChoose said, else the settings'); the
+// page's drawing is then thrown away. False when it did not print.
+static bool prPrintPage(BasFormRtT *rt, BasPrinterT *p) {
+ const PrApiT *api = p->api;
+ bool ok = true;
+
+ if (!p->doc) {
+ PrintChoiceT ch;
+ bool chose = api->basChoice(&ch);
+
+ p->doc = api->docOpen(rt->ctx, rt->title[0] ? rt->title : PR_DEFAULT_TITLE, chose ? &ch : NULL);
+ p->docCopies = p->copies > 0 ? p->copies : (chose && ch.copies > 0 ? ch.copies : 1);
+ ok = p->doc != NULL;
+ }
+
+ for (int32_t c = 0; ok && c < p->docCopies; c++) {
+ ok = p->page.vector ? prVecPage(p) : api->docPage(p->doc, prPageBand, p);
+ }
+
+ prPageClear(p);
+ return ok;
+}
+
+
+// Line, Circle or PSet: its argument block done at once on the measuring
+// surface (CurrentX / CurrentY move, the rows it touches are found) and
+// kept with the drawing state it was done in.
+static void prShape(BasFormRtT *rt, BasPrinterT *p, PrOpKindE kind, BasValueT *args, int32_t argc) {
+ int32_t need = kind == PrOpLineE ? BAS_GFX_LINE_ARGS : (kind == PrOpCircleE ? BAS_GFX_CIRCLE_ARGS : BAS_GFX_PSET_ARGS);
+
+ if (!prPage(rt, p, true)) {
+ return;
+ }
+
+ if (argc != need) {
+ basFormRtRuntimeError(rt, "Illegal function call", "Method: Printer.%s", kind == PrOpLineE ? sMethods[PrMLineE] : (kind == PrOpCircleE ? sMethods[PrMCircleE] : sMethods[PrMPsetE]));
+ return;
+ }
+
+ BasGfxT *g = basFormRtDrawGfx(p->draw);
+ PrOpT op;
+ int32_t n = (int32_t)arrlen(p->states);
+
+ memset(&op, 0, sizeof(op));
+ op.kind = kind;
+ op.u.shape.lastX = g->lastX;
+ op.u.shape.lastY = g->lastY;
+ op.u.shape.argc = argc;
+
+ for (int32_t a = 0; a < argc; a++) {
+ op.u.shape.args[a] = basValToNumber(args[a]);
+ }
+
+ // The state is kept again only once a property or Scale changed it
+ if (n == 0 || p->stateChanged) {
+ arrput(p->states, *g);
+ n++;
+ p->stateChanged = false;
+ }
+
+ op.u.shape.state = n - 1;
+ p->touchTop = INT32_MAX;
+ p->touchBottom = PR_UNSET;
+
+ if (prShapeDo(g, kind, args) != 0) {
+ basFormRtRuntimeError(rt, "Illegal function call", "Method: Printer.%s\nAn argument is out of range (a colour, a radius, or an angle).", kind == PrOpLineE ? sMethods[PrMLineE] : (kind == PrOpCircleE ? sMethods[PrMCircleE] : sMethods[PrMPsetE]));
+ return;
+ }
+
+ // Nothing on the page (off it, or empty): nothing kept
+ if (p->touchBottom < 0) {
+ return;
+ }
+
+ op.top = p->touchTop;
+ op.bottom = p->touchBottom;
+ arrput(p->ops, op);
+}
+
+
+// A kept shape done again on surf, in the drawing state it was done in.
+static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf) {
+ BasGfxT g = p->states[op->u.shape.state];
+ BasValueT args[BAS_GFX_CIRCLE_ARGS];
+
+ g.surf = *surf;
+ g.lastX = op->u.shape.lastX;
+ g.lastY = op->u.shape.lastY;
+
+ for (int32_t a = 0; a < op->u.shape.argc; a++) {
+ args[a] = basValDouble(op->u.shape.args[a]);
+ }
+
+ prShapeDo(&g, op->kind, args);
+}
+
+
+static int32_t prShapeDo(BasGfxT *g, PrOpKindE kind, const BasValueT *args) {
+ switch (kind) {
+ case PrOpLineE:
+ return basGfxLine(g, args);
+
+ case PrOpCircleE:
+ return basGfxCircle(g, args);
+
+ default:
+ return basGfxPset(g, args);
+ }
+}
+
+
+// Print's text (basFormRtDrawPrintItem): kept at its place in ForeColor;
+// its width in dots.
+static int32_t prText(void *ctx, const char *text, int32_t len, int32_t x, int32_t y) {
+ BasPrinterT *p = (BasPrinterT *)ctx;
+ PrintFontT *font = p->fonts[p->font].font;
+ int32_t w = p->api->fontWidth(font, text, len);
+ PrOpT op;
+
+ if (len <= 0) {
+ return w;
+ }
+
+ memset(&op, 0, sizeof(op));
+ op.kind = PrOpTextE;
+ op.top = y;
+ op.bottom = y + p->api->fontHeight(font) - 1;
+ op.u.text.font = p->font;
+ op.u.text.x = x;
+ op.u.text.y = y;
+ op.u.text.color = wgtBasicColor(basFormRtDrawGfx(p->draw)->fore);
+ op.u.text.len = len;
+ op.u.text.text = (char *)malloc((size_t)len + 1);
+
+ if (op.u.text.text) {
+ memcpy(op.u.text.text, text, (size_t)len + 1);
+ arrput(p->ops, op);
+ }
+
+ return w;
+}
+
+
+// Twips in a printer dot across and down.
+static double prTwipsX(const BasPrinterT *p) {
+ return PR_TWIPS_PER_INCH / p->page.dpiX;
+}
+
+
+static double prTwipsY(const BasPrinterT *p) {
+ return PR_TWIPS_PER_INCH / p->page.dpiY;
+}
+
+
+// The vector page's shapes: to the printer whole, in RGB.
+static void prVecDot(void *ctx, int32_t x, int32_t y, uint32_t pixel, int32_t width) {
+ const BasPrinterT *p = (const BasPrinterT *)ctx;
+
+ p->api->vecDot(p->doc, x, y, wgtBasicColor(pixel), width);
+}
+
+
+static void prVecEllipse(void *ctx, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, int32_t width, uint16_t style, bool arc, double start, double end) {
+ const BasPrinterT *p = (const BasPrinterT *)ctx;
+
+ p->api->vecEllipse(p->doc, cx, cy, rx, ry, wgtBasicColor(pixel), width, style, arc, start, end);
+}
+
+
+static void prVecEllipseFill(void *ctx, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, int32_t fillStyle) {
+ const BasPrinterT *p = (const BasPrinterT *)ctx;
+
+ p->api->vecEllipseFill(p->doc, cx, cy, rx, ry, wgtBasicColor(pixel), sFills[fillStyle]);
+}
+
+
+static void prVecLine(void *ctx, double x0, double y0, double x1, double y1, uint32_t pixel, int32_t width, uint16_t style) {
+ const BasPrinterT *p = (const BasPrinterT *)ctx;
+
+ p->api->vecLine(p->doc, x0, y0, x1, y1, wgtBasicColor(pixel), width, style);
+}
+
+
+// A vector page: every kept call done again once, in the order it was
+// made, as the printer's own drawing. False when it did not print.
+static bool prVecPage(BasPrinterT *p) {
+ const PrApiT *api = p->api;
+ BasGfxSurfaceT surf = basFormRtDrawGfx(p->draw)->surf;
+
+ if (!api->vecBegin(p->doc)) {
+ return false;
+ }
+
+ surf.ctx = p;
+ surf.vec = &sPrVec;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(p->ops); i++) {
+ const PrOpT *op = &p->ops[i];
+
+ if (op->kind == PrOpTextE) {
+ api->vecText(p->doc, p->fonts[op->u.text.font].font, op->u.text.x, op->u.text.y, op->u.text.text, op->u.text.len, op->u.text.color);
+ } else if (op->kind == PrOpPictureE) {
+ api->vecImage(p->doc, op->u.picture.rgb, op->u.picture.w, op->u.picture.h, op->u.picture.sx, op->u.picture.sy, op->u.picture.sw, op->u.picture.sh, op->u.picture.dx, op->u.picture.dy, op->u.picture.dw, op->u.picture.dh);
+ } else {
+ prShapeAgain(p, op, &surf);
+ }
+ }
+
+ return api->vecEnd(p->doc);
+}
+
+
+static void prVecRect(void *ctx, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, int32_t fillStyle) {
+ const BasPrinterT *p = (const BasPrinterT *)ctx;
+
+ p->api->vecRect(p->doc, x0, y0, x1, y1, wgtBasicColor(pixel), sFills[fillStyle]);
+}
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
index b05c46ab..9acb6e9f 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
@@ -1497,6 +1497,10 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
return screenMethod(rt, methodName, args, argc);
}
+ if (basFormRtPrinterIs(ctrlRef)) {
+ return basFormRtPrinterMethod(rt, methodName, args, argc);
+ }
+
BasValueT ucResult;
if (basUcSurfaceCall(rt, ctrlRef, methodName, args, argc, &ucResult)) {
@@ -1866,7 +1870,7 @@ const char *basFormRtDefaultProp(void *ctx, void *objRef) {
return BAS_PIC_PROP_DEFAULT;
}
- BasControlT *ctrl = (objRef == &sClipboardObj || objRef == &sScreenObj) ? NULL : resolveCtrlRef(rt, objRef);
+ BasControlT *ctrl = (objRef == &sClipboardObj || objRef == &sScreenObj || basFormRtPrinterIs(objRef)) ? NULL : resolveCtrlRef(rt, objRef);
if (!ctrl || ctrl->menuId > 0 || (ctrl->form && ctrl == &ctrl->form->formCtrl)) {
return NULL;
@@ -1899,6 +1903,9 @@ void basFormRtDestroy(BasFormRtT *rt) {
basFormRtScreenClose(rt);
+ // A page drawn on the Printer, or its document, is printed (VB)
+ basFormRtPrinterEnd(rt);
+
// A busy cursor the program left on goes with it
if (rt->pointerBusy) {
dvxSetBusy(rt->ctx, false);
@@ -2013,12 +2020,16 @@ void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
}
}
- // VB's Clipboard and Screen objects, unless a control or form has the
- // name.
+ // VB's Clipboard, Printer, and Screen objects, unless a control or
+ // form has the name.
if (ctrlName && strcasecmp(ctrlName, BAS_CLIPBOARD_NAME) == 0) {
return (void *)&sClipboardObj;
}
+ if (ctrlName && strcasecmp(ctrlName, BAS_PRINTER_OBJ_NAME) == 0) {
+ return basFormRtPrinterRef();
+ }
+
return (ctrlName && strcasecmp(ctrlName, BAS_SCREEN_NAME) == 0) ? (void *)&sScreenObj : NULL;
}
@@ -2121,6 +2132,10 @@ BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
return screenProp(rt, propName);
}
+ if (basFormRtPrinterIs(ctrlRef)) {
+ return basFormRtPrinterGetProp(rt, propName);
+ }
+
const BasPictureT *pic = pictureFind(rt, ctrlRef);
if (pic) {
@@ -3476,6 +3491,10 @@ const char *basFormRtTypeName(void *ctx, void *objRef) {
return BAS_SCREEN_NAME;
}
+ if (basFormRtPrinterIs(objRef)) {
+ return BAS_PRINTER_OBJ_NAME;
+ }
+
if (pictureFind(rt, objRef)) {
return BAS_PICTURE_TYPE_NAME;
}
@@ -9583,6 +9602,11 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba
return;
}
+ if (basFormRtPrinterIs(ctrlRef)) {
+ basFormRtPrinterSetProp(rt, propName, value);
+ return;
+ }
+
if (pictureFind(rt, ctrlRef)) {
basFormRtRuntimeError(rt,
"Picture properties are read-only",
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
index a2e03824..0e4e221f 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
@@ -34,6 +34,7 @@
#include "../runtime/vm.h"
#include "../runtime/values.h"
+#include "../runtime/gfx.h"
#include "../../../../libs/kpunch/game/game.h"
#include "dvxApp.h"
#include "dvxDlg.h"
@@ -49,6 +50,7 @@
typedef struct BasFormT BasFormT;
typedef struct BasControlT BasControlT;
typedef struct BasDrawT BasDrawT; // a Form's / PictureBox's drawing state (formGfx.c)
+typedef struct BasPrinterT BasPrinterT; // the Printer object's state (formPrinter.c)
typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (formGfx.c)
// ============================================================
@@ -604,6 +606,8 @@ typedef struct {
// Screen.MousePointer when it is not vbHourglass: the pointer over
// every window of the program (BAS_POINTER_DEFAULT: each control's).
int32_t screenPointer;
+ // The Printer object's pages and document (NULL until it is used).
+ BasPrinterT *printer;
} BasFormRtT;
// ============================================================
@@ -650,8 +654,8 @@ WindowT *basFormRtScreenWindow(const BasFormRtT *rt);
// True when name is a Form / PictureBox drawing method (Cls, PSet, Line,
// Circle, Point, Print, PaintPicture, Scale, ScaleX, ScaleY, TextWidth,
// TextHeight) or, with isProp, a drawing property (CurrentX, CurrentY,
-// DrawWidth, FillColor, FillStyle, ForeColor, BackColor, AutoRedraw,
-// ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight).
+// DrawStyle, DrawWidth, FillColor, FillStyle, ForeColor, BackColor,
+// AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight).
bool basFormRtIsDrawMember(const char *name, bool isProp);
// Close all serial/secLink resources and unregister the idle pollers.
@@ -995,6 +999,46 @@ bool basFormRtDrawSetProp(BasFormRtT *rt, BasControlT *ctrl, const char *name, B
// conversions (VB's 15 twips a pixel), shared with Picture.Width / Height.
double basFormRtDrawPixelsToUnits(double pixels, int32_t scale, bool across);
+// A drawing surface with no canvas (the Printer's page, formPrinter.c):
+// the drawing state on surf, twipsX / twipsY twips in a pixel across and
+// down (circles come out round on paper with dots of any shape), the
+// ScaleMode's units over it. NULL when out of memory; free with
+// basFormRtDrawDelete. basFormRtDrawGfx is its state (CurrentX / Y,
+// colours, the scale) for the basGfx calls.
+void basFormRtDrawDelete(BasDrawT *d);
+BasGfxT *basFormRtDrawGfx(BasDrawT *d);
+BasDrawT *basFormRtDrawNew(AppContextT *ctx, const BasGfxSurfaceT *surf, double twipsX, double twipsY, int32_t scaleMode);
+
+// Its properties (CurrentX, CurrentY, DrawStyle, DrawWidth, FillColor,
+// FillStyle, ForeColor, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth,
+// ScaleHeight) and
+// Scale / ScaleX / ScaleY: true when name is one.
+bool basFormRtDrawScaleCall(BasFormRtT *rt, BasDrawT *d, const char *name, BasValueT *args, int32_t argc, BasValueT *result);
+bool basFormRtDrawStateGet(BasDrawT *d, const char *name, BasValueT *out);
+bool basFormRtDrawStateSet(BasFormRtT *rt, BasDrawT *d, const char *name, BasValueT value);
+
+// An object's Print item (BAS_OBJ_PRINT_* mode) laid out on d: columns,
+// zones, TAB and SPC in charW pixels, lines charH pixels; text is drawn by
+// emit with the top of its line at pixel (x, y) -- NUL-terminated at len --
+// which returns its width in pixels, and CurrentX moves past it.
+typedef int32_t (*BasDrawTextFnT)(void *ctx, const char *text, int32_t len, int32_t x, int32_t y);
+void basFormRtDrawPrintItem(BasDrawT *d, int32_t charW, int32_t charH, BasValueT value, int32_t mode, BasDrawTextFnT emit, void *ctx);
+
+// PaintPicture's argument counts (forms, PictureBoxes, and the Printer)
+#define DRAW_PAINT_MIN_ARGS 3 // PaintPicture picture, x, y
+#define DRAW_PAINT_SIZE_ARGS 5 // ... , width, height
+#define DRAW_PAINT_SRC_ARGS 9 // ... , srcX, srcY, srcWidth, srcHeight
+
+// A rectangle in g's scale as pixels (g NULL: the values are pixels).
+void basFormRtDrawToPixels(const BasGfxT *g, double x, double y, double w, double h, int32_t *px, int32_t *py, int32_t *pw, int32_t *ph);
+
+// PaintPicture's picture (a PictureBox, by name -- form's first -- or a
+// Control variable; an image file or a LoadPicture object) as R, G, B
+// bytes, w x h (malloc'd); *scale is a PictureBox's scale for a source
+// rectangle (NULL: pixels). NULL, a runtime error raised, when there is
+// none.
+uint8_t *basFormRtDrawPictureRgb(BasFormRtT *rt, BasFormT *form, BasValueT pic, int32_t *w, int32_t *h, const BasGfxT **scale);
+
// Cls on a form's or PictureBox's (or UserControl's) drawing: cleared to
// its BackColor.
void basFormRtDrawClear(BasFormRtT *rt, BasControlT *ctrl);
@@ -1020,6 +1064,24 @@ BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref);
void basFormRtScreenBind(BasFormRtT *rt);
void basFormRtScreenClose(BasFormRtT *rt);
+// ============================================================
+// Between formrt.c and formPrinter.c (VB's Printer object)
+// ============================================================
+
+// The object BASIC holds for Printer (basFormRtFindCtrl), and whether a
+// reference is it.
+bool basFormRtPrinterIs(const void *ref);
+void *basFormRtPrinterRef(void);
+
+// Its methods and properties (runtime errors for unknown ones).
+BasValueT basFormRtPrinterGetProp(BasFormRtT *rt, const char *name);
+BasValueT basFormRtPrinterMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc);
+void basFormRtPrinterSetProp(BasFormRtT *rt, const char *name, BasValueT value);
+
+// The program ends: a page drawn or a document open is printed (EndDoc),
+// and the Printer's state freed.
+void basFormRtPrinterEnd(BasFormRtT *rt);
+
// ============================================================
// Between formrt.c and formDesign.c
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/ide/idePackage.c b/src/apps/kpunch/dvxbasic/ide/idePackage.c
index ef600c13..ac079a0a 100644
--- a/src/apps/kpunch/dvxbasic/ide/idePackage.c
+++ b/src/apps/kpunch/dvxbasic/ide/idePackage.c
@@ -244,7 +244,13 @@ static void keysList(KeyListT *keys) {
snprintf(keys->items[0], sizeof(keys->items[0]), "%s", PKG_NOT_SIGNED);
- if (!platformUserPath(dir, sizeof(dir), PKG_KEYS_DIR) || (d = opendir(dir)) == NULL) {
+ if (!platformUserPath(dir, sizeof(dir), PKG_KEYS_DIR)) {
+ return;
+ }
+
+ d = opendir(dir);
+
+ if (!d) {
return;
}
diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
index e68c8bab..f1c1cc47 100644
--- a/src/apps/kpunch/dvxbasic/langref.dhs
+++ b/src/apps/kpunch/dvxbasic/langref.dhs
@@ -2278,6 +2278,23 @@ Do
Loop While INKEY$ = ""
.endcode
+.h2 Writing a Screen Saver
+
+A screen saver is a program with no form that shows its screen on the whole screen and draws until DVX closes it. Put the program (.app) in the SAVERS folder of your user folder (FileOpsKindDir$(FILEOPS_KIND_SAVERS, True)); it is then listed on Control Panel's Desktop page by its project Name. DVX starts it with the command line /S after the wait with no input, and closes its screen at the next key or mouse movement, which ends the program, so it needs no INKEY$ of its own.
+
+.code
+SCREEN 13
+CLS
+FULLSCREEN ON
+
+DO
+ ' draw a frame
+ FRAMEWAIT 30
+LOOP
+.endcode
+
+SysScreenSaver$, SysSetScreenSaver, SysScreenSaverStart, and SysIdleSeconds (system.bas) read and change the screen saver from a program.
+
.topic lang.fileio
.title File I/O
.toc 1 File I/O
@@ -4215,6 +4232,101 @@ If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText()
A control or form of your own named Clipboard takes the name's place.
+.topic lang.printerobj
+.title Printer Object
+.toc 1 Printer Object
+.index Printer Object
+.index Printer.Print
+.index Printer.Line
+.index Printer.Circle
+.index Printer.PSet
+.index Printer.PaintPicture
+.index Printer.NewPage
+.index Printer.EndDoc
+.index Printer.KillDoc
+.index Printer.Page
+.index Printer.FontSize
+.index Printer.Copies
+.index NewPage
+.index EndDoc
+.index KillDoc
+.index vbPRORPortrait
+.index vbPRORLandscape
+.index vbPRCMMonochrome
+.index vbPRCMColor
+
+.h1 Printer Object
+
+The Printer object draws pages on the printer chosen in the Control Panel, as in Visual Basic: text in any font at a size in points, lines, boxes, circles, dots, and pictures, anywhere on the page, page after page. It needs a printer model that prints pictures (the Control Panel's Printer page); on a color printer the colors print as they are, on the others in shades of grey.
+
+On a PostScript printer the page is drawn by the printer itself, as lines, curves, and letters rather than dots, so it prints as sharp as the printer can. Text is in the printer's own Helvetica, Times, or Courier, whichever is nearest FontName: Courier for a fixed-pitch font or a name with Mono or Courier in it, Times for a name with Serif, Times, Roman, Tinos, or Ledger in it (but not Sans), and Helvetica for the rest. TextWidth and TextHeight measure the printer's letters, so text lines up as it will print.
+
+A page is 8 by 10 inches. Its coordinates are twips (1440 an inch) until ScaleMode or Scale changes them, as on Visual Basic's Printer, so a program can place things in inches, centimetres, points, or its own units. Drawing goes to the page being made; NewPage prints it and starts the next, and EndDoc prints the last one and ends the document. The document starts when the first page is printed, at the printer and with the copies the program's last PrintChoose (print.bas) chose, or else as the Control Panel has them; while it prints, the Printing window shows, with Cancel. A program that ends with a page drawn or a document started has EndDoc done for it.
+
+.table
+ Method Description
+ ------ -----------
+ Printer.Print items Text at CurrentX / CurrentY in the font and ForeColor, as PRINT lays it out: ; joins, a comma goes to the next 14-character zone, TAB(n) and SPC(n) count characters of the font (its 0's width), and a new line goes down a line of the font. CurrentX / CurrentY move past it.
+ Printer.Line [(x1, y1)]-(x2, y2) [, color] [, B | BF] A line, a box, or a filled box, as on a PictureBox (STEP works too).
+ Printer.Circle (x, y), r [, color [, start, end [, aspect]]] A circle, ellipse, or arc; filled in FillColor when FillStyle is not transparent. Circles are round on paper on every printer.
+ Printer.PSet (x, y) [, color] One dot (DrawWidth across).
+ Printer.PaintPicture pic, x, y [, w, h [, srcX, srcY, srcW, srcH]] A picture (a PictureBox, a file, or LoadPicture's), stretched to w x h; without them, at its size on the screen (15 twips a pixel).
+ Printer.NewPage Print the page (even a blank one) and start the next; Page goes up by 1.
+ Printer.EndDoc Print the page drawn and end the document; Page goes back to 1.
+ Printer.KillDoc Throw the page drawn away and end the document (pages already printed stay printed).
+ Printer.TextWidth(text), TextHeight(text) The size text takes in the font, in the Printer's scale.
+ Printer.Scale (x1, y1)-(x2, y2), ScaleX, ScaleY As on a PictureBox: a scale of your own, and lengths from one scale to another.
+.endtable
+
+.table
+ Property Description
+ -------- -----------
+ CurrentX, CurrentY Where the next drawing starts.
+ ForeColor, FillColor, FillStyle, DrawWidth, DrawStyle As on a PictureBox (black, transparent fills, a solid pen 1 dot wide to start).
+ ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight The scale (vbTwips to start).
+ FontName, FontSize, FontBold, FontItalic, FontUnderline, FontStrikethru The font of Print's text; FontSize is in points (12 to start, in DejaVu Sans). Text prints crisp where the font has large sizes (the Printer Fonts add-on gives DejaVu Sans, Serif, and Sans Mono them), and blockier where a screen size is enlarged.
+ Copies How many times each page prints (1 to 99); as PrintChoose chose when it is not set.
+ Page The page being drawn (1 first). Read only.
+ Width, Height The page's size in twips (11520 by 14400). Read only.
+ TwipsPerPixelX, TwipsPerPixelY Twips in one of the printer's dots. Read only.
+ ColorMode vbPRCMColor on a color printer, else vbPRCMMonochrome. Read only.
+ Orientation vbPRORPortrait: pages print upright only.
+ DeviceName, Port The printer model and where it is connected, as the Control Panel names them. Read only.
+.endtable
+
+.code
+Sub mnuReport_Click ()
+ Printer.ScaleMode = vbInches
+ Printer.FontSize = 18
+ Printer.FontBold = True
+ Printer.CurrentX = 1
+ Printer.CurrentY = 1
+ Printer.Print "Sales by Region"
+ Printer.FontSize = 11
+ Printer.FontBold = False
+ Printer.Line (1, 1.5)-(7, 1.5)
+ Printer.FillStyle = 0
+ Printer.FillColor = vbBlue
+ Printer.Line (1, 2)-(1 + nNorth / 100, 2.4), vbBlue, B
+ Printer.CurrentX = 1: Printer.CurrentY = 2.5
+ Printer.Print "North"; Tab(20); nNorth
+ Printer.EndDoc
+End Sub
+.endcode
+
+The Printer has no Cls, Point, PAINT, or DRAW (Visual Basic's Printer has none of them either), and no Printer.Font object: use FontName, FontSize, and the other Font properties. A raster operation given to PaintPicture is not used on paper. A control or form of your own named Printer takes the name's place.
+
+.h2 Printer Constants
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ vbPRORPortrait 1 Printer.Orientation: upright pages (the only one)
+ vbPRORLandscape 2 Printer.Orientation: sideways pages (not supported)
+ vbPRCMMonochrome 1 Printer.ColorMode: black (and grey) only
+ vbPRCMColor 2 Printer.ColorMode: color
+.endtable
+
.topic lang.screenobj
.title Screen Object
.toc 1 Screen Object
@@ -4399,6 +4511,21 @@ IniWrite App.User & "\app.ini", "Display", "FontSize", Str$(fontSize)
.index vbHimetric
.index vbPicTypeNone
.index vbPicTypeBitmap
+.index vbFSSolid
+.index vbFSTransparent
+.index vbHorizontalLine
+.index vbVerticalLine
+.index vbUpwardDiagonal
+.index vbDownwardDiagonal
+.index vbCross
+.index vbDiagonalCross
+.index vbSolid
+.index vbDash
+.index vbDot
+.index vbDashDot
+.index vbDashDotDot
+.index vbInvisible
+.index vbInsideSolid
.index vbEmpty
.index vbNull
.index vbInteger
@@ -4670,6 +4797,30 @@ The scale units of ScaleMode and of the ScaleX / ScaleY methods, and the values
vbPicTypeBitmap 1 Picture.Type of a loaded picture
.endtable
+.h2 Fill and Pen Constants
+
+The values of FillStyle and DrawStyle on Forms, PictureBoxes, and the Printer (see Drawing on Forms and PictureBoxes).
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ vbFSSolid 0 FillStyle: filled solid in FillColor
+ vbFSTransparent 1 FillStyle: not filled (the default)
+ vbHorizontalLine 2 FillStyle: horizontal lines
+ vbVerticalLine 3 FillStyle: vertical lines
+ vbUpwardDiagonal 4 FillStyle: lines rising to the right
+ vbDownwardDiagonal 5 FillStyle: lines falling to the right
+ vbCross 6 FillStyle: a cross hatch
+ vbDiagonalCross 7 FillStyle: a diagonal cross hatch
+ vbSolid 0 DrawStyle: a solid pen (the default)
+ vbDash 1 DrawStyle: dashes
+ vbDot 2 DrawStyle: dots
+ vbDashDot 3 DrawStyle: dash, dot
+ vbDashDotDot 4 DrawStyle: dash, dot, dot
+ vbInvisible 5 DrawStyle: no lines or outlines (fills still drawn)
+ vbInsideSolid 6 DrawStyle: solid
+.endtable
+
.h2 VarType Constants
The values VARTYPE returns (see Variants). For an array, VARTYPE adds vbArray to the type of its elements.
@@ -4815,11 +4966,17 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index SoundSetPan
.index SoundSetPitch
.index SoundSetFrequency
+.index SoundPause
+.index SoundResume
.index SoundGetVolume
.index SoundCaps
.index SoundPlayEvent
.index SoundStreamOpen
+.index RecordStart
+.index RecordStop
+.index Recording
.index SoundStreamWrite
+.index SoundStreamFlush
.index SoundStreamClose
.index FmAlloc
.index FmWrite
@@ -4840,6 +4997,12 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index MusicFree
.index MusicError$
.index MusicMidiOpen
+.index MusicLoadMacro
+.index MusicSetBank
+.index MusicPatchSet$
+.index MusicRomBank$
+.index SoundFont
+.index PkgFileOwner$
.index MusicNoteOn
.index MusicNoteOff
.index MusicProgram
@@ -4850,6 +5013,19 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index MidiAcquire
.index MidiPut
.index MidiPortInfo$
+.index MidiInOpen
+.index MidiInMessage
+.index MidiInTime
+.index MidiInClock
+.index MusicMidiSend
+.index MidiRecordNew
+.index MidiRecordSave
+.index MIDI keyboard
+.index WtInfo$
+.index WtAlloc
+.index WtStart
+.index WtUpload
+.index Wavetable synthesizer
.index Music
.index MIDI
.index conn.bas
@@ -5099,8 +5275,8 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace)
joystick.bas Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
- sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, the FM synthesizer chip (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
- music.bas Music: play MOD, S3M, XM, IT, and MIDI files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$)
+ sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
+ music.bas Music: play MOD, S3M, XM, IT, MIDI, WAV, and VOC files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); instruments from banks and SoundFonts, a sound card's own (MusicSetBank, MusicPatchSet$, MusicRomBank$); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$); what a MIDI keyboard plays, heard as it comes and recorded into MIDI files (MidiInOpen, MidiInMessage, MusicMidiSend, MidiRecordNew, MidiRecordSave, ...)
fileops.bas Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving, and deleting whole folders at once or as a job with progress, Cancel, and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...)
system.bas What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, and the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...)
font.bas Fonts: the font families DVX has, the sizes each comes in, whether one is fixed-pitch, and how wide text and how tall a line is in a font (FontFamilyCount, FontFamilyName$, FontFamilyFixed, FontSizeCount, FontSizeAt, FontNearestSize, FontTextWidth, FontLineHeight, FontAscent, FontRescan)
@@ -5109,7 +5285,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...)
designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...)
- shell.bas DVX itself, everything Program Manager and Task Manager do: start programs and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
+ shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files and planning what installing them takes; installing and removing as a job with progress; trusting a publisher's key; and making packages, signing them, and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFile, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuild, PkgSign, PkgKeygen, ...)
json.bas JSON: read a document (a web service's reply, a settings file) and find values in it by path such as packages[3].version, and quote text for JSON of your own (JsonParse, JsonText$, JsonNumber, JsonCount, JsonKey$, JsonQuote$, ...)
http.bas Web requests: GET a page, a file, or a web service's answer, or POST to one, over the network a little at a time with progress, to memory or to a file (resuming one left part-way), following redirects (HttpOpen, HttpStart, HttpRunFor, HttpStatus, HttpBody$, HttpSaveTo, ...)
@@ -5276,7 +5452,7 @@ With comm.bas, SerOpen returns False when a port cannot be opened; SerLastError(
.h2 Printing
-With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header, and a footer with the document name, date, and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does). PrintText and PrintPicture print without asking first. Each returns True when it printed.
+With print.bas in the project, a program prints like the programs that come with DVX. PrintTextDialog shows the Print dialog (printer, copies, pages) and prints the text on pages laid out by the page setup: margins, a header, and a footer with the document name, date, and page number. PrintPageSetup shows the Page Setup dialog, which the user's choices are saved from for every program. PrintPictureDialog prints a picture file scaled to the page, on printer models that print pictures (PrintCanPrintPictures says whether the chosen one does), in color on a color printer (PrintCanPrintColor says whether it is one) and in shades of grey on the others. PrintText and PrintPicture print without asking first. Each returns True when it printed.
.code
Sub mnuPrint_Click ()
@@ -5288,7 +5464,7 @@ Sub mnuPageSetup_Click ()
End Sub
.endcode
-For printer output a line at a time, see LPRINT and OPEN "LPT1:".
+For printer output a line at a time, see LPRINT and OPEN "LPT1:". To draw whole pages (text in any font and size, lines, boxes, circles, and pictures), use the Printer object.
A program that prints its own way has more:
@@ -5335,8 +5511,13 @@ A program can also change the printer settings the way the Control Panel's Print
Function Description
-------- -----------
PrintModelCount() / PrintModelLabel$(model) How many printer models there are, and the name of each.
- PrintDestCount() / PrintDestLabel$(dest) How many places the printer can be connected to (LPT1, LPT2, LPT3, a file), and the name of each.
+ PrintDestCount() / PrintDestLabel$(dest) How many places the printer can be connected to (LPT1 to LPT3, COM1 to COM4, a file), and the name of each.
PrintDestIsFile(dest) True for the destination that prints into a file.
+ PrintDestIsSerial(dest) True for a serial port (COM1 to COM4), which needs a speed and a handshake.
+ PrintBaudRate(index) The serial speeds offered, from index 0 (0 past the last).
+ PrintHandshakeCount() / PrintHandshakeLabel$(handshake) How many handshakes there are, and the name of each (None, XON / XOFF, Hardware (CTS), Hardware (DSR, Apple)).
+ PrintSettingBaud(prefs), PrintSettingHandshake(prefs) A serial printer's speed and handshake in the settings.
+ PrintSerialStore(prefs, baud, handshake) Put a serial printer's speed and handshake into the settings; False for a speed not offered.
PrintSettingModel(prefs) The printer model in the settings.
PrintSettingDest(prefs) Where the printer is connected.
PrintSettingFile$(prefs) The file printed into when the destination is a file.
@@ -5374,7 +5555,7 @@ Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), Fil
Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb, and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when nothing can be saved: DVX runs from such a disk and there is no TEMP folder to keep the user folder in. FileOpsTempDir$() is the folder for scratch files ("" when there is none).
-DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, and MIDI instrument banks come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, and FILEOPS_KIND_BANKS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it.
+DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, FILEOPS_KIND_BANKS, and FILEOPS_KIND_SAVERS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it.
FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder at once. For a long copy, move, or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
@@ -5409,7 +5590,7 @@ End Sub
With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC.
-Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), or opens a document in the program set for its type; it returns True when it started. ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (0 when it did not start; DVX has already said why). ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
+Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$.
@@ -5491,7 +5672,7 @@ End Sub
With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.
-What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it.
+What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\BANKS\KPUNCH\DMXOPL.OP2"; "" when none did).
Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is.
@@ -5572,6 +5753,8 @@ Colors: the system colors are numbered from 0 to SysColorCount() - 1. SysColorLa
Wallpaper: SysSetWallpaper(path) shows a BMP, JPG, PNG, or GIF image behind the desktop ("" for none) and returns False when the image could not be loaded (the old one stays). SysSetWallpaperMode mode says how it fills the screen, a choice of PREFS_CHOICE_WALLPAPER (Stretch, Tile, or Center; see Settings Files). SysWallpaper$() and SysWallpaperMode() tell what is in use.
+Screen saver: SysSetScreenSaver fileName, minutes chooses the screen saver -- a program in the SAVERS folders, named as FileOpsKindList(FILEOPS_KIND_SAVERS) lists it, or "" for none -- and the minutes with no key or mouse input before it starts (SCREEN_SAVER_WAIT_MIN to SCREEN_SAVER_WAIT_MAX; DVX's default is SCREEN_SAVER_WAIT_DEFAULT). SysScreenSaver$() and SysScreenSaverWait() tell what is in use, SysScreenSaverStart() starts it now as Control Panel's Test does (False when none is chosen or one is running), and SysScreenSaverRunning() is True while one runs. SysIdleSeconds() is how long it has been since the last key or mouse input anywhere on the screen.
+
Text: SysSetTextSmoothing True smooths the edges of text and SysSetTextSmoothing False stops it; SysTextSmoothing() tells which the user chose. Text is smoothed only when the screen shows more than 256 colors, and only in the fonts and sizes that come smoothed. Every window is drawn again when it changes.
Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower), and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys, and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).
@@ -5582,7 +5765,7 @@ Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb()
System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own.
-Saving: SysColorsStore, SysWallpaperStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.
+Saving: SysColorsStore, SysWallpaperStore, SysScreenSaverStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, the screen saver, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysScreenSaverLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.
.code
Dim snap As Long
@@ -5758,9 +5941,9 @@ End Sub
WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas.
-PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), and "running" are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", and "channels" are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
+PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
-A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. A program's sounds are freed when it ends.
+A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.
.code
Dim Shared laser As Long
@@ -5776,7 +5959,7 @@ Sub Fire (x As Integer)
End Sub
.endcode
-A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan, and SoundSetPitch, and SoundStreamClose stream stops it. A Timer keeps a stream fed:
+A program can also make its sound as it goes, for speech or music of its own. SoundStreamOpen(rate, bits, channels, bufferMs) opens a stream that starts playing at once (silence until data arrives) and holds bufferMs milliseconds of sound (0 for the usual SOUND_STREAM_MS_DEFAULT; SOUND_STREAM_MS_MIN to SOUND_STREAM_MS_MAX). SoundStreamWrite(stream, data) adds raw sound data in the stream's format and returns how many bytes it took; keep the rest and write it again later. SoundStreamSpace(stream) is the room left and SoundStreamQueued(stream) what is waiting to play, both in samples (a stereo sample counts once). SoundStreamVoice(stream) is the stream's voice, for SoundSetVolume, SoundSetPan, SoundSetPitch, SoundPause, and SoundResume. SoundStreamFlush stream drops what is waiting to play, so what is written next is heard at once (to jump to another place in what you play), and SoundStreamClose stream stops it. A Timer keeps a stream fed:
.code
Sub Timer1_Timer ()
@@ -5787,9 +5970,20 @@ Sub Timer1_Timer ()
End Sub
.endcode
+A program can record sound from the sound card's microphone, line, or CD input into a WAV file. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.
+
+.code
+If RecordStart("NOTE.WAV", 0, 0, 0, RECORD_MIC) Then
+ ' ... a Timer shows RecordLength() and RecordLevel()
+ ok = RecordStop()
+End If
+.endcode
+
On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).
-sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, volume stores them (a new driver or MIDI choice takes effect when DVX next starts).
+On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory" and "freememory" (bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100). WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. Everything a program took is given back when it ends.
+
+sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play, SoundMidiInCount() and SoundMidiInLabel$(index) where MIDI input can come from, and SoundModulesCount() and SoundModulesLabel$(index) where music modules can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), SoundSettingModules(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, modules choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, modules, volume stores them (a new driver or MIDI choice takes effect when DVX next starts, a modules choice with the next music); SoundSettingMidiIn(handle) and SoundSettingMidiInStore handle, midiIn read and store the MIDI input choice (it takes effect when DVX next starts).
.code
Dim ini As Long
@@ -5806,10 +6000,18 @@ Next i
.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.
+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 (on a Gravis UltraSound's or Sound Blaster AWE's own voices, which takes the computer little time), 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, and WAV and VOC sound files wherever the computer can play sampled sound (a long WAV recording is read from the disk as it plays, so it needs no memory). 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.
+MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY takes, with its notes on the General MIDI instruments its I commands name (the piano before any I): the tune can then be played in the background, paused, stopped, and made quieter like any music. MIDI music plays on the instruments chosen in Control Panel; MusicSetBank music, bank plays one music on another instrument bank, named as in the banks folders ("" is DVX's standard bank), before the music first plays (MusicError$ says when the bank is missing or damaged). A SoundFont (.SF2 or .SBK) gives the recorded instruments for a wavetable card or the built-in SoundFont synthesizer, as can a Gravis UltraSound's own patches: MusicPatchSet$() names them (their ULTRASND.INI, "" when there are none), and MusicRomBank$() names the instruments built into a Sound Blaster AWE card ("" when Creative's software is not installed).
+
+.code
+tune = MusicLoadMacro("T150 I41 O4 L8 CEG>C4")
+MusicSetBank tune, "KPUNCH\DMXOPL.OP2"
+MusicPlay tune, False
+.endcode
+
.code
Dim Shared tune As Long
@@ -5845,9 +6047,50 @@ Sub tmrNoteOff_Timer ()
End Sub
.endcode
-MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title), "output" (where it plays, for people), "kind" ("module", "midi", or "live"), "where" ("stream", "mpu401", "fm", or "software"; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).
+MusicGetVolume(music) gives a music's volume in percent. MusicInfo$(music, field) tells about it, by name in any case: "title" (a module's own title, or the name a MIDI file gives itself; "" for a sound file), "output" (where it plays, for people), "kind" ("module", "midi", "live", or "sound"), "where" ("stream", "mpu401", "fm", "software", "wave", or "softwave"; "wave" is a wavetable card's voices, for a module too; "" before it first plays), and "paused" and "looping" (True or False as text; VAL gives the value).
-A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present", and "owned" (a program has it). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again), and MidiRelease gives the port back.
+A program that sends MIDI itself (system exclusive messages, a sequencer of its own) can have the MIDI port, one program at a time, when the settings send MIDI music to the MIDI port. MidiPortInfo$(field) describes it: "description" ("" when there is none), "present", "owned" (a program has it), "input" (it brings MIDI in), and "inputowned" (a program reads what comes in). MidiAcquire() takes the port (False when another program has it or there is none), MidiPut(byteValue) sends one byte (False when the port could not take it yet: send it again), and MidiRelease gives the port back.
+
+When the settings take MIDI input from the MIDI port (Control Panel, Sounds, MIDI in), a program can read what a MIDI keyboard or another instrument plays. MidiInOpen() starts reading (False when there is no MIDI input or another program reads it; SoundError$() says which) and MidiInClose stops; a program's reading stops when it ends. MidiInMessage() gives the next message that came, or MIDI_NONE (-1) when none waits. A message is packed into one number: the status is midiMsg AND 255 (its kind, midiMsg AND MIDI_KIND_MASK, is MIDI_NOTE_ON, MIDI_NOTE_OFF, MIDI_CONTROL, MIDI_PROGRAM, MIDI_PITCH_BEND, MIDI_KEY_PRESSURE, or MIDI_CHANNEL_PRESSURE, and its channel, numbered as MusicNoteOn numbers them, is (midiMsg AND MIDI_CHANNEL_MASK) + 1), the first data byte (the note) is (midiMsg \ 256) AND 255, and the second (the velocity) (midiMsg \ 65536) AND 255. A note on with a velocity of 0 is a note off too. MidiInTime() is when the message MidiInMessage gave last came and MidiInClock() the time now, both in milliseconds since MidiInOpen; MidiInLost() counts bytes lost because the program read too late. MusicMidiSend music, midiMsg plays a message as it came on a live music (MIDI thru), so what is played is heard.
+
+A MIDI recording keeps messages with their times and saves them as a MIDI file that MusicLoad plays. MidiRecordNew() returns a recording number (0 when there is no room; MusicError$() says why), MidiRecordAdd recording, milliseconds, midiMsg adds a message (MidiInTime() is the time to give, when the recording started with MidiInOpen), MidiRecordCount(recording) and MidiRecordLength(recording) tell how many messages it has and when the last came, MidiRecordSave(recording, fileName) writes the file (False when it could not be written; MusicError$() says why; a note still held at the end is let go), and MidiRecordFree recording frees it. A program's recordings are freed when it ends. The Sound Recorder records a MIDI keyboard this way:
+
+.code
+Dim Shared take As Long
+Dim Shared keys As Long
+
+Sub cmdRecord_Click ()
+ If MidiInOpen() Then
+ take = MidiRecordNew()
+ keys = MusicMidiOpen()
+ tmrTake.Enabled = True
+ End If
+End Sub
+
+Sub tmrTake_Timer ()
+ Dim midiMsg As Long
+
+ midiMsg = MidiInMessage()
+
+ Do While midiMsg <> MIDI_NONE
+ MusicMidiSend keys, midiMsg
+ MidiRecordAdd take, MidiInTime(), midiMsg
+ midiMsg = MidiInMessage()
+ Loop
+End Sub
+
+Sub cmdStop_Click ()
+ tmrTake.Enabled = False
+ MidiInClose
+ MusicFree keys
+
+ If Not MidiRecordSave(take, "SONG.MID") Then
+ Print "Not saved: " & MusicError$()
+ End If
+
+ MidiRecordFree take
+End Sub
+.endcode
.h2 Network Connections
diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.c b/src/apps/kpunch/dvxbasic/runtime/gfx.c
index 5d2376db..9bd60a35 100644
--- a/src/apps/kpunch/dvxbasic/runtime/gfx.c
+++ b/src/apps/kpunch/dvxbasic/runtime/gfx.c
@@ -43,6 +43,7 @@
#define GFX_STYLE_SOLID 0xFFFF // LINE style: every pixel
#define GFX_STYLE_TOP_BIT 0x8000
#define GFX_STYLE_BITS 16
+#define GFX_STYLE_NONE 0x0000 // LINE style: no pixel
#define GFX_COORD_LIMIT 1.0e9 // pixel coordinates are clamped to this
#define GFX_RADIUS_MAX 32767.0 // largest CIRCLE radius in pixels (QBasic's integer range)
#define GFX_ROUND_HALF 0.5
@@ -71,6 +72,33 @@ typedef struct {
uint8_t *done; // tile fill: one bit per view pixel
} GfxFloodT;
+// An ellipse outline (CIRCLE): its centre and radii in pixels, the arc
+// (radians, counterclockwise as the screen shows it) when arc, and the
+// pen's dashes along it.
+typedef struct {
+ int32_t cx;
+ int32_t cy;
+ int32_t rx;
+ int32_t ry;
+ uint32_t pixel;
+ bool arc;
+ double start;
+ double end;
+ uint16_t dash;
+} GfxEllipseT;
+
+// The LINE style of each DrawStyle: dash, dot, dash-dot, and
+// dash-dot-dot as runs along the pen's path; invisible draws nothing
+static const uint16_t sDrawDashes[BAS_GFX_DRAW_MAX + 1] = {
+ [BAS_GFX_DRAW_SOLID] = GFX_STYLE_SOLID,
+ [BAS_GFX_DRAW_DASH] = 0xFFF0,
+ [BAS_GFX_DRAW_DOT] = 0xCCCC,
+ [BAS_GFX_DRAW_DASH_DOT] = 0xFF18,
+ [BAS_GFX_DRAW_DASH_DOT_DOT] = 0xFCCC,
+ [BAS_GFX_DRAW_INVISIBLE] = GFX_STYLE_NONE,
+ [BAS_GFX_DRAW_INSIDE_SOLID] = GFX_STYLE_SOLID
+};
+
// Unit steps of DRAW's U D L R E F G H (screen y grows downwards).
typedef struct {
char letter;
@@ -102,9 +130,9 @@ static void boxOutline(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32
static bool clipLine(const BasGfxT *g, double *x0, double *y0, double *x1, double *y1);
static int32_t drawMacro(BasGfxT *g, const char *macro, int32_t len, double *px, double *py, int32_t depth);
static int32_t drawNumber(const char *s, int32_t len, int32_t *pos, bool allowSign, int32_t *out);
-static void ellipse(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end);
+static void ellipse(BasGfxT *g, const GfxEllipseT *e);
static void ellipseFill(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel);
-static void ellipsePoint(BasGfxT *g, int32_t cx, int32_t cy, int32_t dx, int32_t dy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end);
+static void ellipsePoint(BasGfxT *g, const GfxEllipseT *e, int32_t dx, int32_t dy);
static bool fillable(const GfxFloodT *f, int32_t x, int32_t y);
static void fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, int32_t style);
static void fillSpan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel, int32_t style);
@@ -118,6 +146,8 @@ static double mapX(const BasGfxT *g, double x);
static double mapY(const BasGfxT *g, double y);
static int32_t originX(const BasGfxT *g);
static int32_t originY(const BasGfxT *g);
+static uint16_t penDashes(const BasGfxT *g);
+static int32_t penSize(const BasGfxT *g);
static void plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel);
static void plotPixel(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel);
static void pointArg(const BasGfxT *g, BasValueT x, BasValueT y, bool step, double *outX, double *outY);
@@ -175,30 +205,36 @@ int32_t basGfxCircle(BasGfxT *g, const BasValueT *args) {
return BAS_ERR_OVERFLOW;
}
- int32_t cx = basGfxRound(mapX(g, x));
- int32_t cy = basGfxRound(mapY(g, y));
- int32_t irx = basGfxRound(rx);
- int32_t iry = basGfxRound(ry);
- bool arc = (flags & (BAS_GFX_ARC_START | BAS_GFX_ARC_END)) != 0;
- uint32_t pixel = basGfxPixel(g, color);
+ GfxEllipseT e;
+
+ e.cx = basGfxRound(mapX(g, x));
+ e.cy = basGfxRound(mapY(g, y));
+ e.rx = basGfxRound(rx);
+ e.ry = basGfxRound(ry);
+ e.pixel = basGfxPixel(g, color);
+ e.arc = (flags & (BAS_GFX_ARC_START | BAS_GFX_ARC_END)) != 0;
+ e.start = fabs(start);
+ e.end = fabs(end);
+ e.dash = penDashes(g);
g->lastX = x;
g->lastY = y;
- if (!arc && shapeFilled(g)) {
- ellipseFill(g, cx, cy, irx, iry, basGfxPixel(g, g->fillColor));
+ if (!e.arc && shapeFilled(g)) {
+ ellipseFill(g, e.cx, e.cy, e.rx, e.ry, basGfxPixel(g, g->fillColor));
}
- ellipse(g, cx, cy, irx, iry, pixel, arc, fabs(start), fabs(end));
+ ellipse(g, &e);
// A negative angle also draws the radius to that end of the arc.
double ends[] = { start, end };
for (int32_t i = 0; i < (int32_t)(sizeof(ends) / sizeof(ends[0])); i++) {
if (ends[i] < 0.0) {
- double a = -ends[i];
+ double a = -ends[i];
+ uint16_t style = e.dash;
- linePixels(g, cx, cy, cx + irx * cos(a), cy - iry * sin(a), pixel, NULL);
+ linePixels(g, e.cx, e.cy, e.cx + e.rx * cos(a), e.cy - e.ry * sin(a), e.pixel, style == GFX_STYLE_SOLID ? NULL : &style);
}
}
@@ -316,7 +352,7 @@ int32_t basGfxLine(BasGfxT *g, const BasValueT *args) {
double x2 = 0.0;
double y2 = 0.0;
uint32_t color = g->fore;
- uint16_t style = GFX_STYLE_SOLID;
+ uint16_t style = penDashes(g);
if (flags & BAS_GFX_START) {
pointArg(g, args[0], args[1], (flags & BAS_GFX_STEP) != 0, &x1, &y1);
@@ -358,11 +394,11 @@ int32_t basGfxLine(BasGfxT *g, const BasValueT *args) {
fillRect(g, px1, py1, px2, py2, basGfxPixel(g, g->fillColor), g->fillStyle);
}
- boxOutline(g, px1, py1, px2, py2, pixel, (flags & BAS_GFX_STYLE) ? &style : NULL);
+ boxOutline(g, px1, py1, px2, py2, pixel, style == GFX_STYLE_SOLID ? NULL : &style);
return 0;
}
- linePixels(g, mapX(g, x1), mapY(g, y1), mapX(g, x2), mapY(g, y2), pixel, (flags & BAS_GFX_STYLE) ? &style : NULL);
+ linePixels(g, mapX(g, x1), mapY(g, y1), mapX(g, x2), mapY(g, y2), pixel, style == GFX_STYLE_SOLID ? NULL : &style);
return 0;
}
@@ -1025,23 +1061,35 @@ static int32_t drawNumber(const char *s, int32_t len, int32_t *pos, bool allowSi
// Ellipse outline by the midpoint algorithm (rx, ry in pixels), each point
-// once per quadrant; an arc keeps only the points between start and end.
-static void ellipse(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end) {
- if (rx == 0 || ry == 0) {
- linePixels(g, cx - rx, cy - ry, cx + rx, cy + ry, pixel, NULL);
+// once per quadrant; an arc keeps only the points between start and end,
+// and dashes the points their bits leave out.
+static void ellipse(BasGfxT *g, const GfxEllipseT *e) {
+ if (e->dash == GFX_STYLE_NONE) {
return;
}
- double rx2 = (double)rx * rx;
- double ry2 = (double)ry * ry;
+ if (g->surf.vec && e->rx > 0 && e->ry > 0) {
+ g->surf.vec->ellipse(g->surf.ctx, e->cx, e->cy, e->rx, e->ry, e->pixel, penSize(g), e->dash, e->arc, e->start, e->end);
+ return;
+ }
+
+ if (e->rx == 0 || e->ry == 0) {
+ uint16_t style = e->dash;
+
+ linePixels(g, e->cx - e->rx, e->cy - e->ry, e->cx + e->rx, e->cy + e->ry, e->pixel, style == GFX_STYLE_SOLID ? NULL : &style);
+ return;
+ }
+
+ double rx2 = (double)e->rx * e->rx;
+ double ry2 = (double)e->ry * e->ry;
int32_t x = 0;
- int32_t y = ry;
+ int32_t y = e->ry;
double dx = 0.0;
double dy = 2.0 * rx2 * y;
- double d1 = ry2 - rx2 * ry + 0.25 * rx2;
+ double d1 = ry2 - rx2 * e->ry + 0.25 * rx2;
while (dx < dy) {
- ellipsePoint(g, cx, cy, x, y, rx, ry, pixel, arc, start, end);
+ ellipsePoint(g, e, x, y);
x++;
dx += 2.0 * ry2;
@@ -1057,7 +1105,7 @@ static void ellipse(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry,
double d2 = ry2 * (x + GFX_ROUND_HALF) * (x + GFX_ROUND_HALF) + rx2 * (y - 1.0) * (y - 1.0) - rx2 * ry2;
while (y >= 0) {
- ellipsePoint(g, cx, cy, x, y, rx, ry, pixel, arc, start, end);
+ ellipsePoint(g, e, x, y);
y--;
dy -= 2.0 * rx2;
@@ -1074,6 +1122,11 @@ static void ellipse(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry,
// A filled ellipse: one span per row.
static void ellipseFill(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel) {
+ if (g->surf.vec) {
+ g->surf.vec->ellipseFill(g->surf.ctx, cx, cy, rx, ry, pixel, g->fillStyle);
+ return;
+ }
+
for (int32_t dy = -ry; dy <= ry; dy++) {
double f = ry > 0 ? 1.0 - ((double)dy * dy) / ((double)ry * ry) : 1.0;
int32_t half = basGfxRound(rx * sqrt(f > 0.0 ? f : 0.0));
@@ -1083,8 +1136,10 @@ static void ellipseFill(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t
}
-// The four mirror points of an ellipse point (dx, dy >= 0).
-static void ellipsePoint(BasGfxT *g, int32_t cx, int32_t cy, int32_t dx, int32_t dy, int32_t rx, int32_t ry, uint32_t pixel, bool arc, double start, double end) {
+// The four mirror points of an ellipse point (dx, dy >= 0): those on the
+// arc, and on a dash (the pixels along the outline from angle 0, about
+// the angle times the mean radius).
+static void ellipsePoint(BasGfxT *g, const GfxEllipseT *e, int32_t dx, int32_t dy) {
static const int32_t signs[][2] = { { 1, 1 }, { -1, 1 }, { -1, -1 }, { 1, -1 } };
for (int32_t i = 0; i < (int32_t)(sizeof(signs) / sizeof(signs[0])); i++) {
@@ -1096,19 +1151,25 @@ static void ellipsePoint(BasGfxT *g, int32_t cx, int32_t cy, int32_t dx, int32_t
continue;
}
- if (arc) {
- double a = atan2((double)-sy / ry, (double)sx / rx);
+ if (e->arc || e->dash != GFX_STYLE_SOLID) {
+ double a = atan2((double)-sy / e->ry, (double)sx / e->rx);
if (a < 0.0) {
a += GFX_TWO_PI;
}
- if (!arcContains(a, start, end)) {
+ if (e->arc && !arcContains(a, e->start, e->end)) {
+ continue;
+ }
+
+ int32_t step = (int32_t)(a * (e->rx + e->ry) / 2.0) % GFX_STYLE_BITS;
+
+ if (!(e->dash & (GFX_STYLE_TOP_BIT >> step))) {
continue;
}
}
- plot(g, cx + sx, cy + sy, pixel);
+ plot(g, e->cx + sx, e->cy + sy, e->pixel);
}
}
@@ -1136,6 +1197,11 @@ static void fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1,
int32_t top = y0 < y1 ? y0 : y1;
int32_t bottom = y0 < y1 ? y1 : y0;
+ if (g->surf.vec) {
+ g->surf.vec->rect(g->surf.ctx, x0, top, x1, bottom, pixel, style);
+ return;
+ }
+
top = top < g->viewY0 ? g->viewY0 : top;
bottom = bottom > g->viewY1 ? g->viewY1 : bottom;
@@ -1325,6 +1391,21 @@ static void linePixels(BasGfxT *g, double x0, double y0, double x1, double y1, u
return;
}
+ if (g->surf.vec) {
+ g->surf.vec->line(g->surf.ctx, x0, y0, x1, y1, pixel, penSize(g), style ? *style : GFX_STYLE_SOLID);
+
+ // The style runs on past the line's pixels, as when it is plotted
+ if (style) {
+ int32_t ax = abs(basGfxRound(x1) - basGfxRound(x0));
+ int32_t ay = abs(basGfxRound(y1) - basGfxRound(y0));
+ int32_t steps = ((ax > ay ? ax : ay) + 1) % GFX_STYLE_BITS;
+
+ *style = (uint16_t)((*style << steps) | (*style >> ((GFX_STYLE_BITS - steps) % GFX_STYLE_BITS)));
+ }
+
+ return;
+ }
+
int32_t ax = basGfxRound(x0);
int32_t ay = basGfxRound(y0);
int32_t bx = basGfxRound(x1);
@@ -1396,10 +1477,31 @@ static int32_t originY(const BasGfxT *g) {
}
+// The pen's dashes as a LINE style: DrawStyle's, dashed only with a pen
+// one pixel wide (as VB draws them).
+static uint16_t penDashes(const BasGfxT *g) {
+ uint16_t dash = sDrawDashes[g->drawStyle];
+
+ return (g->drawWidth > 1 && dash != GFX_STYLE_NONE) ? GFX_STYLE_SOLID : dash;
+}
+
+
+// The pen's size across in pixels: a disc DrawWidth / 2 pixels round
+// its centre (one pixel for DrawWidth 0 and 1).
+static int32_t penSize(const BasGfxT *g) {
+ return 2 * (g->drawWidth / 2) + 1;
+}
+
+
// One point with the pen: a pixel, or a disc DrawWidth across.
static void plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel) {
int32_t r = g->drawWidth / 2;
+ if (g->surf.vec) {
+ g->surf.vec->dot(g->surf.ctx, x, y, pixel, penSize(g));
+ return;
+ }
+
if (r <= 0) {
plotPixel(g, x, y, pixel);
return;
diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.h b/src/apps/kpunch/dvxbasic/runtime/gfx.h
index f69dc05b..bbcfdd3a 100644
--- a/src/apps/kpunch/dvxbasic/runtime/gfx.h
+++ b/src/apps/kpunch/dvxbasic/runtime/gfx.h
@@ -62,6 +62,17 @@
#define BAS_GFX_FILL_DIAGONAL_CROSS 7
#define BAS_GFX_FILL_MAX 7
+// DrawStyle values (VB): the pen solid, dashed four ways (with a pen one
+// pixel wide; a wider one draws solid), drawing no lines at all, or solid.
+#define BAS_GFX_DRAW_SOLID 0
+#define BAS_GFX_DRAW_DASH 1
+#define BAS_GFX_DRAW_DOT 2
+#define BAS_GFX_DRAW_DASH_DOT 3
+#define BAS_GFX_DRAW_DASH_DOT_DOT 4
+#define BAS_GFX_DRAW_INVISIBLE 5
+#define BAS_GFX_DRAW_INSIDE_SOLID 6
+#define BAS_GFX_DRAW_MAX 6
+
// PAINT tiles: up to 8 pixels wide and 64 rows (QBasic's limits).
#define BAS_GFX_TILE_MAX_W 8
#define BAS_GFX_TILE_MAX_H 64
@@ -69,8 +80,26 @@
// DRAW's S (scale) is in quarter pixels; S4 is one pixel a unit.
#define BAS_GFX_DRAW_SCALE_ONE 4
+// A surface that takes shapes whole instead of spans (a PostScript
+// page): lines (from x0, y0 to x1, y1, clipped to the view grown by the
+// pen; style is LINE's 16-pixel mask as it stands at the line's start,
+// 0xFFFF solid), filled boxes (corners inclusive) and ellipses, ellipse
+// outlines (an arc from start to end radians, counterclockwise as the
+// screen shows it, when arc; style's bits a pixel each along the outline
+// from angle 0), and pen dots, in pixel coordinates (the others not
+// clipped); width is the pen's size across in pixels, and fillStyle
+// BAS_GFX_FILL_SOLID or a hatch.
+typedef struct {
+ void (*line)(void *ctx, double x0, double y0, double x1, double y1, uint32_t pixel, int32_t width, uint16_t style);
+ void (*rect)(void *ctx, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, int32_t fillStyle);
+ void (*ellipse)(void *ctx, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, int32_t width, uint16_t style, bool arc, double start, double end);
+ void (*ellipseFill)(void *ctx, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel, int32_t fillStyle);
+ void (*dot)(void *ctx, int32_t x, int32_t y, uint32_t pixel, int32_t width);
+} BasGfxVectorT;
+
// A drawing surface. span and get take pixel coordinates the caller has
-// already clipped to the surface.
+// already clipped to the surface. A surface with vec takes the shapes
+// whole through it instead (span is then not used for them).
typedef struct {
void *ctx;
int32_t width;
@@ -80,6 +109,7 @@ typedef struct {
uint32_t (*get)(const void *ctx, int32_t x, int32_t y);
uint32_t (*toPixel)(void *ctx, uint32_t color); // NULL: colours are pixels
uint32_t (*toColor)(const void *ctx, uint32_t pixel); // NULL: pixels are colours
+ const BasGfxVectorT *vec; // NULL: shapes are spans
} BasGfxSurfaceT;
// A PAINT tile: colour numbers, repeated over the surface from its
@@ -111,6 +141,7 @@ typedef struct {
uint32_t fillColor; // VB FillColor / FillStyle for boxes and circles
int32_t fillStyle;
int32_t drawWidth; // pen size in pixels (VB DrawWidth)
+ int32_t drawStyle; // the pen's dashes (VB DrawStyle: BAS_GFX_DRAW_*)
double aspect; // CIRCLE aspect when none is given
int32_t drawScale; // DRAW S (quarter pixels a unit)
int32_t drawTurn; // DRAW A / TA (degrees)
diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c
index 63275191..979cb7d9 100644
--- a/src/apps/kpunch/dvxbasic/stub/basstub.c
+++ b/src/apps/kpunch/dvxbasic/stub/basstub.c
@@ -80,11 +80,11 @@ static void stubPrint(void *ctx, const char *text, bool newline);
int32_t appMain(DxeAppContextT *ctx);
-// Resolved by the shell as _appShutdown and called on BOTH graceful reap and
-// force-kill, before this app's DXE is unmapped. On a normal exit appMain
-// already ran basFormRtDestroy; on a force-kill it never returns, so this is
-// the only path that drops the serial/secLink idle pollers from the shell
-// registry -- otherwise they would keep polling this app's freed terminals.
+// Resolved by the shell as _appShutdown and called when the app is reaped
+// after appMain returned (which already ran basFormRtDestroy, so this
+// finds the serial ports closed). A force kill runs none of the app's
+// code: basrt's kill hook (basRunHostAppKilled) ends the runtime then,
+// serial ports and idle pollers included, while its windows are there.
void appShutdown(void) {
basFormRtSerialShutdown();
}
@@ -212,6 +212,7 @@ int32_t appMain(DxeAppContextT *ctx) {
spec.printFn = stubPrint;
spec.inputFn = stubInput;
spec.doEventsFn = stubDoEvents;
+ spec.standalone = true;
BasFormRtT *rt = basRunHostCreate(sAc, mod, &spec, &hostError);
@@ -227,7 +228,6 @@ int32_t appMain(DxeAppContextT *ctx) {
if (!rt->vm->module) {
dvxMessageBox(sAc, "Error", basVmGetError(rt->vm), 0);
basRunHostDestroy(rt);
- basModuleFree(mod);
dvxResClose(res);
return 1;
}
@@ -309,7 +309,6 @@ int32_t appMain(DxeAppContextT *ctx) {
ctx->newArgsCtx = NULL;
sRt = NULL;
basRunHostDestroy(rt);
- basModuleFree(mod);
return 0;
}
diff --git a/src/apps/kpunch/dvxbasic/userctl.dhs b/src/apps/kpunch/dvxbasic/userctl.dhs
index 0d3b6563..11619df9 100644
--- a/src/apps/kpunch/dvxbasic/userctl.dhs
+++ b/src/apps/kpunch/dvxbasic/userctl.dhs
@@ -132,7 +132,7 @@ Inside the control's code, UserControl is the control itself: the surface it dra
PropertyChanged name Tells a designer that a property changed, so its property sheet shows the new value.
Cls, Line, PSet, Circle, Print, Point, PaintPicture, DrawFocusRect, TextWidth, TextHeight, Scale, ScaleX, ScaleY Drawing, as on a PictureBox.
DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar, DrawScrollArrow, ScrollBarClick, ScrollBarsNeeded, PrintAccel, TextWidthAccel, AccelKey Drawing in DVX's own look (bevels, button faces, arrows, upright and horizontal scroll bars and their arrow buttons, captions with an underlined access key), as on a PictureBox.
- CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, FontScale, DrawWidth, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox.
+ CurrentX, CurrentY, ForeColor, BackColor, FillColor, FillStyle, FontScale, DrawWidth, DrawStyle, AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight Drawing properties, as on a PictureBox.
.endtable
The drawing members work exactly as on a PictureBox (see Drawing on Forms and PictureBoxes). Write them with UserControl in front -- UserControl.Line, UserControl.Print -- as a form's code writes Me.Line: a drawing statement on its own (Line, Cls, Print) draws on the program's screen, not on the control.
diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.c b/src/apps/kpunch/dvxdemo/dvxdemo.c
index 08f5cac0..724f8ea6 100644
--- a/src/apps/kpunch/dvxdemo/dvxdemo.c
+++ b/src/apps/kpunch/dvxdemo/dvxdemo.c
@@ -1033,7 +1033,8 @@ static void setupControlsWindow(void) {
wgtSetFont(serif, "DejaVu Serif", 16, 0);
wgtSetFont(mono, "Cousine", 14, FONT_STYLE_BOLD);
wgtSetFont(script, "Pacifico", 24, 0);
- rich->prefW = wgtPixels(DEMO_RICH_W);
+ rich->minW = wgtPixels(DEMO_RICH_W);
+ rich->maxW = wgtPixels(DEMO_RICH_W);
choose->onClick = onChooseFont;
wgtSetName(sample, DEMO_FONT_SAMPLE);
diff --git a/src/apps/kpunch/fileMgr/fileMgr.dhs b/src/apps/kpunch/fileMgr/fileMgr.dhs
index 73da0d4a..0ed22e22 100644
--- a/src/apps/kpunch/fileMgr/fileMgr.dhs
+++ b/src/apps/kpunch/fileMgr/fileMgr.dhs
@@ -105,6 +105,7 @@ Double-click an item, or select it and press Enter (or choose File > Open):
.list
.item A folder opens in the same window.
.item A program (Type: Program) starts.
+.item A DOS program (a file ending in .EXE, .COM, or .BAT) runs with DVX stepped aside: DVX first names the programs that will close, this File Manager too, and asks whether to go on, and comes back when the DOS program ends (see DOS Programs in the Program Manager's help).
.item A document opens in the program chosen for its type, for example a .TXT file in Notepad. Which program opens each kind of file is set on the File Types tab of the Control Panel. If no program is chosen for a file's type, the File Manager says so.
.endlist
diff --git a/src/apps/kpunch/fileMgr/fileMgr.frm b/src/apps/kpunch/fileMgr/fileMgr.frm
index da070ccc..67d17645 100644
--- a/src/apps/kpunch/fileMgr/fileMgr.frm
+++ b/src/apps/kpunch/fileMgr/fileMgr.frm
@@ -1092,7 +1092,9 @@ SUB CmdOpen
ext = FileOpsExtension$(entryName)
- IF LCase$(ext) = "app" THEN
+ ' A DOS program runs with DVX stepped aside (DVX asks first when other
+ ' programs, this one too, would close)
+ IF LCase$(ext) = "app" OR ShellIsDosProgram(path) THEN
ShellRun path, ""
EXIT SUB
END IF
diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.dhs b/src/apps/kpunch/pkgbuild/pkgbuild.dhs
index 0e3fd062..b349496d 100644
--- a/src/apps/kpunch/pkgbuild/pkgbuild.dhs
+++ b/src/apps/kpunch/pkgbuild/pkgbuild.dhs
@@ -67,7 +67,7 @@ What you fill in can be saved as a template (File > Save) and opened again (File
.item Name: what Software shows, such as Chess.
.item Package name: who made it and what it is, with a dot between them, such as acme.chess. Each part is 1 to 8 lower-case letters, digits, or _. Two packages with the same package name are the same package, so keep it once you have chosen it.
.item Version: numbers with dots between them, such as 1.0.0. Give each new build you hand out a higher version: Software installs a higher version over a lower one, but not the other way around.
-.item Kind: what the package holds -- app (a program), library, widget, font, bank (MIDI instruments), theme, wallpaper (desktop pictures), sound, help, or bundle (no files of its own: a set of other packages, named on the Needs page).
+.item Kind: what the package holds -- app (a program), library, widget, font, bank (MIDI instruments), theme, wallpaper (desktop pictures), sound, saver (screen savers), help, or bundle (no files of its own: a set of other packages, named on the Needs page).
.item Summary: a sentence about what it is.
.item Oldest DVX: the oldest version of DVX it works with. It starts as this DVX's version; leave it empty when any version will do.
.endlist
@@ -114,6 +114,7 @@ Where each kind of package keeps its files, for a package named acme.chess:
theme SHARED\THEMES\ACME
wallpaper SHARED\WPAPER\ACME
sound SHARED\SOUNDS\ACME
+ saver SHARED\SAVERS\ACME
.endtable
A package may put files only in its own kind's folder, so it cannot change DVX or another publisher's files. Build Package says so when a file is somewhere else. A file named PACKAGE.INI or PACKAGE.SIG at the top of the folder is left out.
diff --git a/src/apps/kpunch/player/ICON32.BMP b/src/apps/kpunch/player/ICON32.BMP
new file mode 100644
index 00000000..daf9dc04
--- /dev/null
+++ b/src/apps/kpunch/player/ICON32.BMP
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e71d0dd252d1b019e32c45c5b85ede02332a638f77b1ad627cd8837c6eda8ac7
+size 3126
diff --git a/src/apps/kpunch/player/player.dbp b/src/apps/kpunch/player/player.dbp
new file mode 100644
index 00000000..66f92b91
--- /dev/null
+++ b/src/apps/kpunch/player/player.dbp
@@ -0,0 +1,24 @@
+[Project]
+Name = Media Player
+Author = Scott Duensing
+Publisher = Kangaroo Punch Studios
+Version = 1.00
+Copyright = Copyright 2026 Scott Duensing
+Description = Plays music modules, MIDI files, and WAV and VOC sounds from a list
+Icon = ICON32.BMP
+
+[Modules]
+File0 = ../../../include/basic/commdlg.bas
+File1 = ../../../include/basic/fileops.bas
+File2 = ../../../include/basic/help.bas
+File3 = ../../../include/basic/music.bas
+File4 = ../../../include/basic/prefs.bas
+Count = 5
+
+[Forms]
+File0 = player.frm
+Count = 1
+
+[Settings]
+StartupForm = PlayerForm
+MultiInstance = False
diff --git a/src/apps/kpunch/player/player.dhs b/src/apps/kpunch/player/player.dhs
new file mode 100644
index 00000000..bf43fb2f
--- /dev/null
+++ b/src/apps/kpunch/player/player.dhs
@@ -0,0 +1,150 @@
+# 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.
+
+# Media Player help. Compiled to player.hlp next to player.app; F1 and
+# Help > Contents in Media Player open it.
+
+.topic player.overview
+.title Media Player
+.toc 0 Media Player
+.default
+.index Media Player
+.index Music
+.index Sounds
+.index MOD
+.index S3M
+.index XM
+.index IT
+.index MIDI
+.index WAV
+.index VOC
+.index Playlist
+
+.h1 Media Player
+
+Media Player plays music and sounds from a list: music modules (MOD, S3M, XM, and IT files), MIDI music (MID and RMI files), and recorded sounds (WAV and VOC files). Add the files you want to hear to the list, and Media Player plays them one after another.
+
+.h2 Playing a File
+
+Choose File > Open..., pick a file, and it plays at once. It is added to the end of the list, unless it is there already.
+
+A music or sound file can also be opened straight from the File Manager (double-click it) or the Program Manager: Media Player starts and plays it. If Media Player is already open, the file is added to its list and plays there, so double-clicking one file after another builds up a list.
+
+The top of the window shows what is playing: the music's own title when it has one (modules and many MIDI files do), otherwise the file's name, and below it what kind of file it is and what plays it, for example "Module (XM) - Sound device" or "MIDI music (MID) - OPL3 FM synthesizer". The window's title bar shows the title too.
+
+If a file cannot be played, Media Player says why. Recorded sounds must be uncompressed (8-bit or 16-bit); a damaged file, or one that is not music at all, is also refused.
+
+.h2 The Controls
+
+.table
+ Control What it does
+ ------- ------------
+ Previous Go back to the start of the music, or, in its first three seconds, to the track before it.
+ Play Play the selected track. While music plays the button says Pause and pauses it; choose it again to go on from the same place.
+ Stop Stop the music and go back to its start.
+ Next Go to the next track.
+ Volume Drag the slider to make the music quieter or louder.
+.endtable
+
+The long slider under the title shows where the music is, with the time played and the whole length beside it, for example 1:23 / 4:56. Drag it, or click it and use the arrow keys, to go to another place. A module goes to the start of the part of the song that holds that place, so it may start a little earlier than where you let go.
+
+.h2 The List
+
+The list shows the files to play, in the order they play. Double-click a track, or select it and press Enter, to play it.
+
+.table
+ Command What it does
+ ------- ------------
+ Add File... Add one file to the end of the list.
+ Add Folder... Add every music and sound file in a folder, in name order.
+ Move Up Move the selected track one place up (Ctrl+Up).
+ Move Down Move the selected track one place down (Ctrl+Down).
+ Remove Take the selected track out of the list (Del). If it is playing, it stops.
+ Clear List Take every track out of the list.
+.endtable
+
+Taking a track out of the list never deletes the file.
+
+When a track ends, the next one plays. After the last track Media Player stops, unless Repeat List is on.
+
+.h2 Repeat and Shuffle
+
+On the Play menu:
+
+.list
+.item Repeat Off: play the list once and stop.
+.item Repeat List: after the last track, start the list again.
+.item Repeat Track: play the same track over and over.
+.item Shuffle: play the tracks in a mixed-up order. Each track plays once before any plays again; with Repeat List each new round has a new order.
+.endlist
+
+.h2 Where the Music Plays
+
+Media Player plays through the sound card and the settings on the Control Panel's Sounds page. Recorded sounds and modules play on any sound card that plays recorded sound; with a Gravis UltraSound or a Sound Blaster AWE32 or AWE64, modules can play on the card's own voices, which takes the computer less time. MIDI music plays on the music synthesizer the Sounds page chooses: an external MIDI keyboard or sound module, a wavetable card, the FM chip, or DVX's own synthesizer when the card has none. Change those settings on the Control Panel's Sounds page.
+
+WAV recordings are read from the disk as they play, so even a recording far bigger than the computer's memory plays. A VOC file is read into memory first.
+
+.h2 Keys
+
+.table
+ Key What it does
+ --- ------------
+ Ctrl+O Open a file and play it.
+ Ctrl+P Play or pause.
+ Ctrl+S Stop.
+ Ctrl+B Previous.
+ Ctrl+F Next.
+ Ctrl+Up Move the selected track up.
+ Ctrl+Down Move the selected track down.
+ Enter Play the selected track (in the list).
+ Del Take the selected track out of the list (in the list).
+ F1 Open this help.
+.endtable
+
+.h2 Menus
+
+.table
+ Menu Command What it does
+ ---- ------- ------------
+ File Open... Play a file, adding it to the list.
+ File Add File... Add a file to the list.
+ File Add Folder... Add a folder's music and sound files to the list.
+ File Move Up Move the selected track up.
+ File Move Down Move the selected track down.
+ File Remove Take the selected track out of the list.
+ File Clear List Take every track out of the list.
+ File Exit Close Media Player.
+ Play Play / Pause Play, pause, or go on.
+ Play Stop Stop and go back to the start.
+ Play Previous The start of the track, or the track before.
+ Play Next The next track.
+ Play Repeat Off Play the list once.
+ Play Repeat List Play the list over and over.
+ Play Repeat Track Play one track over and over.
+ Play Shuffle Play the list in a mixed-up order.
+ Help Contents Open this help.
+ Help About Media Player Show the program name and copyright.
+.endtable
+
+.note tip
+Media Player remembers the list, the track you had selected, the volume, Repeat, Shuffle, and the folder you last opened files from. The next time it starts, the list is there, ready to play.
+.endnote
diff --git a/src/apps/kpunch/player/player.frm b/src/apps/kpunch/player/player.frm
new file mode 100644
index 00000000..80663aa5
--- /dev/null
+++ b/src/apps/kpunch/player/player.frm
@@ -0,0 +1,1146 @@
+VERSION DVX 1.00
+Begin Form PlayerForm
+ Caption = "Media Player"
+ Layout = VBox
+ AutoSize = False
+ Resizable = True
+ Centered = True
+ Width = 520
+ Height = 340
+ Begin Menu mnuFile
+ Caption = "&File"
+ Begin Menu mnuOpen
+ Caption = "&Open..."
+ Shortcut = "Ctrl+O"
+ End
+ Begin Menu mnuAddFile
+ Caption = "&Add File..."
+ End
+ Begin Menu mnuAddFolder
+ Caption = "Add Fol&der..."
+ End
+ Begin Menu mnuFileSep1
+ Caption = "-"
+ End
+ Begin Menu mnuMoveUp
+ Caption = "Move &Up"
+ Shortcut = "Ctrl+Up"
+ End
+ Begin Menu mnuMoveDown
+ Caption = "Move Do&wn"
+ Shortcut = "Ctrl+Down"
+ End
+ Begin Menu mnuRemove
+ Caption = "&Remove"
+ ShortcutText = "Del"
+ End
+ Begin Menu mnuClear
+ Caption = "&Clear List"
+ End
+ Begin Menu mnuFileSep2
+ Caption = "-"
+ End
+ Begin Menu mnuExit
+ Caption = "E&xit"
+ End
+ End
+ Begin Menu mnuPlay
+ Caption = "&Play"
+ Begin Menu mnuPlayPause
+ Caption = "&Play / Pause"
+ Shortcut = "Ctrl+P"
+ End
+ Begin Menu mnuStop
+ Caption = "&Stop"
+ Shortcut = "Ctrl+S"
+ End
+ Begin Menu mnuPrev
+ Caption = "Pre&vious"
+ Shortcut = "Ctrl+B"
+ End
+ Begin Menu mnuNext
+ Caption = "&Next"
+ Shortcut = "Ctrl+F"
+ End
+ Begin Menu mnuPlaySep1
+ Caption = "-"
+ End
+ Begin Menu mnuRepeatOff
+ Caption = "Repeat O&ff"
+ RadioCheck = True
+ Checked = True
+ End
+ Begin Menu mnuRepeatList
+ Caption = "Repeat &List"
+ RadioCheck = True
+ Checked = False
+ End
+ Begin Menu mnuRepeatTrack
+ Caption = "Repeat &Track"
+ RadioCheck = True
+ Checked = False
+ End
+ Begin Menu mnuPlaySep2
+ Caption = "-"
+ End
+ Begin Menu mnuShuffle
+ Caption = "S&huffle"
+ Checked = False
+ End
+ End
+ Begin Menu mnuHelp
+ Caption = "&Help"
+ Begin Menu mnuHelpContents
+ Caption = "&Contents"
+ ShortcutText = "F1"
+ End
+ Begin Menu mnuHelpSep
+ Caption = "-"
+ End
+ Begin Menu mnuAbout
+ Caption = "&About Media Player..."
+ End
+ End
+ Begin Label lblTitle
+ Caption = "Nothing is playing"
+ FontBold = True
+ Weight = 0
+ End
+ Begin Label lblInfo
+ Caption = ""
+ Weight = 0
+ End
+ Begin HBox rowPosition
+ Weight = 0
+ Begin HScrollBar sldPosition
+ Weight = 1
+ ToolTipText = "Where the music is. Drag it to go to another place."
+ End
+ Begin Label lblTime
+ Caption = "0:00 / 0:00"
+ Alignment = Right
+ MinWidth = 104
+ Weight = 0
+ End
+ End
+ Begin HBox rowButtons
+ Weight = 0
+ Begin CommandButton btnPrev
+ Caption = "Pre&vious"
+ MinWidth = 76
+ Weight = 0
+ End
+ Begin CommandButton btnPlay
+ Caption = "Pl&ay"
+ MinWidth = 76
+ Weight = 0
+ End
+ Begin CommandButton btnStop
+ Caption = "&Stop"
+ MinWidth = 76
+ Weight = 0
+ End
+ Begin CommandButton btnNext
+ Caption = "&Next"
+ MinWidth = 76
+ Weight = 0
+ End
+ Begin Spacer spcButtons
+ Weight = 1
+ End
+ Begin Label lblVolume
+ Caption = "Vol&ume:"
+ Weight = 0
+ End
+ Begin HScrollBar sldVolume
+ MinWidth = 80
+ Weight = 0
+ ToolTipText = "How loud the music plays"
+ End
+ End
+ Begin ListBox lstTracks
+ Weight = 1
+ End
+ Begin Timer tmrTick
+ Interval = 250
+ Enabled = True
+ End
+End
+
+' 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.
+
+' Media Player: music modules (MOD, S3M, XM, IT), MIDI files, and WAV and
+' VOC sounds played from a list (music.bas).
+'
+' The list holds files by their paths (trackPath, in the list box's
+' order); the track loaded is current, its music number music. Next and
+' Previous, and the end of a track, walk playOrder: the list's order, or
+' a shuffled one with the track playing first. Repeat and Shuffle are
+' the Play menu's checks, the volume is its slider: nothing else keeps
+' them.
+'
+' A timer shows where the music is and notices its end (playing, not
+' paused, and MusicPlaying False). Moving the position slider fires
+' Change at every step of a drag, so the music goes there only once the
+' slider has stood still a moment; the timer leaves the slider alone
+' meanwhile.
+'
+' Files named when the program starts, or handed to it while it runs
+' (a music file opened from Program Manager or File Manager), are added
+' to the list and the first of them plays. The list and the settings
+' are kept in PLAYER.INI in the program's own user folder.
+
+OPTION EXPLICIT
+
+CONST APP_TITLE = "Media Player"
+CONST HELP_FILE = "player.hlp"
+
+' What Open, Add File, and Add Folder offer
+CONST MUSIC_PATTERN = "*.mod;*.s3m;*.xm;*.it;*.mid;*.rmi;*.wav;*.voc"
+CONST OPEN_FILTER = "Music and sounds (" + MUSIC_PATTERN + ")|All Files (*.*)"
+
+' Settings: PLAYER.INI in App.User; the list is File1, File2, ...
+CONST SETTINGS_FILE = "PLAYER.INI"
+CONST INI_PLAYER = "Player"
+CONST INI_LIST = "List"
+CONST KEY_VOLUME = "Volume"
+CONST KEY_REPEAT = "Repeat"
+CONST KEY_SHUFFLE = "Shuffle"
+CONST KEY_FOLDER = "Folder"
+CONST KEY_TRACK = "Track"
+CONST KEY_FILE = "File"
+CONST REPEAT_OFF = "off"
+CONST REPEAT_LIST = "list"
+CONST REPEAT_TRACK = "track"
+
+CONST VOLUME_MAX = 100
+CONST SEEK_SETTLE_TICKS = 2 ' the slider stood still a whole tick (tmrTick.Interval)
+CONST RESTART_MS = 3000 ' Previous this far into a track starts it again
+CONST MS_PER_SEC = 1000
+CONST SEC_PER_MIN = 60
+CONST LIST_GROW = 16 ' trackPath's first size; it grows by doubling
+CONST NO_TRACK = -1
+CONST NO_SEEK = -1
+
+DIM SHARED trackPath() AS STRING ' the list's files, in its order
+DIM SHARED trackCount AS LONG
+DIM SHARED playOrder() AS LONG ' the order Next takes the tracks in
+DIM SHARED orderPos AS LONG ' current's place in playOrder
+DIM SHARED current AS LONG ' the track loaded (NO_TRACK: none)
+DIM SHARED music AS LONG ' its music number (0: none)
+DIM SHARED playing AS BOOLEAN ' started and not stopped since
+DIM SHARED paused AS BOOLEAN
+DIM SHARED lastFolder AS STRING ' where the file dialogs start
+DIM SHARED seekTo AS LONG ' where the slider was moved to (NO_SEEK: nowhere)
+DIM SHARED seekTicks AS LONG ' ticks the slider has stood still since
+
+
+' ============================================================
+' Window
+' ============================================================
+
+SUB PlayerForm_Load
+ RANDOMIZE TIMER
+ ResetList
+ seekTo = NO_SEEK
+ sldVolume.Max = VOLUME_MAX
+ sldVolume.Value = VOLUME_MAX
+ LoadSettings
+ ShowTrack
+ AddArguments
+END SUB
+
+
+' Another start of the program while it runs: its files are added here.
+SUB PlayerForm_NewArgs
+ AddArguments
+END SUB
+
+
+SUB PlayerForm_Unload
+ SaveSettings
+END SUB
+
+
+' The files named by COMMAND$ added to the list (one already there is
+' not added again); the first of them plays.
+SUB AddArguments
+ DIM i AS LONG
+ DIM index AS LONG
+ DIM first AS LONG
+
+ first = NO_TRACK
+ i = 1
+
+ DO WHILE COMMAND$(i) <> ""
+ index = FindTrack(COMMAND$(i))
+
+ IF index = NO_TRACK THEN
+ index = AppendTrack(COMMAND$(i))
+ END IF
+
+ IF first = NO_TRACK THEN
+ first = index
+ END IF
+
+ i = i + 1
+ LOOP
+
+ IF first <> NO_TRACK THEN
+ BuildOrder
+ PlayTrack first
+ END IF
+END SUB
+
+
+' ============================================================
+' The list
+' ============================================================
+
+' An empty list (its memory let go): nothing loaded, nothing current.
+SUB ResetList
+ REDIM trackPath(LIST_GROW) AS STRING
+ trackCount = 0
+ current = NO_TRACK
+END SUB
+
+
+' Add path at the end of the list; its index. The caller rebuilds the
+' play order (BuildOrder) once it has added what it adds.
+FUNCTION AppendTrack(BYVAL path AS STRING) AS LONG
+ IF trackCount > UBOUND(trackPath) THEN
+ REDIM PRESERVE trackPath(trackCount * 2 + LIST_GROW) AS STRING
+ END IF
+
+ trackPath(trackCount) = path
+ lstTracks.AddItem FileOpsBaseName$(path)
+ AppendTrack = trackCount
+ trackCount = trackCount + 1
+END FUNCTION
+
+
+' The index of path in the list, or NO_TRACK.
+FUNCTION FindTrack(BYVAL path AS STRING) AS LONG
+ DIM i AS LONG
+
+ FOR i = 0 TO trackCount - 1
+ IF FileOpsPathSame(trackPath(i), path) THEN
+ FindTrack = i
+ EXIT FUNCTION
+ END IF
+ NEXT i
+
+ FindTrack = NO_TRACK
+END FUNCTION
+
+
+' The list box shown again from trackPath, index selected.
+SUB RefreshList(BYVAL index AS LONG)
+ DIM i AS LONG
+ DIM spec AS STRING
+
+ FOR i = 0 TO trackCount - 1
+ IF i > 0 THEN
+ spec = spec + "|"
+ END IF
+
+ spec = spec + FileOpsBaseName$(trackPath(i))
+ NEXT i
+
+ lstTracks.SetItems spec
+ lstTracks.ListIndex = index
+END SUB
+
+
+' Take a track out of the list; the one playing stops.
+SUB RemoveTrack(BYVAL index AS LONG)
+ DIM i AS LONG
+
+ FOR i = index TO trackCount - 2
+ trackPath(i) = trackPath(i + 1)
+ NEXT i
+
+ trackCount = trackCount - 1
+
+ IF index = current THEN
+ FreeMusic
+ current = NO_TRACK
+ ShowTrack
+ ELSEIF index < current THEN
+ current = current - 1
+ END IF
+
+ lstTracks.RemoveItem index
+
+ IF index < trackCount THEN
+ lstTracks.ListIndex = index
+ ELSE
+ lstTracks.ListIndex = trackCount - 1
+ END IF
+
+ BuildOrder
+END SUB
+
+
+' Swap the selected track with the one direction (-1 up, 1 down) from it.
+SUB MoveTrack(BYVAL direction AS LONG)
+ DIM index AS LONG
+ DIM other AS LONG
+ DIM path AS STRING
+
+ index = lstTracks.ListIndex
+ other = index + direction
+
+ IF index < 0 OR other < 0 OR other >= trackCount THEN
+ EXIT SUB
+ END IF
+
+ path = trackPath(index)
+ trackPath(index) = trackPath(other)
+ trackPath(other) = path
+
+ IF current = index THEN
+ current = other
+ ELSEIF current = other THEN
+ current = index
+ END IF
+
+ RefreshList other
+ BuildOrder
+END SUB
+
+
+' ============================================================
+' Play order
+' ============================================================
+
+' playOrder made again: the list's order, or shuffled with the track
+' playing first so the others all follow it.
+SUB BuildOrder
+ DIM i AS LONG
+ DIM j AS LONG
+ DIM t AS LONG
+
+ orderPos = 0
+
+ IF trackCount = 0 THEN
+ EXIT SUB
+ END IF
+
+ REDIM playOrder(trackCount - 1) AS LONG
+
+ FOR i = 0 TO trackCount - 1
+ playOrder(i) = i
+ NEXT i
+
+ IF mnuShuffle.Checked THEN
+ FOR i = trackCount - 1 TO 1 STEP -1
+ j = INT(RND * (i + 1))
+ t = playOrder(i)
+ playOrder(i) = playOrder(j)
+ playOrder(j) = t
+ NEXT i
+
+ IF current <> NO_TRACK THEN
+ j = OrderOf(current)
+ playOrder(j) = playOrder(0)
+ playOrder(0) = current
+ END IF
+ END IF
+
+ orderPos = OrderOf(current)
+END SUB
+
+
+' The place of a track in playOrder (0 for NO_TRACK).
+FUNCTION OrderOf(BYVAL track AS LONG) AS LONG
+ DIM i AS LONG
+
+ FOR i = 0 TO trackCount - 1
+ IF playOrder(i) = track THEN
+ OrderOf = i
+ EXIT FUNCTION
+ END IF
+ NEXT i
+
+ OrderOf = 0
+END FUNCTION
+
+
+' The track direction (1 next, -1 previous) steps to in playOrder, or
+' NO_TRACK past the end. With Repeat List the list goes round again (a
+' new order when shuffled); before the first track is the first track.
+FUNCTION StepTrack(BYVAL direction AS LONG) AS LONG
+ DIM pos AS LONG
+
+ IF trackCount = 0 THEN
+ StepTrack = NO_TRACK
+ EXIT FUNCTION
+ END IF
+
+ pos = orderPos + direction
+
+ IF current = NO_TRACK THEN
+ pos = 0
+ ELSEIF pos >= trackCount THEN
+ IF NOT mnuRepeatList.Checked THEN
+ StepTrack = NO_TRACK
+ EXIT FUNCTION
+ END IF
+
+ pos = 0
+
+ IF mnuShuffle.Checked AND trackCount > 1 THEN
+ BuildOrder
+ pos = 1
+ END IF
+ ELSEIF pos < 0 THEN
+ pos = 0
+
+ IF mnuRepeatList.Checked THEN
+ pos = trackCount - 1
+ END IF
+ END IF
+
+ orderPos = pos
+ StepTrack = playOrder(pos)
+END FUNCTION
+
+
+' ============================================================
+' Playing
+' ============================================================
+
+' Load a track and play it; False, once the user has been told why, when
+' it cannot be played.
+FUNCTION PlayTrack(BYVAL index AS LONG) AS BOOLEAN
+ DIM reason AS STRING
+
+ FreeMusic
+ current = index
+ orderPos = OrderOf(index)
+ lstTracks.ListIndex = index
+ music = MusicLoad(trackPath(index))
+
+ ' MusicError$ says why the load or the play (the last call) failed.
+ IF music <> 0 THEN
+ MusicVolume music, sldVolume.Value
+ MusicPlay music, mnuRepeatTrack.Checked
+ playing = MusicPlaying(music) <> 0
+ END IF
+
+ IF NOT playing THEN
+ reason = MusicError$()
+ FreeMusic
+ ShowTrack
+ lblInfo.Caption = reason
+ MsgBox "Media Player cannot play " + FileOpsBaseName$(trackPath(index)) + "." + CHR$(10) + CHR$(10) + reason, vbOKOnly + vbExclamation, APP_TITLE
+ PlayTrack = False
+ EXIT FUNCTION
+ END IF
+
+ ShowTrack
+ PlayTrack = True
+END FUNCTION
+
+
+' Play index, or when it cannot be played the tracks after it (direction
+' 1) or before it (-1), until one plays or every one has been tried.
+SUB PlayFrom(BYVAL index AS LONG, BYVAL direction AS LONG)
+ DIM tries AS LONG
+
+ DO WHILE index <> NO_TRACK AND tries < trackCount
+ IF PlayTrack(index) THEN
+ EXIT SUB
+ END IF
+
+ tries = tries + 1
+ index = StepTrack(direction)
+ LOOP
+END SUB
+
+
+' Let the music go; nothing is loaded.
+SUB FreeMusic
+ IF music <> 0 THEN
+ MusicFree music
+ END IF
+
+ music = 0
+ playing = False
+ paused = False
+ seekTo = NO_SEEK
+END SUB
+
+
+' Play, pause, or go on. Stopped, or with nothing loaded, the selected
+' track plays (the loaded one goes on from where it is).
+SUB PlayOrPause
+ DIM index AS LONG
+
+ IF playing THEN
+ IF paused THEN
+ MusicPlay music, mnuRepeatTrack.Checked
+ ELSE
+ MusicPause music
+ END IF
+
+ paused = NOT paused
+ ShowState
+ EXIT SUB
+ END IF
+
+ index = lstTracks.ListIndex
+
+ IF index < 0 THEN
+ index = current
+ END IF
+
+ IF index = NO_TRACK AND trackCount > 0 THEN
+ index = playOrder(0)
+ END IF
+
+ IF index = NO_TRACK THEN
+ EXIT SUB
+ END IF
+
+ IF index = current AND music <> 0 THEN
+ MusicPlay music, mnuRepeatTrack.Checked
+ playing = True
+ ShowState
+ ELSE
+ PlayTrack index
+ END IF
+END SUB
+
+
+SUB StopPlaying
+ IF music = 0 THEN
+ EXIT SUB
+ END IF
+
+ MusicStop music
+ playing = False
+ paused = False
+ seekTo = NO_SEEK
+ ShowState
+ ShowPosition
+END SUB
+
+
+SUB PlayNext
+ PlayFrom StepTrack(1), 1
+END SUB
+
+
+' Back to the start of the track, or, near its start, the track before.
+SUB PlayPrevious
+ IF music <> 0 THEN
+ IF MusicPosition(music) > RESTART_MS THEN
+ MusicSeek music, 0
+ ShowPosition
+ EXIT SUB
+ END IF
+ END IF
+
+ PlayFrom StepTrack(-1), -1
+END SUB
+
+
+' The track played to its end: the next one, or at the end of the list
+' the last one stopped.
+SUB TrackEnded
+ DIM index AS LONG
+
+ index = StepTrack(1)
+
+ IF index = NO_TRACK THEN
+ StopPlaying
+ EXIT SUB
+ END IF
+
+ PlayFrom index, 1
+END SUB
+
+
+' Repeat Track loops the music itself; a change reaches it at once.
+SUB ApplyRepeat
+ IF playing AND NOT paused THEN
+ MusicPlay music, mnuRepeatTrack.Checked
+ END IF
+END SUB
+
+
+SUB tmrTick_Timer
+ IF music = 0 THEN
+ EXIT SUB
+ END IF
+
+ IF seekTo <> NO_SEEK THEN
+ seekTicks = seekTicks + 1
+
+ IF seekTicks >= SEEK_SETTLE_TICKS THEN
+ MusicSeek music, seekTo
+ seekTo = NO_SEEK
+ END IF
+
+ EXIT SUB
+ END IF
+
+ IF playing AND NOT paused AND NOT MusicPlaying(music) THEN
+ TrackEnded
+ EXIT SUB
+ END IF
+
+ ShowPosition
+END SUB
+
+
+SUB sldPosition_Change
+ IF music = 0 THEN
+ EXIT SUB
+ END IF
+
+ seekTo = sldPosition.Value * MS_PER_SEC
+ seekTicks = 0
+ lblTime.Caption = TimeText$(seekTo) + " / " + TimeText$(MusicLength(music))
+END SUB
+
+
+SUB sldVolume_Change
+ IF music <> 0 THEN
+ MusicVolume music, sldVolume.Value
+ END IF
+END SUB
+
+
+' ============================================================
+' What is shown
+' ============================================================
+
+' The track's title (its own, or its file name), what it is and where it
+' plays, and the window's title; then the buttons and the position.
+SUB ShowTrack
+ DIM title AS STRING
+ DIM fileName AS STRING
+
+ IF current = NO_TRACK THEN
+ lblTitle.Caption = "Nothing is playing"
+ lblInfo.Caption = ""
+ PlayerForm.Caption = APP_TITLE
+ ELSE
+ fileName = FileOpsBaseName$(trackPath(current))
+ title = ""
+
+ IF music <> 0 THEN
+ title = TRIM$(MusicInfo$(music, "title"))
+ END IF
+
+ IF title = "" THEN
+ title = fileName
+ END IF
+
+ lblTitle.Caption = title
+ lblInfo.Caption = KindText$(fileName)
+ PlayerForm.Caption = title + " - " + APP_TITLE
+ END IF
+
+ sldPosition.Value = 0
+ sldPosition.Max = 0
+
+ IF music <> 0 THEN
+ sldPosition.Max = MusicLength(music) \ MS_PER_SEC
+ END IF
+
+ ShowState
+ ShowPosition
+END SUB
+
+
+' "Module (XM) - Sound device": what the music is and where it plays.
+FUNCTION KindText$(BYVAL fileName AS STRING)
+ DIM kind AS STRING
+
+ IF music = 0 THEN
+ KindText$ = ""
+ EXIT FUNCTION
+ END IF
+
+ SELECT CASE MusicInfo$(music, "kind")
+ CASE "module"
+ kind = "Module"
+ CASE "midi"
+ kind = "MIDI music"
+ CASE ELSE
+ kind = "Sound"
+ END SELECT
+
+ KindText$ = kind + " (" + UCASE$(FileOpsExtension$(fileName)) + ") - " + MusicInfo$(music, "output")
+END FUNCTION
+
+
+' The Play button says what it does; the position slider and Stop work
+' with a music loaded.
+SUB ShowState
+ IF playing AND NOT paused THEN
+ btnPlay.Caption = "P&ause"
+ ELSE
+ btnPlay.Caption = "Pl&ay"
+ END IF
+
+ sldPosition.Enabled = music <> 0
+ btnStop.Enabled = music <> 0
+END SUB
+
+
+' The slider and the time at the music's place.
+SUB ShowPosition
+ DIM position AS LONG
+ DIM length AS LONG
+ DIM text AS STRING
+
+ IF music <> 0 THEN
+ position = MusicPosition(music)
+ length = MusicLength(music)
+ END IF
+
+ IF sldPosition.Value <> position \ MS_PER_SEC THEN
+ sldPosition.Value = position \ MS_PER_SEC
+ END IF
+
+ text = TimeText$(position) + " / " + TimeText$(length)
+
+ IF lblTime.Caption <> text THEN
+ lblTime.Caption = text
+ END IF
+END SUB
+
+
+' Milliseconds as minutes and seconds: "3:07".
+FUNCTION TimeText$(BYVAL ms AS LONG)
+ DIM secs AS LONG
+
+ secs = ms \ MS_PER_SEC
+ TimeText$ = LTRIM$(STR$(secs \ SEC_PER_MIN)) + ":" + RIGHT$("0" + LTRIM$(STR$(secs MOD SEC_PER_MIN)), 2)
+END FUNCTION
+
+
+' ============================================================
+' Settings
+' ============================================================
+
+FUNCTION SettingsPath$()
+ SettingsPath$ = FileOpsPathJoin$(App.User, SETTINGS_FILE)
+END FUNCTION
+
+
+' The volume, Repeat, Shuffle, the folder, and the list with its
+' selected track, as the program last left them.
+SUB LoadSettings
+ DIM ini AS LONG
+ DIM i AS LONG
+ DIM path AS STRING
+ DIM track AS LONG
+ DIM repeatName AS STRING
+
+ ini = PrefsLoad(SettingsPath$())
+
+ IF ini = 0 THEN
+ EXIT SUB
+ END IF
+
+ sldVolume.Value = PrefsGetInt(ini, INI_PLAYER, KEY_VOLUME, VOLUME_MAX)
+ repeatName = LCASE$(PrefsGet$(ini, INI_PLAYER, KEY_REPEAT, REPEAT_OFF))
+
+ mnuRepeatOff.Checked = repeatName <> REPEAT_LIST AND repeatName <> REPEAT_TRACK
+ mnuRepeatList.Checked = repeatName = REPEAT_LIST
+ mnuRepeatTrack.Checked = repeatName = REPEAT_TRACK
+ mnuShuffle.Checked = PrefsGetBool(ini, INI_PLAYER, KEY_SHUFFLE, False)
+ lastFolder = PrefsGet$(ini, INI_PLAYER, KEY_FOLDER, "")
+
+ i = 1
+
+ DO
+ path = PrefsGet$(ini, INI_LIST, KEY_FILE + LTRIM$(STR$(i)), "")
+
+ IF path = "" THEN
+ EXIT DO
+ END IF
+
+ AppendTrack path
+ i = i + 1
+ LOOP
+
+ track = PrefsGetInt(ini, INI_PLAYER, KEY_TRACK, NO_TRACK)
+
+ IF track >= 0 AND track < trackCount THEN
+ lstTracks.ListIndex = track
+ END IF
+
+ PrefsClose ini
+ BuildOrder
+END SUB
+
+
+' Written whole each time, so tracks taken out of the list are gone from
+' the file too.
+SUB SaveSettings
+ DIM ini AS LONG
+ DIM i AS LONG
+ DIM repeatName AS STRING
+
+ ini = PrefsCreate()
+
+ IF ini = 0 THEN
+ EXIT SUB
+ END IF
+
+ repeatName = REPEAT_OFF
+
+ IF mnuRepeatList.Checked THEN
+ repeatName = REPEAT_LIST
+ ELSEIF mnuRepeatTrack.Checked THEN
+ repeatName = REPEAT_TRACK
+ END IF
+
+ PrefsSetInt ini, INI_PLAYER, KEY_VOLUME, sldVolume.Value
+ PrefsSet ini, INI_PLAYER, KEY_REPEAT, repeatName
+ PrefsSetBool ini, INI_PLAYER, KEY_SHUFFLE, mnuShuffle.Checked
+ PrefsSet ini, INI_PLAYER, KEY_FOLDER, lastFolder
+ PrefsSetInt ini, INI_PLAYER, KEY_TRACK, lstTracks.ListIndex
+
+ FOR i = 0 TO trackCount - 1
+ PrefsSet ini, INI_LIST, KEY_FILE + LTRIM$(STR$(i + 1)), trackPath(i)
+ NEXT i
+
+ PrefsSaveAs ini, SettingsPath$()
+ PrefsClose ini
+END SUB
+
+
+' ============================================================
+' Menus, buttons, and the list
+' ============================================================
+
+' A file played: added to the list unless it is there already.
+SUB mnuOpen_Click
+ DIM path AS STRING
+ DIM index AS LONG
+
+ path = DlgFileOpenIn$("Open", OPEN_FILTER, lastFolder)
+
+ IF path = "" THEN
+ EXIT SUB
+ END IF
+
+ lastFolder = FileOpsPathParent$(path)
+ index = FindTrack(path)
+
+ IF index = NO_TRACK THEN
+ index = AppendTrack(path)
+ BuildOrder
+ END IF
+
+ PlayTrack index
+END SUB
+
+
+SUB mnuAddFile_Click
+ DIM path AS STRING
+
+ path = DlgFileOpenIn$("Add File", OPEN_FILTER, lastFolder)
+
+ IF path = "" THEN
+ EXIT SUB
+ END IF
+
+ lastFolder = FileOpsPathParent$(path)
+ AppendTrack path
+ BuildOrder
+END SUB
+
+
+' Every music and sound file in a folder, by name.
+SUB mnuAddFolder_Click
+ DIM folder AS STRING
+ DIM listing AS LONG
+ DIM i AS LONG
+ DIM added AS LONG
+
+ folder = DlgFolder$("Add Folder", lastFolder)
+
+ IF folder = "" THEN
+ EXIT SUB
+ END IF
+
+ lastFolder = folder
+ listing = FileOpsList(folder, MUSIC_PATTERN)
+
+ IF listing = 0 THEN
+ MsgBox "The folder " + folder + " cannot be read.", vbOKOnly + vbExclamation, APP_TITLE
+ EXIT SUB
+ END IF
+
+ FOR i = 0 TO FileOpsListCount(listing) - 1
+ IF NOT FileOpsListIsFolder(listing, i) THEN
+ AppendTrack FileOpsPathJoin$(folder, FileOpsListName$(listing, i))
+ added = added + 1
+ END IF
+ NEXT i
+
+ FileOpsListFree listing
+
+ IF added = 0 THEN
+ MsgBox "There are no music or sound files in " + folder + ".", vbOKOnly + vbInformation, APP_TITLE
+ EXIT SUB
+ END IF
+
+ BuildOrder
+END SUB
+
+
+SUB mnuMoveUp_Click
+ MoveTrack -1
+END SUB
+
+
+SUB mnuMoveDown_Click
+ MoveTrack 1
+END SUB
+
+
+SUB mnuRemove_Click
+ IF lstTracks.ListIndex >= 0 THEN
+ RemoveTrack lstTracks.ListIndex
+ END IF
+END SUB
+
+
+SUB mnuClear_Click
+ FreeMusic
+ ResetList
+ lstTracks.Clear
+ BuildOrder
+ ShowTrack
+END SUB
+
+
+SUB mnuExit_Click
+ Unload PlayerForm
+END SUB
+
+
+SUB mnuPlayPause_Click
+ PlayOrPause
+END SUB
+
+
+SUB mnuStop_Click
+ StopPlaying
+END SUB
+
+
+SUB mnuPrev_Click
+ PlayPrevious
+END SUB
+
+
+SUB mnuNext_Click
+ PlayNext
+END SUB
+
+
+SUB mnuRepeatOff_Click
+ ApplyRepeat
+END SUB
+
+
+SUB mnuRepeatList_Click
+ ApplyRepeat
+END SUB
+
+
+SUB mnuRepeatTrack_Click
+ ApplyRepeat
+END SUB
+
+
+SUB mnuShuffle_Click
+ BuildOrder
+END SUB
+
+
+SUB btnPrev_Click
+ PlayPrevious
+END SUB
+
+
+SUB btnPlay_Click
+ PlayOrPause
+END SUB
+
+
+SUB btnStop_Click
+ StopPlaying
+END SUB
+
+
+SUB btnNext_Click
+ PlayNext
+END SUB
+
+
+SUB lstTracks_DblClick
+ IF lstTracks.ListIndex >= 0 THEN
+ PlayTrack lstTracks.ListIndex
+ END IF
+END SUB
+
+
+' Enter plays the selected track, Del takes it out of the list.
+SUB lstTracks_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)
+ IF Shift <> 0 OR lstTracks.ListIndex < 0 THEN
+ EXIT SUB
+ END IF
+
+ IF KeyCode = vbKeyReturn THEN
+ PlayTrack lstTracks.ListIndex
+ ELSEIF KeyCode = vbKeyDelete THEN
+ RemoveTrack lstTracks.ListIndex
+ END IF
+END SUB
+
+
+SUB mnuHelpContents_Click
+ HelpView HELP_FILE
+END SUB
+
+
+SUB mnuAbout_Click
+ DIM msg AS STRING
+
+ msg = "Media Player" + CHR$(10) + CHR$(10)
+ msg = msg + "Plays music modules (MOD, S3M, XM, and IT), MIDI files, and WAV and VOC sounds." + CHR$(10) + CHR$(10)
+ msg = msg + "Copyright 2026 Scott Duensing"
+ MsgBox msg, vbOKOnly + vbInformation, "About Media Player"
+END SUB
diff --git a/src/apps/kpunch/progman/pmProps.c b/src/apps/kpunch/progman/pmProps.c
index c755a529..93bd478b 100644
--- a/src/apps/kpunch/progman/pmProps.c
+++ b/src/apps/kpunch/progman/pmProps.c
@@ -57,6 +57,7 @@ typedef struct {
WidgetT *icon;
WidgetT *hotkey;
WidgetT *minimized;
+ WidgetT *pause;
WidgetT *preview;
} PropsDlgT;
@@ -131,6 +132,7 @@ static void onBrowseProgram(WidgetT *w) {
PropsDlgT *pd = (PropsDlgT *)w->userData;
const FileFilterT filters[] = {
{ "Programs (*.app)" },
+ { SHELL_DOS_FILTER },
{ "All Files (*.*)" }
};
@@ -184,6 +186,7 @@ static bool onOk(WindowT *dlg, void *userCtx) {
it->hotkeyKey = key;
it->hotkeyMods = mods;
it->runMinimized = wgtCheckboxIsChecked(pd->minimized);
+ it->pause = wgtCheckboxIsChecked(pd->pause);
return true;
}
@@ -228,11 +231,14 @@ bool pmPropsDialog(AppContextT *ctx, const PmModelT *model, int32_t group, int32
pd.hotkey = dvxDialogAddField(dlg, "&Shortcut key:", PP_LABEL_W, PM_HOTKEY_TEXT - 1, hotkey);
WidgetT *lowRow = wgtHBox(dlg->widgetRoot);
+ WidgetT *checks = wgtVBox(lowRow);
lowRow->spacing = wgtPixels(PP_ROW_GAP);
- pd.minimized = wgtCheckbox(lowRow, "Run &minimized");
+ checks->weight = WGT_WEIGHT_FILL;
+ pd.minimized = wgtCheckbox(checks, "Run &minimized");
+ pd.pause = wgtCheckbox(checks, "Wait for a &key when a DOS program ends");
wgtCheckboxSetChecked(pd.minimized, item->runMinimized);
- pd.minimized->weight = WGT_WEIGHT_FILL;
+ wgtCheckboxSetChecked(pd.pause, item->pause);
int32_t pitch = SHELL_PROG_ICON * ctx->display.format.bytesPerPixel;
uint8_t *blank = (uint8_t *)calloc((size_t)pitch, SHELL_PROG_ICON);
diff --git a/src/apps/kpunch/progman/progman.c b/src/apps/kpunch/progman/progman.c
index 6e3d8c57..4ea17412 100644
--- a/src/apps/kpunch/progman/progman.c
+++ b/src/apps/kpunch/progman/progman.c
@@ -1184,10 +1184,17 @@ static void showSystemInfo(void) {
// minimized by the shell (shellMinimizeApp).
static void startItem(const PmItemT *item) {
PmItemT run = *item;
- int32_t id = shellRunProgram(sAc, run.program, run.args, run.folder);
- if (id > 0 && run.runMinimized) {
- shellMinimizeApp(sAc, id);
+ // A DOS program runs once the shell gets to it, waiting for a key at
+ // its end when the item says so.
+ if (shellIsDosProgram(run.program)) {
+ shellRunDos(run.program, run.args, run.folder, run.pause);
+ } else {
+ int32_t id = shellRunProgram(sAc, run.program, run.args, run.folder);
+
+ if (id > 0 && run.runMinimized) {
+ shellMinimizeApp(sAc, id);
+ }
}
updateStatusText();
diff --git a/src/apps/kpunch/progman/progman.dhs b/src/apps/kpunch/progman/progman.dhs
index 60f97639..382aca5c 100644
--- a/src/apps/kpunch/progman/progman.dhs
+++ b/src/apps/kpunch/progman/progman.dhs
@@ -132,16 +132,46 @@ Right-click an icon for the same commands.
Property What it is
-------- ----------
Name The name shown under the icon. Leave it empty to use the program's own name.
- Program The program to start. Click Browse... to pick it. It can also be a document, such as a text file: it then opens in the program set up for its type (see Running a Command).
+ Program The program to start. Click Browse... to pick it. It can be a DOS program (see DOS Programs), or a document, such as a text file: it then opens in the program set up for its type (see Running a Command).
Arguments Extra information given to the program when it starts, such as the name of a file to open.
- Working folder The folder where files named in Arguments are looked for. Click Browse... beside it to pick it. For example, with the working folder LETTERS and the argument JUNE.TXT, the program gets LETTERS\JUNE.TXT.
+ Working folder The folder where files named in Arguments are looked for. Click Browse... beside it to pick it. For example, with the working folder LETTERS and the argument JUNE.TXT, the program gets LETTERS\JUNE.TXT. A DOS program runs in this folder (empty: the folder the program is in).
Icon file A different picture for the icon: a program file (its icon is used) or a picture (BMP, PNG, JPEG, or GIF). Leave it empty for the program's own icon. The small picture beside Run minimized shows the icon.
Shortcut key Keys that start the item from any program, whichever window is in front, such as Ctrl+Alt+N. They work while the Program Manager is running (it may be minimized), but not while a menu or a dialog is open. A shortcut is Ctrl together with Alt or Shift (or both), plus a letter, a digit, or F1 to F12. Each shortcut can belong to one item only.
Run minimized Start the program with its windows made into icons.
+ Wait for a key For a DOS program: when it ends, DVX shows "Press any key to return to DVX." and waits, so you can read what the program printed.
.endtable
Items in Installed Programs have no properties to change; Properties just shows the program's name, file, and description.
+.topic progman.dos
+.title DOS Programs
+.toc 0 DOS Programs
+.index DOS Programs
+.index EXE
+.index COM
+.index BAT
+.index Batch Files
+.index Running DOS Programs
+.index Wait for a key
+
+.h1 DOS Programs
+
+Program Manager can start ordinary DOS programs too: games, editors, and other programs written for DOS, and batch files. Such a program has the whole screen to itself while it runs; when it ends, DVX comes back with the desktop as it was.
+
+.h2 Adding a DOS Program
+
+Choose File > New Item, and type the program's file (for example C:\GAMES\PINBALL.EXE) in the Program box, or click Browse... and choose DOS programs in the Filter box. A DOS program is a file ending in .EXE, .COM, or .BAT. Give the item a name, and if the program needs them, Arguments and a Working folder (the folder it runs in; empty means the folder the program is in). Turn on Wait for a key when a DOS program ends for a program that prints something and stops, so you can read it before DVX comes back. The item gets a DOS icon unless you choose an Icon file.
+
+.h2 Running It
+
+Start the item as any other. DVX shows the programs that will close first and asks whether to go on: each one closes the way it would if you closed it, asking about unsaved work, so nothing is lost. If you choose No, or keep a program open, the DOS program does not run.
+
+Then DVX steps aside: the screen changes to the DOS screen, and the program runs with the keyboard, the mouse, and the sound card to itself. When it ends, DVX comes back, and the Program Manager is there again. Programs that were closed stay closed; start them again when you need them.
+
+DVX stays in memory while the program runs, so a program that needs a lot of memory may not start (DVX says why). Such a program is best started from DOS, after leaving DVX.
+
+DOS programs can also be run from File > Run..., opened from the File Manager (double-click one), and set to open documents on the File Types page of the Control Panel.
+
.topic progman.arrange
.title Views and Arranging Icons
.toc 0 Views and Arranging
diff --git a/src/apps/kpunch/progman/userHelp.dhs b/src/apps/kpunch/progman/userHelp.dhs
index 399590ff..3d21cdd7 100644
--- a/src/apps/kpunch/progman/userHelp.dhs
+++ b/src/apps/kpunch/progman/userHelp.dhs
@@ -530,7 +530,7 @@ Programs that can print have Print... in their File menu, and programs that prin
.h2 The Print Dialog
-File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, or a file), how many copies you want, and (for a document of several pages) whether to print all of it or only some pages. Then choose Print. When the printout goes into a file, type the file's name, or click Browse... to pick it.
+File > Print... opens the Print dialog. Choose where the printout goes (a printer port such as LPT1, a serial port such as COM1, or a file), how many copies you want, and (for a document of several pages) whether to print all of it or only some pages. Then choose Print. When the printout goes into a file, type the file's name, or click Browse... to pick it.
While a printout is going to the printer, a small Printing window shows it is busy. You can keep working in other programs, and Cancel stops the printout.
@@ -544,7 +544,9 @@ File > Page Setup... chooses how text is laid out on paper: the margins, how man
.h2 Pictures
-Pictures are printed in black and white, as large as fits on the page. Printing pictures needs your printer's model to be set; with the setting for text only, a program tells you so.
+Pictures print as large as fits on the page: in color on a color printer (one with a color ribbon or cartridge), and in shades of grey on the others. Printing pictures needs your printer's model to be set (see Changing Settings); with the setting for text only, a program tells you so.
+
+Some programs draw whole pages of their own, such as reports with charts. On a PostScript printer the printer draws those pages itself, as sharp as it can print. On other printers their text prints crisper with the Printer Fonts add-on installed (see Adding and Removing Software).
.topic user.programs
.title Running Programs
@@ -558,6 +560,8 @@ Pictures are printed in black and white, as large as fits on the page. Printing
.index Whole screen
.index Program Help
.index About
+.index DOS Programs
+.index Running DOS Programs
.h1 Running Programs
@@ -571,13 +575,13 @@ To start a program file that has no icon, choose Run... from the Program Manager
.h2 Opening Documents
-A document can be opened the same way as a program: DVX starts the program set up for its kind of file (the letters after the dot in its name, such as TXT) and gives it the document, which the program opens right away. Which program opens which kind of file is set in the settings program (see Changing Settings).
+A document can be opened the same way as a program: DVX starts the program set up for its kind of file (the letters after the dot in its name, such as TXT) and gives it the document, which the program opens right away: a picture opens in the Image Viewer, and a music or sound file (MOD, S3M, XM, IT, MID, RMI, WAV, or VOC) plays in Media Player. Which program opens which kind of file is set in the settings program (see Changing Settings).
.h2 Running Several Programs
You can run many programs at the same time, each in its own window. They all keep running while you work in another one.
-Most programs can only run once at a time. If you start a program that is already running, DVX tells you so; find its window instead with Alt+Tab or the Task Manager. Some programs can be opened more than once, each copy in its own window.
+Most programs can only run once at a time. If you start a program that is already running, DVX tells you so; find its window instead with Alt+Tab or the Task Manager. A few take the document you opened instead: a music file opened while Media Player is running is added to its list and plays. Some programs can be opened more than once, each copy in its own window.
.h2 Switching Between Programs
@@ -591,6 +595,10 @@ Most programs can only run once at a time. If you start a program that is alread
Some programs, games especially, can fill the whole screen instead of a window (many use Alt+Enter to switch). Your other programs keep running while they do. Alt+Tab and Ctrl+Esc still work and bring the desktop back; when you return to the program, it fills the screen again.
+.h2 Running DOS Programs
+
+DVX can start ordinary DOS programs too (files ending in .EXE, .COM, or .BAT): from an icon in the Program Manager, from Run..., or by double-clicking one in the File Manager. Such a program has the whole screen to itself. Before it starts, DVX names the programs that will close and asks whether to go on; each one closes as it would if you closed it, asking about unsaved work. When the DOS program ends, DVX comes back with the Program Manager. The Program Manager's own help (DOS Programs) tells how to add one as an icon.
+
.h2 Closing a Program
Close the program's main window, or choose Exit from its File menu. A program that has unsaved work usually asks whether you want to save it first. See Working with Windows for all the ways to close a window.
@@ -695,6 +703,8 @@ If DVX does not start after you installed something, hold down a Shift key while
.index Themes
.index Wallpaper
.index Desktop Picture
+.index Screen Saver
+.index Screensaver
.index Mouse Settings
.index Video Mode
.index Screen Resolution
@@ -706,12 +716,14 @@ If DVX does not start after you installed something, hold down a Shift key while
.h1 Changing Settings
-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.
+The mouse, the screen colors, the desktop picture and the screen saver, 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.
+A screen saver can fill the screen with a moving picture when the computer has not been used for a while; pressing a key or moving the mouse brings your work back. Choose one, and how many minutes to wait, on the Desktop page of the settings program. There is none until you choose one.
+
.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
@@ -720,7 +732,7 @@ If a new screen resolution leaves the screen unreadable, just wait: DVX goes bac
Everything DVX and its programs save for you goes in one folder, the USER folder inside the DVX folder (for example C:\DVX\USER): your settings, each program's settings and the files it saves (such as saved games), Terminal's downloads, and DVX's log. To back up your work, copy this one folder. Installing a new version of DVX never changes it.
-The fonts, sounds, themes, and desktop pictures that come with DVX are in the SHARED folder. To add your own, put them in the folder of the same name in your USER folder: FONTS, SOUNDS, THEMES, or WPAPER. The settings program lists yours together with DVX's, and a file of yours with the same name as one of DVX's is used instead of it. Themes you save go there too.
+The fonts, sounds, themes, desktop pictures, and screen savers that come with DVX are in the SHARED folder. To add your own, put them in the folder of the same name in your USER folder: FONTS, SOUNDS, THEMES, WPAPER, or SAVERS. The settings program lists yours together with DVX's, and a file of yours with the same name as one of DVX's is used instead of it. Themes you save go there too.
To keep the USER folder somewhere else (on another disk, or one for each person who uses the computer), set DVXUSER to that folder before starting DVX, for example SET DVXUSER=D:\MYDVX in AUTOEXEC.BAT.
@@ -898,7 +910,9 @@ You can pick another resolution at any time with the settings program (see Chang
.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.
+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, and a Gravis UltraSound its ULTRASND setting, for example SET ULTRASND=220,1,1,11,5 (the card's manual or setup program gives the numbers; a Sound Blaster AWE's setting includes E620 for its music chip). A Windows Sound System card is found at its usual addresses; a sound device on the printer port (a Disney Sound Source or a Stereo-on-1) is found on its own, while a Covox Speech Thing must be chosen in the settings file (DVX.INI [audio] lptDac = covox). 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.
+
+MIDI music plays on the sound card's own recorded instruments (a Gravis UltraSound or a Sound Blaster AWE) when DVX's General MIDI SoundFont is installed (the Software program offers it) or the card's own instruments are there; otherwise on its music chip, or on DVX's own synthesizer. Music modules (MOD, S3M, XM, and IT files) play on a Gravis UltraSound's or a Sound Blaster AWE's own voices, which takes the computer little time; on other cards the computer mixes them, which takes much of a slow computer's time (the Control Panel's Sounds page can have them always mixed).
.h2 An empty floppy or CD-ROM drive
diff --git a/src/apps/kpunch/recorder/ICON32.BMP b/src/apps/kpunch/recorder/ICON32.BMP
new file mode 100644
index 00000000..2f9448d9
--- /dev/null
+++ b/src/apps/kpunch/recorder/ICON32.BMP
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a32c5333170cc9af696bc02cf29754938b249600a6866db783a77186e141533a
+size 3126
diff --git a/src/apps/kpunch/recorder/recorder.dbp b/src/apps/kpunch/recorder/recorder.dbp
new file mode 100644
index 00000000..c897f3f9
--- /dev/null
+++ b/src/apps/kpunch/recorder/recorder.dbp
@@ -0,0 +1,25 @@
+[Project]
+Name = Sound Recorder
+Author = Scott Duensing
+Publisher = Kangaroo Punch Studios
+Version = 1.00
+Copyright = Copyright 2026 Scott Duensing
+Description = Records sound from a microphone, a line input, or a CD into WAV files, and a MIDI keyboard into MIDI files
+Icon = ICON32.BMP
+
+[Modules]
+File0 = ../../../include/basic/commdlg.bas
+File1 = ../../../include/basic/fileops.bas
+File2 = ../../../include/basic/help.bas
+File3 = ../../../include/basic/music.bas
+File4 = ../../../include/basic/prefs.bas
+File5 = ../../../include/basic/sound.bas
+Count = 6
+
+[Forms]
+File0 = recorder.frm
+Count = 1
+
+[Settings]
+StartupForm = RecorderForm
+MultiInstance = False
diff --git a/src/apps/kpunch/recorder/recorder.dhs b/src/apps/kpunch/recorder/recorder.dhs
new file mode 100644
index 00000000..f3c27a28
--- /dev/null
+++ b/src/apps/kpunch/recorder/recorder.dhs
@@ -0,0 +1,87 @@
+# 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 Recorder help. Compiled to recorder.hlp next to recorder.app;
+# F1 and Help > Contents in Sound Recorder open it.
+
+.topic recorder.overview
+.title Sound Recorder
+.toc 0 Sound Recorder
+.default
+.index Sound Recorder
+.index Recording
+.index Microphone
+.index Line In
+.index WAV
+.index MIDI keyboard
+.index MIDI file
+
+.h1 Sound Recorder
+
+Sound Recorder records sound from your sound card's microphone, line, or CD input, plays it back, and saves it as a WAV file, which Media Player and other programs can play. It also records what you play on a MIDI keyboard into a MIDI file.
+
+.h2 Making a Recording
+
+.list
+.item Choose the input on the Options menu: Microphone, Line In (a tape player, a radio, or another device plugged into the sound card), or CD Audio (a music CD in the CD-ROM drive).
+.item Click Record. The time counts up and the level bar shows how loud the sound is coming in: speak or play a little louder if it barely moves, and a little softer if it stays at the end.
+.item Click Stop when you are done, then Play to hear it.
+.item Choose File > Save As... to keep it. A recording you have not saved is lost when you record again, start a new one, or close Sound Recorder; you are asked first.
+.endlist
+
+While Sound Recorder records, no other program can play sounds or music made by the sound card: most sound cards can record or play, but not both at once. Everything plays again when the recording stops.
+
+If an input cannot be chosen on the Options menu, your sound card does not have it. If Record cannot be clicked, the sound card cannot record at all.
+
+.h2 Recording a MIDI Keyboard
+
+A MIDI keyboard (or another instrument) plugged into the sound card's MIDI port can be recorded too. First set MIDI in to the MIDI port on the Sounds page of Control Panel, and start DVX again; then choose MIDI Keyboard on the Options menu (it cannot be chosen until then).
+
+Click Record and play. What you play sounds as you play it, on the computer's music synthesizer, and the level bar shows how hard you strike the keys. Click Stop when you are done, and Play to hear it again. Save As... keeps it as a MIDI file (.MID), which Media Player and other programs play with whatever instruments the computer has; it takes very little disk space. The quality chosen on the Options menu does not matter for a MIDI recording, and other programs can still play sounds while you record.
+
+.h2 Quality
+
+The Options menu also chooses how good the recording sounds, and how much disk space it takes:
+
+.table
+ Quality Sounds like Space for a minute
+ ------- ----------- ------------------
+ Telephone A telephone call; fine for speech About 470 KB
+ Radio An AM radio; good for speech and most sounds About 1.3 MB
+ CD A music CD, in stereo About 10 MB
+.endtable
+
+Your sound card may record a little differently from what you choose (an older card cannot record in stereo, for example); the line under the buttons shows what it records.
+
+.h2 Files
+
+.table
+ Command What it does
+ ------- ------------
+ New Start over with no recording.
+ Open... Open a WAV or MIDI file to play it.
+ Save Save the recording; Sound Recorder asks for its name.
+ Save As... Save the recording, or the file you opened, under a new name.
+ Exit Close Sound Recorder.
+.endtable
+
+Sound Recorder remembers the quality, the input, and the folder you last used.
diff --git a/src/apps/kpunch/recorder/recorder.frm b/src/apps/kpunch/recorder/recorder.frm
new file mode 100644
index 00000000..9b529a9e
--- /dev/null
+++ b/src/apps/kpunch/recorder/recorder.frm
@@ -0,0 +1,918 @@
+VERSION DVX 1.00
+Begin Form RecorderForm
+ Caption = "Sound Recorder"
+ Layout = VBox
+ AutoSize = False
+ Resizable = False
+ Centered = True
+ Width = 380
+ Height = 210
+ Begin Menu mnuFile
+ Caption = "&File"
+ Begin Menu mnuNew
+ Caption = "&New"
+ Shortcut = "Ctrl+N"
+ End
+ Begin Menu mnuOpen
+ Caption = "&Open..."
+ Shortcut = "Ctrl+O"
+ End
+ Begin Menu mnuSave
+ Caption = "&Save"
+ Shortcut = "Ctrl+S"
+ End
+ Begin Menu mnuSaveAs
+ Caption = "Save &As..."
+ End
+ Begin Menu mnuFileSep
+ Caption = "-"
+ End
+ Begin Menu mnuExit
+ Caption = "E&xit"
+ End
+ End
+ Begin Menu mnuOptions
+ Caption = "&Options"
+ Begin Menu mnuPhone
+ Caption = "&Telephone Quality"
+ RadioCheck = True
+ Checked = False
+ End
+ Begin Menu mnuRadio
+ Caption = "&Radio Quality"
+ RadioCheck = True
+ Checked = True
+ End
+ Begin Menu mnuCd
+ Caption = "&CD Quality"
+ RadioCheck = True
+ Checked = False
+ End
+ Begin Menu mnuOptionsSep
+ Caption = "-"
+ End
+ Begin Menu mnuInMic
+ Caption = "&Microphone"
+ RadioCheck = True
+ Checked = True
+ End
+ Begin Menu mnuInLine
+ Caption = "&Line In"
+ RadioCheck = True
+ Checked = False
+ End
+ Begin Menu mnuInCd
+ Caption = "C&D Audio"
+ RadioCheck = True
+ Checked = False
+ End
+ Begin Menu mnuInMidi
+ Caption = "MIDI &Keyboard"
+ RadioCheck = True
+ Checked = False
+ End
+ End
+ Begin Menu mnuHelp
+ Caption = "&Help"
+ Begin Menu mnuHelpContents
+ Caption = "&Contents"
+ ShortcutText = "F1"
+ End
+ Begin Menu mnuHelpSep
+ Caption = "-"
+ End
+ Begin Menu mnuAbout
+ Caption = "&About Sound Recorder..."
+ End
+ End
+ Begin Label lblFile
+ Caption = "New recording"
+ FontBold = True
+ Weight = 0
+ End
+ Begin Label lblState
+ Caption = "Ready to record"
+ Weight = 0
+ End
+ Begin HBox rowLevel
+ Weight = 0
+ Begin Label lblTime
+ Caption = "0:00.0"
+ MinWidth = 64
+ Weight = 0
+ End
+ Begin ProgressBar prgLevel
+ Weight = 1
+ ToolTipText = "How loud the sound being recorded is"
+ End
+ End
+ Begin HBox rowButtons
+ Weight = 0
+ Begin CommandButton btnRecord
+ Caption = "&Record"
+ MinWidth = 80
+ Weight = 0
+ End
+ Begin CommandButton btnStop
+ Caption = "S&top"
+ MinWidth = 80
+ Weight = 0
+ End
+ Begin CommandButton btnPlay
+ Caption = "&Play"
+ MinWidth = 80
+ Weight = 0
+ End
+ End
+ Begin Label lblFormat
+ Caption = ""
+ Weight = 0
+ End
+ Begin Timer tmrTick
+ Interval = 100
+ Enabled = False
+ End
+End
+
+' 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 Recorder: records sound from the sound card's microphone, line,
+' or CD input into a WAV file, or what is played on a MIDI keyboard into
+' a MIDI file, plays it back, and saves it.
+'
+' A recording goes into RECORDING.WAV (RECORDING.MID) in the program's
+' own user folder (App.User) until it is saved: Save As copies it where
+' the user says. Most sound cards record or play, not both, so nothing
+' else plays while it records sound. While it records MIDI, what is
+' played sounds as it comes (on the music synthesizer) and is kept in a
+' MIDI recording, written out at Stop. A timer shows the length and the
+' level while recording (faster for MIDI, so what is played is heard
+' soon) and notices the end of playing. The quality and the input are
+' kept in RECORDER.INI beside the recording.
+
+OPTION EXPLICIT
+
+CONST APP_TITLE = "Sound Recorder"
+CONST HELP_FILE = "recorder.hlp"
+CONST WAV_FILTER = "Sounds (*.wav)|All Files (*.*)"
+CONST MID_FILTER = "MIDI music (*.mid)|All Files (*.*)"
+CONST OPEN_FILTER = "Recordings (*.wav;*.mid)|All Files (*.*)"
+CONST TEMP_FILE = "RECORDING.WAV"
+CONST TEMP_MIDI = "RECORDING.MID"
+CONST WAV_EXT = ".wav"
+CONST MID_EXT = ".mid"
+
+' Settings: RECORDER.INI in App.User
+CONST SETTINGS_FILE = "RECORDER.INI"
+CONST INI_RECORDER = "Recorder"
+CONST KEY_QUALITY = "Quality"
+CONST KEY_INPUT = "Input"
+CONST KEY_FOLDER = "Folder"
+CONST QUALITY_PHONE = "telephone"
+CONST QUALITY_RADIO = "radio"
+CONST QUALITY_CD = "cd"
+CONST INPUT_MIDI = -1 ' the MIDI keyboard (the others are RECORD_* inputs)
+
+' The qualities: telephone 8 kHz 8-bit mono, radio 22 kHz 8-bit mono, CD
+' 44.1 kHz 16-bit stereo
+CONST PHONE_RATE = 8000
+CONST RADIO_RATE = 22050
+CONST CD_RATE = 44100
+CONST BITS_LOW = 8
+CONST BITS_HIGH = 16
+CONST MONO = 1
+CONST STEREO = 2
+
+CONST STATE_IDLE = 0
+CONST STATE_RECORDING = 1
+CONST STATE_PLAYING = 2
+
+CONST MS_PER_SEC = 1000
+CONST MS_PER_TENTH = 100
+CONST SEC_PER_MIN = 60
+
+' The timer: sound, and MIDI (what is played is heard this soon)
+CONST TICK_SOUND_MS = 100
+CONST TICK_MIDI_MS = 20
+
+' Recording MIDI, the level: how hard the last note was played, falling
+' away a little each tick
+CONST VELOCITY_MAX = 127
+CONST LEVEL_FALL = 4
+CONST PERCENT = 100
+CONST BYTE_MASK = 255
+CONST DATA2_SCALE = 65536 ' a packed message's second data byte (the velocity)
+
+DIM SHARED state AS INTEGER
+DIM SHARED voice AS LONG ' playing back a sound (0: nothing)
+DIM SHARED music AS LONG ' playing back MIDI music (0: nothing)
+DIM SHARED soundIsMidi AS BOOLEAN ' the recording or file shown is MIDI music
+DIM SHARED thru AS LONG ' recording MIDI: where what is played sounds (0: nowhere)
+DIM SHARED take AS LONG ' recording MIDI: the messages so far
+DIM SHARED level AS LONG ' recording MIDI: the level shown
+DIM SHARED currentFile AS STRING ' the file shown: saved or opened ("" for a recording not saved)
+DIM SHARED haveSound AS BOOLEAN ' something to play
+DIM SHARED dirty AS BOOLEAN ' a recording not saved
+DIM SHARED lengthMs AS LONG
+DIM SHARED formatText AS STRING ' what the recording is (rate, bits, channels)
+DIM SHARED lastFolder AS STRING ' where the file dialogs start
+
+
+' ============================================================
+' Window
+' ============================================================
+
+SUB RecorderForm_Load
+ LoadSettings
+ ShowInputs
+ ShowState
+END SUB
+
+
+' Recording and playing stop first; a recording not saved is asked about.
+SUB RecorderForm_QueryUnload(Cancel AS INTEGER)
+ StopAll
+ Cancel = AskSave()
+END SUB
+
+
+SUB RecorderForm_Unload
+ SaveSettings
+ DropTemp
+END SUB
+
+
+' ============================================================
+' Recording and playing
+' ============================================================
+
+' A new recording replaces the one not saved (asked first).
+SUB StartRecording
+ DIM rate AS LONG
+ DIM bits AS LONG
+ DIM channels AS LONG
+
+ IF NOT mnuInMidi.Checked AND SoundCaps("record") = False THEN
+ MsgBox "The sound card in this computer cannot record.", vbOKOnly + vbExclamation, APP_TITLE
+ EXIT SUB
+ END IF
+
+ IF AskSave() THEN
+ EXIT SUB
+ END IF
+
+ StopAll
+
+ IF mnuInMidi.Checked THEN
+ IF NOT MidiStart() THEN
+ EXIT SUB
+ END IF
+
+ formatText = "MIDI"
+ tmrTick.Interval = TICK_MIDI_MS
+ ELSE
+ QualityChosen rate, bits, channels
+
+ IF NOT RecordStart(FileOpsPathJoin$(App.User, TEMP_FILE), rate, bits, channels, InputChosen()) THEN
+ MsgBox "Sound Recorder cannot record." + CHR$(10) + CHR$(10) + SoundError$(), vbOKOnly + vbExclamation, APP_TITLE
+ EXIT SUB
+ END IF
+
+ formatText = FormatOf$(RecordRate(), RecordBits(), RecordChannels())
+ END IF
+
+ state = STATE_RECORDING
+ soundIsMidi = mnuInMidi.Checked
+ currentFile = ""
+ haveSound = False
+ dirty = False
+ lengthMs = 0
+ tmrTick.Enabled = True
+ ShowState
+END SUB
+
+
+' The MIDI input, an empty recording, and a live music for what is
+' played (none when nothing can play it: the recording goes on).
+FUNCTION MidiStart() AS BOOLEAN
+ MidiStart = False
+
+ IF NOT MidiInOpen() THEN
+ MsgBox "Sound Recorder cannot record from the MIDI keyboard." + CHR$(10) + CHR$(10) + SoundError$(), vbOKOnly + vbExclamation, APP_TITLE
+ EXIT FUNCTION
+ END IF
+
+ take = MidiRecordNew()
+
+ IF take = 0 THEN
+ MidiInClose
+ MsgBox "Sound Recorder cannot record." + CHR$(10) + CHR$(10) + MusicError$(), vbOKOnly + vbExclamation, APP_TITLE
+ EXIT FUNCTION
+ END IF
+
+ thru = MusicMidiOpen()
+ level = 0
+ MidiStart = True
+END FUNCTION
+
+
+' The MIDI recording ends: what came last is taken, what is sounding
+' stops, and the messages are written to RECORDING.MID.
+SUB MidiStop
+ MidiTake
+ MidiInClose
+
+ IF thru <> 0 THEN
+ MusicFree thru
+ thru = 0
+ END IF
+
+ lengthMs = MidiRecordLength(take)
+
+ IF MidiRecordSave(take, FileOpsPathJoin$(App.User, TEMP_MIDI)) THEN
+ haveSound = MidiRecordCount(take) > 0
+ dirty = haveSound
+ ELSE
+ MsgBox "The recording could not be written." + CHR$(10) + CHR$(10) + MusicError$(), vbOKOnly + vbExclamation, APP_TITLE
+ END IF
+
+ MidiRecordFree take
+ take = 0
+ tmrTick.Interval = TICK_SOUND_MS
+END SUB
+
+
+' The messages that came: played as they come and kept. A note's
+' velocity becomes the level, which falls away between notes.
+SUB MidiTake
+ DIM midiMsg AS LONG
+ DIM velocity AS LONG
+
+ level = level - LEVEL_FALL
+
+ IF level < 0 THEN
+ level = 0
+ END IF
+
+ midiMsg = MidiInMessage()
+
+ DO WHILE midiMsg <> MIDI_NONE
+ IF thru <> 0 THEN
+ MusicMidiSend thru, midiMsg
+ END IF
+
+ MidiRecordAdd take, MidiInTime(), midiMsg
+ velocity = (midiMsg \ DATA2_SCALE) AND BYTE_MASK
+
+ IF (midiMsg AND MIDI_KIND_MASK) = MIDI_NOTE_ON AND velocity > 0 THEN
+ level = velocity * PERCENT \ VELOCITY_MAX
+ END IF
+
+ midiMsg = MidiInMessage()
+ LOOP
+END SUB
+
+
+' Plays the recording, or the file opened.
+SUB StartPlaying
+ DIM failure AS STRING
+
+ IF NOT haveSound THEN
+ EXIT SUB
+ END IF
+
+ StopAll
+
+ IF soundIsMidi THEN
+ music = MusicLoad(SoundPath$())
+
+ IF music <> 0 THEN
+ MusicPlay music, False
+ ELSE
+ failure = MusicError$()
+ END IF
+ ELSE
+ voice = PlayWave(SoundPath$())
+
+ IF voice = 0 THEN
+ failure = SoundError$()
+ END IF
+ END IF
+
+ IF music = 0 AND voice = 0 THEN
+ MsgBox "Sound Recorder cannot play this recording." + CHR$(10) + CHR$(10) + failure, vbOKOnly + vbExclamation, APP_TITLE
+ EXIT SUB
+ END IF
+
+ state = STATE_PLAYING
+ tmrTick.Enabled = True
+ ShowState
+END SUB
+
+
+' Whether what Play started still plays.
+FUNCTION StillPlaying() AS BOOLEAN
+ IF soundIsMidi THEN
+ StillPlaying = MusicPlaying(music)
+ ELSE
+ StillPlaying = SoundPlaying(voice)
+ END IF
+END FUNCTION
+
+
+' Stop recording (the recording is kept) or playing.
+SUB StopAll
+ IF state = STATE_RECORDING AND soundIsMidi THEN
+ MidiStop
+ ELSEIF state = STATE_RECORDING THEN
+ lengthMs = RecordLength()
+
+ IF RecordStop() THEN
+ haveSound = True
+ dirty = lengthMs > 0
+ ELSE
+ MsgBox "The recording could not all be written." + CHR$(10) + CHR$(10) + SoundError$(), vbOKOnly + vbExclamation, APP_TITLE
+ END IF
+ ELSEIF state = STATE_PLAYING AND soundIsMidi THEN
+ MusicFree music
+ music = 0
+ ELSEIF state = STATE_PLAYING THEN
+ StopWave voice
+ voice = 0
+ END IF
+
+ state = STATE_IDLE
+ tmrTick.Enabled = False
+ ShowState
+END SUB
+
+
+SUB tmrTick_Timer
+ IF state = STATE_RECORDING AND soundIsMidi THEN
+ MidiTake
+ lblTime.Caption = TimeText$(MidiInClock())
+ prgLevel.Value = level
+ ELSEIF state = STATE_RECORDING THEN
+ lblTime.Caption = TimeText$(RecordLength())
+ prgLevel.Value = RecordLevel()
+ ELSEIF state = STATE_PLAYING AND NOT StillPlaying() THEN
+ voice = 0 ' it ended: the number may be another sound's by now
+ StopAll
+ END IF
+END SUB
+
+
+' ============================================================
+' Files
+' ============================================================
+
+' The recording not saved, or the file shown.
+FUNCTION SoundPath$()
+ IF currentFile <> "" THEN
+ SoundPath$ = currentFile
+ ELSEIF soundIsMidi THEN
+ SoundPath$ = FileOpsPathJoin$(App.User, TEMP_MIDI)
+ ELSE
+ SoundPath$ = FileOpsPathJoin$(App.User, TEMP_FILE)
+ END IF
+END FUNCTION
+
+
+' The recordings not saved go (the program is ending).
+SUB DropTemp
+ DropFile FileOpsPathJoin$(App.User, TEMP_FILE)
+ DropFile FileOpsPathJoin$(App.User, TEMP_MIDI)
+END SUB
+
+
+SUB DropFile(path AS STRING)
+ ON ERROR GOTO noFile
+ KILL path
+noFile:
+END SUB
+
+
+' Ask about a recording not saved: True to stay where we are (Cancel, or
+' Save As cancelled), False to go on.
+FUNCTION AskSave() AS BOOLEAN
+ DIM result AS INTEGER
+
+ AskSave = False
+
+ IF NOT dirty THEN
+ EXIT FUNCTION
+ END IF
+
+ result = DlgPromptSave(APP_TITLE)
+
+ IF result = DVX_SAVE_YES THEN
+ SaveAs
+ AskSave = dirty
+ ELSEIF result <> DVX_SAVE_NO THEN
+ AskSave = True
+ END IF
+END FUNCTION
+
+
+' Copies the recording to a WAV (MIDI) file the user names.
+SUB SaveAs
+ DIM path AS STRING
+ DIM filter AS STRING
+ DIM ext AS STRING
+
+ IF soundIsMidi THEN
+ filter = MID_FILTER
+ ext = MID_EXT
+ ELSE
+ filter = WAV_FILTER
+ ext = WAV_EXT
+ END IF
+
+ path = DlgFileSaveIn$("Save As", filter, lastFolder)
+
+ IF path = "" THEN
+ EXIT SUB
+ END IF
+
+ IF FileOpsExtension$(path) = "" THEN
+ path = path + ext
+ END IF
+
+ ON ERROR GOTO copyFailed
+ FILECOPY SoundPath$(), path
+ currentFile = path
+ lastFolder = FileOpsPathParent$(path)
+ dirty = False
+ ShowState
+ EXIT SUB
+
+copyFailed:
+ MsgBox "Sound Recorder could not save " + path + ".", vbOKOnly + vbExclamation, APP_TITLE
+END SUB
+
+
+' A WAV or MIDI file to play (its kind and length read with MusicLoad).
+SUB OpenSound(path AS STRING)
+ DIM snd AS LONG
+ DIM kind AS STRING
+
+ snd = MusicLoad(path)
+
+ IF snd = 0 THEN
+ MsgBox "Sound Recorder cannot open " + FileOpsBaseName$(path) + "." + CHR$(10) + CHR$(10) + MusicError$(), vbOKOnly + vbExclamation, APP_TITLE
+ EXIT SUB
+ END IF
+
+ kind = MusicInfo$(snd, "kind")
+ lengthMs = MusicLength(snd)
+ MusicFree snd
+
+ IF kind <> "sound" AND kind <> "midi" THEN
+ MsgBox "Sound Recorder cannot open " + FileOpsBaseName$(path) + "." + CHR$(10) + CHR$(10) + "It is not a WAV or MIDI file.", vbOKOnly + vbExclamation, APP_TITLE
+ EXIT SUB
+ END IF
+
+ soundIsMidi = kind = "midi"
+ currentFile = path
+ lastFolder = FileOpsPathParent$(path)
+ haveSound = True
+ dirty = False
+ formatText = ""
+ ShowState
+END SUB
+
+
+' ============================================================
+' Showing it
+' ============================================================
+
+' The buttons, the title, the state, and the length as they are now.
+SUB ShowState
+ DIM recordOk AS BOOLEAN
+ DIM title AS STRING
+
+ recordOk = CanRecord()
+
+ btnRecord.Enabled = recordOk AND state <> STATE_RECORDING
+ btnStop.Enabled = state <> STATE_IDLE
+ btnPlay.Enabled = haveSound AND state = STATE_IDLE
+ mnuSave.Enabled = dirty
+ mnuSaveAs.Enabled = haveSound
+
+ IF currentFile <> "" THEN
+ title = FileOpsBaseName$(currentFile)
+ ELSEIF haveSound THEN
+ title = "New recording (not saved)"
+ ELSE
+ title = "New recording"
+ END IF
+
+ lblFile.Caption = title
+ RecorderForm.Caption = title + " - " + APP_TITLE
+
+ IF state = STATE_RECORDING THEN
+ lblState.Caption = "Recording from the " + InputName$() + "..."
+ ELSEIF state = STATE_PLAYING THEN
+ lblState.Caption = "Playing"
+ ELSEIF NOT recordOk THEN
+ lblState.Caption = "This computer's sound card cannot record."
+ ELSE
+ lblState.Caption = "Stopped"
+ END IF
+
+ IF state <> STATE_RECORDING THEN
+ lblTime.Caption = TimeText$(lengthMs)
+ prgLevel.Value = 0
+ END IF
+
+ lblFormat.Caption = formatText
+END SUB
+
+
+' Inputs the computer does not have are greyed (MIDI Keyboard: no MIDI
+' input in the settings); the one chosen moves to the microphone.
+SUB ShowInputs
+ DIM sources AS LONG
+
+ sources = SoundCaps("recordsources")
+
+ mnuInMic.Enabled = (sources AND 2 ^ RECORD_MIC) <> 0
+ mnuInLine.Enabled = (sources AND 2 ^ RECORD_LINE) <> 0
+ mnuInCd.Enabled = (sources AND 2 ^ RECORD_CD) <> 0
+ mnuInMidi.Enabled = VAL(MidiPortInfo$("input")) <> 0
+
+ IF (mnuInLine.Checked AND NOT mnuInLine.Enabled) OR (mnuInCd.Checked AND NOT mnuInCd.Enabled) OR (mnuInMidi.Checked AND NOT mnuInMidi.Enabled) THEN
+ mnuInMic.Checked = True
+ mnuInLine.Checked = False
+ mnuInCd.Checked = False
+ mnuInMidi.Checked = False
+ END IF
+END SUB
+
+
+' The input chosen can record: the MIDI input is there, or the sound
+' card records.
+FUNCTION CanRecord() AS BOOLEAN
+ IF mnuInMidi.Checked THEN
+ CanRecord = mnuInMidi.Enabled
+ ELSE
+ CanRecord = SoundCaps("record") <> 0
+ END IF
+END FUNCTION
+
+
+' "22,050 Hz, 8-bit, mono"
+FUNCTION FormatOf$(rate AS LONG, bits AS LONG, channels AS LONG)
+ DIM kind AS STRING
+
+ IF channels = STEREO THEN
+ kind = "stereo"
+ ELSE
+ kind = "mono"
+ END IF
+
+ FormatOf$ = FORMAT$(rate, "#,##0") + " Hz, " + LTRIM$(STR$(bits)) + "-bit, " + kind
+END FUNCTION
+
+
+' m:ss.t
+FUNCTION TimeText$(ms AS LONG)
+ DIM secs AS LONG
+
+ secs = ms \ MS_PER_SEC
+ TimeText$ = LTRIM$(STR$(secs \ SEC_PER_MIN)) + ":" + RIGHT$("0" + LTRIM$(STR$(secs MOD SEC_PER_MIN)), 2) + "." + LTRIM$(STR$((ms MOD MS_PER_SEC) \ MS_PER_TENTH))
+END FUNCTION
+
+
+' ============================================================
+' The choices
+' ============================================================
+
+SUB QualityChosen(rate AS LONG, bits AS LONG, channels AS LONG)
+ IF mnuPhone.Checked THEN
+ rate = PHONE_RATE
+ bits = BITS_LOW
+ channels = MONO
+ ELSEIF mnuCd.Checked THEN
+ rate = CD_RATE
+ bits = BITS_HIGH
+ channels = STEREO
+ ELSE
+ rate = RADIO_RATE
+ bits = BITS_LOW
+ channels = MONO
+ END IF
+END SUB
+
+
+FUNCTION InputChosen() AS LONG
+ IF mnuInLine.Checked THEN
+ InputChosen = RECORD_LINE
+ ELSEIF mnuInCd.Checked THEN
+ InputChosen = RECORD_CD
+ ELSE
+ InputChosen = RECORD_MIC
+ END IF
+END FUNCTION
+
+
+FUNCTION InputName$()
+ IF mnuInMidi.Checked THEN
+ InputName$ = "MIDI keyboard"
+ ELSEIF mnuInLine.Checked THEN
+ InputName$ = "line input"
+ ELSEIF mnuInCd.Checked THEN
+ InputName$ = "CD"
+ ELSE
+ InputName$ = "microphone"
+ END IF
+END FUNCTION
+
+
+' ============================================================
+' Settings
+' ============================================================
+
+FUNCTION SettingsPath$()
+ SettingsPath$ = FileOpsPathJoin$(App.User, SETTINGS_FILE)
+END FUNCTION
+
+
+SUB LoadSettings
+ DIM ini AS LONG
+ DIM quality AS STRING
+ DIM inputSource AS LONG
+
+ ini = PrefsLoad(SettingsPath$())
+
+ IF ini = 0 THEN
+ EXIT SUB
+ END IF
+
+ quality = LCASE$(PrefsGet$(ini, INI_RECORDER, KEY_QUALITY, QUALITY_RADIO))
+ inputSource = PrefsGetInt(ini, INI_RECORDER, KEY_INPUT, RECORD_MIC)
+ lastFolder = PrefsGet$(ini, INI_RECORDER, KEY_FOLDER, "")
+
+ mnuPhone.Checked = quality = QUALITY_PHONE
+ mnuCd.Checked = quality = QUALITY_CD
+ mnuRadio.Checked = NOT mnuPhone.Checked AND NOT mnuCd.Checked
+ mnuInLine.Checked = inputSource = RECORD_LINE
+ mnuInCd.Checked = inputSource = RECORD_CD
+ mnuInMidi.Checked = inputSource = INPUT_MIDI
+ mnuInMic.Checked = NOT mnuInLine.Checked AND NOT mnuInCd.Checked AND NOT mnuInMidi.Checked
+ PrefsClose ini
+END SUB
+
+
+SUB SaveSettings
+ DIM ini AS LONG
+ DIM quality AS STRING
+ DIM inputSource AS LONG
+
+ ini = PrefsCreate()
+
+ IF ini = 0 THEN
+ EXIT SUB
+ END IF
+
+ quality = QUALITY_RADIO
+ inputSource = InputChosen()
+
+ IF mnuPhone.Checked THEN
+ quality = QUALITY_PHONE
+ ELSEIF mnuCd.Checked THEN
+ quality = QUALITY_CD
+ END IF
+
+ IF mnuInMidi.Checked THEN
+ inputSource = INPUT_MIDI
+ END IF
+
+ PrefsSet ini, INI_RECORDER, KEY_QUALITY, quality
+ PrefsSetInt ini, INI_RECORDER, KEY_INPUT, inputSource
+ PrefsSet ini, INI_RECORDER, KEY_FOLDER, lastFolder
+ PrefsSaveAs ini, SettingsPath$()
+ PrefsClose ini
+END SUB
+
+
+' ============================================================
+' Menus and buttons
+' ============================================================
+
+SUB mnuNew_Click
+ IF AskSave() THEN
+ EXIT SUB
+ END IF
+
+ StopAll
+ currentFile = ""
+ haveSound = False
+ dirty = False
+ lengthMs = 0
+ formatText = ""
+ ShowState
+END SUB
+
+
+SUB mnuOpen_Click
+ DIM path AS STRING
+
+ IF AskSave() THEN
+ EXIT SUB
+ END IF
+
+ path = DlgFileOpenIn$("Open", OPEN_FILTER, lastFolder)
+
+ IF path <> "" THEN
+ StopAll
+ OpenSound path
+ END IF
+END SUB
+
+
+' A recording not saved yet is saved under a name the user gives.
+SUB mnuSave_Click
+ SaveAs
+END SUB
+
+
+SUB mnuSaveAs_Click
+ SaveAs
+END SUB
+
+
+SUB mnuExit_Click
+ Unload RecorderForm
+END SUB
+
+
+SUB btnRecord_Click
+ StartRecording
+END SUB
+
+
+' The input chosen decides whether Record can be used.
+SUB mnuInMic_Click
+ ShowState
+END SUB
+
+
+SUB mnuInLine_Click
+ ShowState
+END SUB
+
+
+SUB mnuInCd_Click
+ ShowState
+END SUB
+
+
+SUB mnuInMidi_Click
+ ShowState
+END SUB
+
+
+SUB btnStop_Click
+ StopAll
+END SUB
+
+
+SUB btnPlay_Click
+ StartPlaying
+END SUB
+
+
+SUB mnuHelpContents_Click
+ HelpView HELP_FILE
+END SUB
+
+
+SUB mnuAbout_Click
+ DIM msg AS STRING
+
+ msg = APP_TITLE + CHR$(10) + CHR$(10)
+ msg = msg + "Records sound from a microphone, a line input, or a CD into WAV files, and what is played on a MIDI keyboard into MIDI files." + CHR$(10) + CHR$(10)
+ msg = msg + "Copyright 2026 Scott Duensing"
+ MsgBox msg, vbOKOnly + vbInformation, "About " + APP_TITLE
+END SUB
diff --git a/src/common.mk b/src/common.mk
index 4a9613ec..bb2ee279 100644
--- a/src/common.mk
+++ b/src/common.mk
@@ -89,6 +89,9 @@ endif
ifeq ($(origin FONT_SOURCES),undefined)
FONT_SOURCES := $(DVX_TOOLCHAINS)/fonts
endif
+ifeq ($(origin SOUNDFONT_SOURCES),undefined)
+SOUNDFONT_SOURCES := $(DVX_TOOLCHAINS)/soundfonts
+endif
ifeq ($(origin UPX),undefined)
UPX := $(DVX_TOOLCHAINS)/upx/upx
endif
@@ -136,7 +139,7 @@ PKG_MINIZ_FLAGS = -DMINIZ_NO_ARCHIVE_APIS -DMINIZ_NO_STDIO -DMINIZ_NO_TIME -DM
DVX_DRIVERS = $(notdir $(patsubst %/,%,$(wildcard drivers/*/)))
define DVX_DRIVER_RULE
-$(DRVDIR)/$1.drv: $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/$1/*.c)) | $(DRVDIR)
+$(DRVDIR)/$1.drv: $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/$1/*.c)) $(DRV_EXTRA_$1) | $(DRVDIR)
$$(DXE3GEN) -o $(DRVDIR)/$1.dxe -U $$^
mv $(DRVDIR)/$1.dxe $$@
endef
diff --git a/src/include/basic/fileops.bas b/src/include/basic/fileops.bas
index 10ce25c8..db0fea9e 100644
--- a/src/include/basic/fileops.bas
+++ b/src/include/basic/fileops.bas
@@ -73,6 +73,7 @@ CONST FILEOPS_KIND_SOUNDS = 1
CONST FILEOPS_KIND_THEMES = 2
CONST FILEOPS_KIND_WPAPER = 3
CONST FILEOPS_KIND_BANKS = 4 ' MIDI instrument banks (OP2)
+CONST FILEOPS_KIND_SAVERS = 5 ' screen savers (programs; system.bas SysSetScreenSaver)
' FileOpsDriveType kinds
CONST FILEOPS_DRIVE_FIXED = 0 ' a hard disk
diff --git a/src/include/basic/music.bas b/src/include/basic/music.bas
index eae6b703..09c3fcb6 100644
--- a/src/include/basic/music.bas
+++ b/src/include/basic/music.bas
@@ -22,19 +22,24 @@
' music.bas -- Music Library for DVX BASIC
'
-' Plays music through whatever sound hardware the computer has. Two
-' kinds of music files:
+' Plays music through whatever sound hardware the computer has. Three
+' kinds of files:
'
-' Tracker modules (MOD, S3M, XM, and IT files) play wherever the computer
-' can play sampled sound.
+' Tracker modules (MOD, S3M, XM, and IT files) play on a wavetable sound
+' card's own voices, or 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.
+' Sound files (WAV and VOC) play wherever the computer can play sampled
+' sound; a long WAV recording is read from the disk as it plays.
'
' 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).
+' and controller values 0 to 127 (middle C is note 60). What a MIDI
+' keyboard plays (MidiInOpen) can be played on it as it comes
+' (MusicMidiSend) and kept in a MIDI recording, saved as a .MID file
+' (MidiRecordNew and the rest).
'
' MusicLoad and MusicMidiOpen return a music number for the other
' functions, or 0 when the music could not be loaded (MusicError$ says
@@ -50,10 +55,49 @@ CONST MUSIC_FAILED = 0
' The drum channel
CONST MUSIC_DRUMS = 10
+' MIDI input (MidiInMessage): the kinds of message (the status AND
+' MIDI_KIND_MASK; the channel, as MusicNoteOn numbers it, is (the
+' status AND MIDI_CHANNEL_MASK) + 1), and what MidiInMessage gives when
+' no message waits
+CONST MIDI_NOTE_OFF = &H80
+CONST MIDI_NOTE_ON = &H90 ' a velocity of 0 is a note off too
+CONST MIDI_KEY_PRESSURE = &HA0
+CONST MIDI_CONTROL = &HB0
+CONST MIDI_PROGRAM = &HC0
+CONST MIDI_CHANNEL_PRESSURE = &HD0
+CONST MIDI_PITCH_BEND = &HE0
+CONST MIDI_KIND_MASK = &HF0
+CONST MIDI_CHANNEL_MASK = &H0F
+CONST MIDI_NONE = -1
+
DECLARE LIBRARY "music"
- ' Load a module or MIDI file. Nothing plays until MusicPlay.
+ ' Load a module, a MIDI file, or a WAV or VOC file. Nothing plays
+ ' until MusicPlay.
DECLARE FUNCTION MusicLoad(BYVAL fileName AS STRING) AS LONG
+ ' A MIDI music made from a music macro string (as PLAY takes it): its
+ ' notes play on the General MIDI instruments its I commands name, the
+ ' piano before any I. Nothing plays until MusicPlay.
+ DECLARE FUNCTION MusicLoadMacro(BYVAL macro AS STRING) AS LONG
+
+ ' Play a MIDI music on another instrument bank than the one Control
+ ' Panel chose: its name in the banks folders ("" = DVX's standard
+ ' bank). A SoundFont (.SF2 or .SBK) or an UltraSound's ULTRASND.INI
+ ' is for wavetable music, any other bank for FM music. Before the music first plays; MusicError$ says
+ ' when the bank is missing or damaged.
+ DECLARE SUB MusicSetBank(BYVAL music AS LONG, BYVAL bank AS STRING)
+
+ ' A Gravis UltraSound's own instruments, as MusicSetBank takes them
+ ' (the ULTRASND.INI in the folder its ULTRADIR setting names), or ""
+ ' when there are none.
+ DECLARE FUNCTION MusicPatchSet$() AS STRING
+
+ ' A Sound Blaster AWE's own General MIDI instruments, which play from
+ ' its sound ROM, as MusicSetBank takes them (SYNTHGM.SF2 or
+ ' SYNTHGM.SBK in SFBANK in the folder its SOUND setting names), or ""
+ ' when there are none.
+ DECLARE FUNCTION MusicRomBank$() AS STRING
+
' 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)
@@ -88,11 +132,12 @@ DECLARE LIBRARY "music"
DECLARE FUNCTION MusicGetVolume(BYVAL music AS LONG) AS LONG
' About the music, by name (any case): "title" (a module's own
- ' title), "output" (where it plays, for people, like "OPL3 FM
- ' synthesizer at 388h"), "kind" ("module", "midi", or "live"), "where"
- ' ("stream", "mpu401", "fm", or "software"; "" before it first plays),
- ' "paused" and "looping" (True or False as text: VAL gives the
- ' value).
+ ' title, or the name a MIDI file gives itself), "output" (where it
+ ' plays, for people, like "OPL3 FM synthesizer at 388h"), "kind"
+ ' ("module", "midi", "live", or "sound"), "where"
+ ' ("stream", "mpu401", "fm", "software", "wave", or "softwave"; ""
+ ' before it first plays), "paused" and "looping" (True or False as
+ ' text: VAL gives the value).
DECLARE FUNCTION MusicInfo$(BYVAL music AS LONG, BYVAL field AS STRING) AS STRING
' A live music for playing notes yourself (it is playing already).
@@ -113,15 +158,41 @@ DECLARE LIBRARY "music"
' 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)
+
+ ' Play a message packed as MidiInMessage gives it: what a keyboard
+ ' plays sounds as it comes (MIDI thru).
+ DECLARE SUB MusicMidiSend(BYVAL music AS LONG, BYVAL midiMsg AS LONG)
+
+ ' A MIDI recording keeps messages (packed as MidiInMessage gives
+ ' them) with their times and saves them as a MIDI file that MusicLoad
+ ' plays. MidiRecordNew returns its number (0 when there is no room);
+ ' everything your program records goes when it ends.
+ DECLARE FUNCTION MidiRecordNew() AS LONG
+
+ ' Add a message at milliseconds from the recording's start (MidiInTime
+ ' gives that when the MIDI input was opened at the start).
+ DECLARE SUB MidiRecordAdd(BYVAL recording AS LONG, BYVAL milliseconds AS LONG, BYVAL midiMsg AS LONG)
+
+ ' How many messages it has, and when the last came (milliseconds).
+ DECLARE FUNCTION MidiRecordCount(BYVAL recording AS LONG) AS LONG
+ DECLARE FUNCTION MidiRecordLength(BYVAL recording AS LONG) AS LONG
+
+ ' Save it as a MIDI file (False when it could not be written:
+ ' MusicError$ says why). A note still held at the end is let go.
+ DECLARE FUNCTION MidiRecordSave(BYVAL recording AS LONG, BYVAL fileName AS STRING) AS LONG
+
+ DECLARE SUB MidiRecordFree(BYVAL recording AS LONG)
END DECLARE
' ---- The MIDI port itself: raw MIDI bytes (system exclusive messages,
-' your own sequencer). One program at a time has the port; it is used
-' only when the settings send MIDI music to the MIDI port. ----
+' your own sequencer), and what a keyboard plays into its input. One
+' program at a time has the port, and one reads its input; it is used
+' only when the settings send MIDI music or take MIDI input there. ----
DECLARE LIBRARY "audio"
' The MIDI port by name (any case): "description" (like "MPU-401
' MIDI port at 330h", "" when there is none), "present", "owned" (a
- ' program has it) -- True or False as text.
+ ' program has it), "input" (it brings MIDI in: MidiInOpen works),
+ ' "inputowned" (a program reads it) -- True or False as text.
DECLARE FUNCTION MidiPortInfo$(BYVAL field AS STRING) AS STRING
' Take the port for this program (False when another program has it
@@ -134,4 +205,26 @@ DECLARE LIBRARY "audio"
' Give the port back.
DECLARE SUB MidiRelease()
+
+ ' Read what a keyboard or another instrument plays into the MIDI
+ ' input (True), until MidiInClose; False when there is no MIDI input
+ ' or another program reads it (SoundError$ says which).
+ DECLARE FUNCTION MidiInOpen() AS LONG
+ DECLARE SUB MidiInClose()
+
+ ' The next message that came, -1 when none waits. It is packed: the
+ ' status is MidiMsg AND 255 (the kind, MIDI_NOTE_ON and the rest, plus
+ ' the channel less one), the first data byte (MidiMsg \ 256) AND 255
+ ' (the note), the second (MidiMsg \ 65536) AND 255 (the velocity).
+ ' Running status is filled in; clock and active-sensing bytes and
+ ' system exclusive messages are left out.
+ DECLARE FUNCTION MidiInMessage() AS LONG
+
+ ' When the message MidiInMessage gave last came (milliseconds since
+ ' MidiInOpen), and the bytes lost because the program read too late.
+ DECLARE FUNCTION MidiInTime() AS LONG
+ DECLARE FUNCTION MidiInLost() AS LONG
+
+ ' Milliseconds since MidiInOpen now (on the same clock as MidiInTime).
+ DECLARE FUNCTION MidiInClock() AS LONG
END DECLARE
diff --git a/src/include/basic/pkg.bas b/src/include/basic/pkg.bas
index 9a525647..c8acb84f 100644
--- a/src/include/basic/pkg.bas
+++ b/src/include/basic/pkg.bas
@@ -41,7 +41,7 @@
CONST PKG_FIELD_ID = 0
CONST PKG_FIELD_NAME = 1
CONST PKG_FIELD_VERSION = 2 ' major.minor.patch
-CONST PKG_FIELD_KIND = 3 ' app, library, widget, font, bank, theme, wallpaper, sound, help, bundle, system
+CONST PKG_FIELD_KIND = 3 ' app, library, widget, font, bank, theme, wallpaper, sound, saver, help, bundle, system
CONST PKG_FIELD_SUMMARY = 4
CONST PKG_FIELD_PUBLISHER = 5
CONST PKG_FIELD_AUTHOR = 6
@@ -87,6 +87,10 @@ DECLARE LIBRARY "pkg"
' The installed package's files. Rows: name, size.
DECLARE FUNCTION PkgFilesRows$(BYVAL s AS LONG, BYVAL id AS STRING) AS STRING
+ ' The id of the installed package that put a file there (its name from
+ ' the DVX folder, such as "SHARED\BANKS\KPUNCH\DMXOPL3.OP2"), or "".
+ DECLARE FUNCTION PkgFileOwner$(BYVAL s AS LONG, BYVAL fileName AS STRING) AS STRING
+
' What it needs. Rows: id, the oldest version that will do, the
' version installed ("" when none).
DECLARE FUNCTION PkgDependsRows$(BYVAL s AS LONG, BYVAL id AS STRING) AS STRING
diff --git a/src/include/basic/print.bas b/src/include/basic/print.bas
index e42cd0c4..a426e3e6 100644
--- a/src/include/basic/print.bas
+++ b/src/include/basic/print.bas
@@ -54,6 +54,10 @@ DECLARE LIBRARY "print"
' True when the printer model in the settings can print pictures.
DECLARE FUNCTION PrintCanPrintPictures() AS LONG
+ ' True when the printer model in the settings prints in color (on the
+ ' others, pictures print in shades of grey).
+ DECLARE FUNCTION PrintCanPrintColor() AS LONG
+
' ---- The printer settings (what the Control Panel's Printer page
' edits). prefs is a handle from prefs.bas: PrefsLoad(PrefsDvxIni$())
' for DVX's own settings. Models and destinations are numbered from 0.
@@ -72,6 +76,16 @@ DECLARE LIBRARY "print"
' name).
DECLARE FUNCTION PrintDestIsFile(BYVAL dest AS LONG) AS LONG
+ ' True for a serial port (COM1 to COM4), which needs a speed and a
+ ' handshake (PrintSerialStore).
+ DECLARE FUNCTION PrintDestIsSerial(BYVAL dest AS LONG) AS LONG
+
+ ' The serial speeds offered (index 0 up; 0 past the last), and the
+ ' handshakes: how many, and the name of each (like "XON / XOFF").
+ DECLARE FUNCTION PrintBaudRate(BYVAL index AS LONG) AS LONG
+ DECLARE FUNCTION PrintHandshakeCount() AS LONG
+ DECLARE FUNCTION PrintHandshakeLabel$(BYVAL handshake AS LONG) AS STRING
+
' The printer model, destination, and file name in the settings of
' prefs (the usual settings when they are missing).
DECLARE FUNCTION PrintSettingModel(BYVAL prefs AS LONG) AS LONG
@@ -88,6 +102,13 @@ DECLARE LIBRARY "print"
' a number out of range.
DECLARE FUNCTION PrintSettingsStore(BYVAL prefs AS LONG, BYVAL model AS LONG, BYVAL dest AS LONG, BYVAL filePath AS STRING) AS LONG
+ ' A serial printer's speed (like 9600) and handshake number in the
+ ' settings of prefs, and storing them the same way; False for a speed
+ ' PrintBaudRate does not offer or a handshake out of range.
+ DECLARE FUNCTION PrintSettingBaud(BYVAL prefs AS LONG) AS LONG
+ DECLARE FUNCTION PrintSettingHandshake(BYVAL prefs AS LONG) AS LONG
+ DECLARE FUNCTION PrintSerialStore(BYVAL prefs AS LONG, BYVAL baud AS LONG, BYVAL handshake AS LONG) AS LONG
+
' ---- The page setup without the Page Setup dialog. Margins count
' lines (top, bottom) and characters (left, right). In the header and
' footer: &f document name, &p page, &n number of pages, &d date,
diff --git a/src/include/basic/shell.bas b/src/include/basic/shell.bas
index 067c085b..02e0d602 100644
--- a/src/include/basic/shell.bas
+++ b/src/include/basic/shell.bas
@@ -50,26 +50,43 @@ CONST SHELL_VIEW_LARGE = 0 ' large icons
CONST SHELL_VIEW_SMALL = 1 ' small icons
CONST SHELL_VIEW_LIST = 2 ' a list
+' What ShellRunProgram and ShellRunCommand return for a DOS program: it
+' runs once the calling procedure has returned (no program id)
+CONST SHELL_RUN_DOS = -1
+
DECLARE LIBRARY "dvxshell"
' ---- Starting programs and documents ----
' Start a program (.app) with arguments ("" for none) -- what it gets
- ' as COMMAND$ -- or open a document (anything else) in the program
- ' set to open its type. Returns True when it started (if it cannot
- ' start, DVX says why). The same as ShellRunProgram with no folder.
+ ' as COMMAND$ -- run a DOS program (.EXE, .COM, or .BAT: see
+ ' ShellRunDos), or open a document (anything else) in the program set
+ ' to open its type. Returns True when it started, or will (if it
+ ' cannot, DVX says why). The same as ShellRunProgram with no folder.
DECLARE FUNCTION ShellRun(BYVAL program AS STRING, BYVAL args AS STRING) AS LONG
' Start a program, or open a document in the program set to open its
' type, the way Program Manager starts an item: file names in args
' (and a document named without a folder) are looked for in folder
- ' first ("" for none). Returns the new program's id, or 0 when it did
- ' not start (DVX says why).
+ ' first ("" for none). Returns the new program's id, SHELL_RUN_DOS
+ ' for a DOS program (in folder), or 0 when it did not start (DVX says
+ ' why).
DECLARE FUNCTION ShellRunProgram(BYVAL program AS STRING, BYVAL args AS STRING, BYVAL folder AS STRING) AS LONG
' Run a command line: a program (in double quotes when its name has a
- ' blank) followed by its arguments. The new program's id, or 0.
+ ' blank) followed by its arguments. The new program's id,
+ ' SHELL_RUN_DOS, or 0.
DECLARE FUNCTION ShellRunCommand(BYVAL commandLine AS STRING) AS LONG
+ ' Run a DOS program (an .EXE, .COM, or .BAT file, or a command DOS
+ ' knows) with DVX stepped aside, and come back to the desktop when it
+ ' ends. It runs once the calling procedure has returned: DVX first
+ ' names the programs running besides the desktop -- this one too --
+ ' and asks them to close (No, or unsaved work kept, cancels the run).
+ ' args goes after the program; folder is where it runs ("": its own
+ ' folder); pause True waits for a key when it ends, so what it printed
+ ' can be read.
+ DECLARE SUB ShellRunDos(BYVAL program AS STRING, BYVAL args AS STRING, BYVAL folder AS STRING, BYVAL pause AS LONG)
+
' Show the Run dialog. history is the commands to offer, newest
' first, one per line (separated by CHR$(10)). Returns the command
' line the user chose, or "" when the dialog was cancelled; run it
@@ -111,6 +128,10 @@ DECLARE LIBRARY "dvxshell"
' True when path names a program (a .app file).
DECLARE FUNCTION ShellIsProgram(BYVAL path AS STRING) AS LONG
+ ' True when path names a DOS program (an .EXE, .COM, or .BAT file,
+ ' which ShellRun and ShellRunDos run with DVX stepped aside).
+ DECLARE FUNCTION ShellIsDosProgram(BYVAL path AS STRING) AS LONG
+
' The folder the installed programs are in (relative to DVX's folder):
' where ShellProgramScan("") looks, and where a Browse for a program
' starts (DlgFileOpenIn$).
@@ -297,14 +318,15 @@ DECLARE LIBRARY "dvxshell"
' The items of a group.
DECLARE FUNCTION ShellItemCount(BYVAL groups AS LONG, BYVAL group AS LONG) AS LONG
- ' Add an item (a name and a program, or a document opened by its file
- ' type) at the end of a group; its index, or -1.
+ ' Add an item (a name and a program, a DOS program, or a document
+ ' opened by its file type) at the end of a group; its index, or -1.
DECLARE FUNCTION ShellItemAdd(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL itemName AS STRING, BYVAL program AS STRING) AS LONG
' An item's field as text: "name", "program", "args", "folder" (where
' its file arguments are looked for), "icon" (a picture file, "" for
' the program's own), "key" (its shortcut, "" for none), "minimized"
- ' ("1" to start it minimized, else "0"), "col" and "row" (its grid
+ ' ("1" to start it minimized, else "0"), "pause" ("1": a DOS program
+ ' waits for a key when it ends, else "0"), "col" and "row" (its grid
' cell, -1 when not placed), and "description" (its tooltip).
DECLARE FUNCTION ShellItemGet$(BYVAL groups AS LONG, BYVAL group AS LONG, BYVAL item AS LONG, BYVAL field AS STRING) AS STRING
diff --git a/src/include/basic/sound.bas b/src/include/basic/sound.bas
index f563f662..0b086c3c 100644
--- a/src/include/basic/sound.bas
+++ b/src/include/basic/sound.bas
@@ -78,6 +78,12 @@ CONST SOUND_STREAM_MS_MIN = 20
CONST SOUND_STREAM_MS_DEFAULT = 250
CONST SOUND_STREAM_MS_MAX = 2000
+' The inputs a recording comes from (RecordStart); SoundCaps
+' ("recordsources") has the bit 2 ^ input set for each the card has
+CONST RECORD_MIC = 0
+CONST RECORD_LINE = 1
+CONST RECORD_CD = 2
+
' The FM synthesizer chip: the most voices it can have, its own sample
' rate (frequency numbers are based on it: hertz = fnum * FM_CHIP_HZ *
' 2 ^ block / 2 ^ 20), the frequency number and block limits, and where
@@ -157,8 +163,10 @@ DECLARE LIBRARY "audio"
' What the sound device can do, by name (any case): "dac" (plays
' recorded sound), "synth" (has a music synthesizer), "tone" (plays
- ' tones on a tone generator), "running" -- True or False; "voices"
- ' (sounds at once), "rate" (hertz), "bits", "channels" -- numbers.
+ ' tones on a tone generator), "running", "record" (records sound) --
+ ' True or False; "voices" (sounds at once), "rate" (hertz), "bits",
+ ' "channels", "recordsources" (the inputs it records from: bit
+ ' 2 ^ RECORD_... set for each) -- numbers.
DECLARE FUNCTION SoundCaps(BYVAL field AS STRING) AS LONG
' Play one of DVX's event sounds (SOUND_EVENT_...), the one the
@@ -196,6 +204,11 @@ DECLARE LIBRARY "audio"
DECLARE SUB SoundSetPitch(BYVAL voice AS LONG, BYVAL percent AS LONG)
DECLARE SUB SoundSetFrequency(BYVAL voice AS LONG, BYVAL frequency AS LONG)
+ ' Hold a sound's or a stream's voice where it is (silent, not moving
+ ' on), and let it go on from there. Tones cannot be paused.
+ DECLARE SUB SoundPause(BYVAL voice AS LONG)
+ DECLARE SUB SoundResume(BYVAL voice AS LONG)
+
' ---- Streams: sound your program makes as it goes ----
' Open a stream of rate hertz, 8 or 16 bits, 1 or 2 channels, holding
@@ -211,13 +224,52 @@ DECLARE LIBRARY "audio"
DECLARE FUNCTION SoundStreamSpace(BYVAL stream AS LONG) AS LONG
DECLARE FUNCTION SoundStreamQueued(BYVAL stream AS LONG) AS LONG
- ' The stream's voice, for SoundSetVolume, SoundSetPan, and
- ' SoundSetPitch.
+ ' The stream's voice, for SoundSetVolume, SoundSetPan, SoundSetPitch,
+ ' SoundPause, and SoundResume.
DECLARE FUNCTION SoundStreamVoice(BYVAL stream AS LONG) AS LONG
+ ' Drop what the stream holds and has not played yet (to jump to
+ ' another place in what you play: what you write next is heard at
+ ' once).
+ DECLARE SUB SoundStreamFlush(BYVAL stream AS LONG)
+
' Stop the stream and close it.
DECLARE SUB SoundStreamClose(BYVAL stream AS LONG)
+ ' ---- Recording ----
+ '
+ ' Sound from the card's microphone, line, or CD input into a WAV
+ ' file. Most sound cards record or play, not both, so while a
+ ' program records nothing plays, and one program records at a time.
+ ' SoundCaps("record") says whether the card can.
+
+ ' Start recording from input (RECORD_MIC, RECORD_LINE, RECORD_CD)
+ ' into the WAV file fileName (made, or replaced) at rate, bits (8 or
+ ' 16), and channels (1 or 2); 0 for 22,050 Hz, 16-bit, mono. The
+ ' card records at its nearest: RecordRate, RecordBits, and
+ ' RecordChannels say what it chose. True when it started; False with
+ ' SoundError$ saying why.
+ DECLARE FUNCTION RecordStart(BYVAL fileName AS STRING, BYVAL rate AS LONG, BYVAL bits AS LONG, BYVAL channels AS LONG, BYVAL inputSource AS LONG) AS LONG
+
+ ' Stop recording: the file is complete. True when it was all
+ ' written.
+ DECLARE FUNCTION RecordStop() AS LONG
+
+ ' True while the program records.
+ DECLARE FUNCTION Recording() AS LONG
+
+ ' How long the recording is so far (milliseconds), the loudest sound
+ ' since the last call (percent: a level meter), and the samples lost
+ ' because DVX was too busy to take them in time.
+ DECLARE FUNCTION RecordLength() AS LONG
+ DECLARE FUNCTION RecordLevel() AS LONG
+ DECLARE FUNCTION RecordLost() AS LONG
+
+ ' The format the card records in.
+ DECLARE FUNCTION RecordRate() AS LONG
+ DECLARE FUNCTION RecordBits() AS LONG
+ DECLARE FUNCTION RecordChannels() AS LONG
+
' ---- The FM synthesizer chip (AdLib-style instruments) ----
' The chip by name (any case): "description" (like "OPL3 FM
@@ -238,6 +290,44 @@ DECLARE LIBRARY "audio"
DECLARE FUNCTION FmRawWrite(BYVAL reg AS LONG, BYVAL value AS LONG) AS LONG
DECLARE SUB FmRawRelease()
+ ' ---- The wavetable synthesizer (sampled instruments on the card) ----
+
+ ' The synthesizer by name (any case): "description" (like "Gravis
+ ' UltraSound wavetable at 220h (26 voices, 1019 KB)", "" when there is
+ ' none), "voices", "free" (voices no program has), "memory" (bytes of
+ ' sample memory), "freememory" (the largest free block), and what it
+ ' does itself: "envelopes", "lfos", "filter", "effects", "8bit",
+ ' "rom" -- numbers and True / False as text.
+ DECLARE FUNCTION WtInfo$(BYVAL field AS STRING) AS STRING
+
+ ' Sample memory for this program: bytes of it for samples of bits (8
+ ' or 16 a sample); its address, or -1 when there is no room. WtUpload
+ ' copies 16-bit samples (signed, mono, two bytes each) there, stored
+ ' at bits (True when it worked); WtMemFree gives the memory back.
+ DECLARE FUNCTION WtMemAlloc(BYVAL bytes AS LONG, BYVAL bits AS LONG) AS LONG
+ DECLARE FUNCTION WtUpload(BYVAL address AS LONG, BYVAL pcm AS BINARY STRING, BYVAL bits AS LONG) AS LONG
+ DECLARE SUB WtMemFree(BYVAL address AS LONG)
+
+ ' Take a voice (0 when none is free) and start it on a sample in the
+ ' memory: from address, endFrame samples long, playing from startFrame
+ ' (0: its first), its loop from loopStart up to loopEnd (loopMode 0
+ ' plays once, 1 loops, 3 loops until released), stored at bits; pitch in cents (0 plays 44,100 samples a
+ ' second, 1200 an octave higher), attenuation in centibels (0 loudest,
+ ' 60 half as loud, 1440 silent), pan -100 (left) to 100 (right). True
+ ' when it started. WtSetPitch, WtSetAttenuation and WtSetPan change
+ ' it, WtRelease lets a note looping until released play out, WtStop
+ ' silences it, WtActive is True while it sounds, and WtFree gives the
+ ' voice back.
+ DECLARE FUNCTION WtAlloc() AS LONG
+ DECLARE FUNCTION WtStart(BYVAL voice AS LONG, BYVAL address AS LONG, BYVAL startFrame AS LONG, BYVAL endFrame AS LONG, BYVAL loopStart AS LONG, BYVAL loopEnd AS LONG, BYVAL loopMode AS LONG, BYVAL bits AS LONG, BYVAL pitch AS LONG, BYVAL attenuation AS LONG, BYVAL pan AS LONG) AS LONG
+ DECLARE FUNCTION WtSetPitch(BYVAL voice AS LONG, BYVAL pitch AS LONG) AS LONG
+ DECLARE FUNCTION WtSetAttenuation(BYVAL voice AS LONG, BYVAL attenuation AS LONG) AS LONG
+ DECLARE FUNCTION WtSetPan(BYVAL voice AS LONG, BYVAL pan AS LONG) AS LONG
+ DECLARE SUB WtRelease(BYVAL voice AS LONG)
+ DECLARE SUB WtStop(BYVAL voice AS LONG)
+ DECLARE FUNCTION WtActive(BYVAL voice AS LONG) AS LONG
+ DECLARE SUB WtFree(BYVAL voice AS LONG)
+
' ---- DVX's own sounds and sound settings ----
' How many events DVX plays a sound for (numbered 0 to
@@ -288,15 +378,31 @@ DECLARE LIBRARY "audio"
DECLARE FUNCTION SoundMidiCount() AS LONG
DECLARE FUNCTION SoundMidiLabel$(BYVAL index AS LONG) AS STRING
+ ' Where MIDI input can come from: how many choices, and each one's
+ ' name for people ("None", "MIDI port").
+ DECLARE FUNCTION SoundMidiInCount() AS LONG
+ DECLARE FUNCTION SoundMidiInLabel$(BYVAL index AS LONG) AS STRING
+
+ ' Where tracker modules (MOD, S3M, XM, IT) can play, the same way
+ ' ("Wavetable synthesizer when free", "Mixed in software").
+ DECLARE FUNCTION SoundModulesCount() AS LONG
+ DECLARE FUNCTION SoundModulesLabel$(BYVAL index AS LONG) AS STRING
+
' The settings' sound driver (SoundDriverAuto$ when none is set),
- ' MIDI choice (an index for SoundMidiLabel$), and master volume
- ' (percent).
+ ' MIDI choice (an index for SoundMidiLabel$), modules choice (an
+ ' index for SoundModulesLabel$), and master volume (percent).
DECLARE FUNCTION SoundSettingDriver$(BYVAL prefs AS LONG) AS STRING
DECLARE FUNCTION SoundSettingMidi(BYVAL prefs AS LONG) AS LONG
+ DECLARE FUNCTION SoundSettingModules(BYVAL prefs AS LONG) AS LONG
DECLARE FUNCTION SoundSettingVolume(BYVAL prefs AS LONG) AS LONG
- ' Store the sound driver, MIDI choice, and master volume in the
- ' settings. A new driver or MIDI choice takes effect when DVX next
- ' starts.
- DECLARE SUB SoundSettingsStore(BYVAL prefs AS LONG, BYVAL driver AS STRING, BYVAL midi AS LONG, BYVAL volume AS LONG)
+ ' Store the sound driver, MIDI choice, modules choice, and master
+ ' volume in the settings. A new driver or MIDI choice takes effect
+ ' when DVX next starts; a modules choice at the next module.
+ DECLARE SUB SoundSettingsStore(BYVAL prefs AS LONG, BYVAL driver AS STRING, BYVAL midi AS LONG, BYVAL modules AS LONG, BYVAL volume AS LONG)
+
+ ' The settings' MIDI input choice (an index for SoundMidiInLabel$),
+ ' and storing it; a new choice takes effect when DVX next starts.
+ DECLARE FUNCTION SoundSettingMidiIn(BYVAL prefs AS LONG) AS LONG
+ DECLARE SUB SoundSettingMidiInStore(BYVAL prefs AS LONG, BYVAL midiIn AS LONG)
END DECLARE
diff --git a/src/include/basic/system.bas b/src/include/basic/system.bas
index 52bfad26..0081ccf2 100644
--- a/src/include/basic/system.bas
+++ b/src/include/basic/system.bas
@@ -24,8 +24,9 @@
' system.bas -- System Settings Library for DVX BASIC
' Changes what Control Panel changes, for the whole desktop and at once:
' the screen mode, the system colors and color themes, the wallpaper,
-' text smoothing, and the mouse. Together with prefs.bas (DVX.INI), it
-' lets a program save these settings so DVX starts with them. Also reads
+' the screen saver, text smoothing, and the mouse. Together with
+' prefs.bas (DVX.INI), it lets a program save these settings so DVX
+' starts with them. Also reads
' the system font's character pictures (glyphs), the memory, the mouse
' pointer, and the Shift / Ctrl / Alt keys anywhere on the screen, moves
' the pointer, types keys into the window in front, and saves the screen
@@ -54,6 +55,12 @@ CONST SYS_VIDEO_DEFAULT_WIDTH = 640
CONST SYS_VIDEO_DEFAULT_HEIGHT = 480
CONST SYS_VIDEO_DEFAULT_BPP = 16
+' The screen saver's wait in minutes: its range and DVX's default
+' (SysSetScreenSaver)
+CONST SCREEN_SAVER_WAIT_MIN = 1
+CONST SCREEN_SAVER_WAIT_MAX = 60
+CONST SCREEN_SAVER_WAIT_DEFAULT = 10
+
DECLARE LIBRARY "dvxshell"
' ---- Screen mode ----
@@ -137,6 +144,36 @@ DECLARE LIBRARY "dvxshell"
' loaded.
DECLARE FUNCTION SysWallpaperLoad(BYVAL prefs AS LONG) AS LONG
+ ' ---- Screen saver ----
+
+ ' The screen saver: a program in the SAVERS folders that DVX starts
+ ' after the wait with no key or mouse input, and ends at the next.
+ ' Its file as SAVERS knows it ("" for none; FileOpsKindList with
+ ' FILEOPS_KIND_SAVERS lists them) and the wait in minutes.
+ DECLARE FUNCTION SysScreenSaver$() AS STRING
+ DECLARE FUNCTION SysScreenSaverWait() AS LONG
+
+ ' Choose the screen saver (a file in SAVERS, any other path, or ""
+ ' for none) and the wait in minutes (kept within
+ ' SCREEN_SAVER_WAIT_MIN and SCREEN_SAVER_WAIT_MAX). At once.
+ DECLARE SUB SysSetScreenSaver(BYVAL fileName AS STRING, BYVAL minutes AS LONG)
+
+ ' Start the screen saver now, as Control Panel's Test does. False
+ ' when none is chosen or one runs already.
+ DECLARE FUNCTION SysScreenSaverStart() AS LONG
+
+ ' True while a screen saver runs.
+ DECLARE FUNCTION SysScreenSaverRunning() AS LONG
+
+ ' Store the screen saver and its wait in [desktop] of a prefs.bas
+ ' handle, or use the ones it has (none and the default wait when it
+ ' names none).
+ DECLARE SUB SysScreenSaverStore(BYVAL prefs AS LONG)
+ DECLARE SUB SysScreenSaverLoad(BYVAL prefs AS LONG)
+
+ ' Seconds since the last key or mouse input, anywhere on the screen.
+ DECLARE FUNCTION SysIdleSeconds() AS LONG
+
' ---- Text ----
' Smooth the edges of text (True) or not (False). Text is smoothed
diff --git a/src/installer/mkDist.sh b/src/installer/mkDist.sh
index 59bb0a4c..3db17ed9 100755
--- a/src/installer/mkDist.sh
+++ b/src/installer/mkDist.sh
@@ -41,6 +41,10 @@
# files but the fonts (Setup's optional tiers),
# for the package server, where the operator
# signs it (server/README.md)
+# dist/addons/NAME.DPK DVX's own add-ons, from src/addons/*/NAME
+# (PACKAGE.INI and the files; files the build
+# makes, the fonts of one with a fonts.ini, from
+# obj/addons/NAME), for the package server too
#
# 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
@@ -182,6 +186,23 @@ tar -C "${STAGE}" --exclude="./${FONTS_DIR}" -cf - . | tar -C "${UPD_STAGE}" -xf
"${TOOL}" build "${UPD_TEMPLATE}" "${UPD_STAGE}" "${DIST}/DVXUPD.DPK" > /dev/null || fail "DVX's update package could not be built"
rm -rf "${UPD_STAGE}" "${UPD_TEMPLATE}"
+# DVX's own add-ons (src/addons/*/*/PACKAGE.INI), each from its folder
+mkdir -p "${DIST}/addons"
+
+for template in "${ROOT}"/src/addons/*/*/PACKAGE.INI; do
+ folder="$(dirname "${template}")"
+ base="$(basename "${folder}")"
+ name="$(echo "${base}" | tr '[:lower:]' '[:upper:]')"
+
+ # An add-on whose files the build makes (fonts.ini: src/tools/Makefile)
+ # is packed from obj/addons/NAME
+ if [ -d "${ROOT}/obj/addons/${base}" ]; then
+ folder="${ROOT}/obj/addons/${base}"
+ fi
+
+ "${TOOL}" build "${template}" "${folder}" "${DIST}/addons/${name}.DPK" > /dev/null || fail "the add-on ${folder} could not be built"
+done
+
rm -rf "${STAGE}"
# The SDK: bin/sdk and the references (make all writes docs/) in DVX-SDK/
@@ -197,3 +218,5 @@ echo "mkDist: CD: ${DIST}/DVX.ISO (SETUP.EXE $(stat -c %s "${DIST}/cd/SETUP.EXE"
echo "mkDist: floppies (${FLOPPY}): ${DISKS} disk(s) in ${DIST}/floppy, ${ROOM1} bytes of the package on Disk 1"
echo "mkDist: SDK: ${DIST}/sdk.zip ($(stat -c %s "${DIST}/sdk.zip") bytes)"
echo "mkDist: DVX's update: ${DIST}/DVXUPD.DPK ($(stat -c %s "${DIST}/DVXUPD.DPK") bytes)"
+addonFiles=("${DIST}/addons/"*)
+echo "mkDist: add-ons: ${addonFiles[*]##*/}"
diff --git a/src/installer/setup.c b/src/installer/setup.c
index e462af0b..16c43b66 100644
--- a/src/installer/setup.c
+++ b/src/installer/setup.c
@@ -342,7 +342,7 @@ static const SetupRgbT sShadow = { 0, 0, 0 };
static const SetupBoardT sCopyBoards[] = {
{ "Windows, your way", { "Move, size, stack, and tile windows with the mouse or keys.", "Program Manager starts everything; Alt+Tab switches.", "Themes and wallpaper make the desktop your own." }, SetupTypeHeadE, false },
{ "Build your own programs", { "DVX BASIC: draw a form, add controls, write some code.", "Press F5 to run it, then make it a program of its own.", "Forms, graphics, files, sound, and databases built in." }, SetupTypeHeadE, false },
- { "Games, sound, and music", { "Sound effects and music while you work or play.", "Joysticks and game pads, from BASIC and from C.", "Full-screen mode gives a game the whole display." }, SetupTypeHeadE, false },
+ { "Games, sound, and music", { "Media Player plays modules, MIDI, WAV, and VOC files.", "Joysticks and game pads, from BASIC and from C.", "Full-screen mode gives a game the whole display." }, SetupTypeHeadE, false },
{ "Help on every screen", { "Press F1 anywhere for help about what you see.", "The Help Viewer holds every guide, and searches them.", "" }, SetupTypeHeadE, false },
{ "Files and the world", { "File Manager copies, moves, and opens your files.", "Terminal, networking, and file transfer connect you.", "" }, SetupTypeHeadE, false }
};
diff --git a/src/libs/kpunch/audio/Makefile b/src/libs/kpunch/audio/Makefile
index 6dc26bc8..47242b90 100644
--- a/src/libs/kpunch/audio/Makefile
+++ b/src/libs/kpunch/audio/Makefile
@@ -24,7 +24,11 @@
#
# audio.lib (see audio.h) and its sound drivers: every drivers//
# directory is one driver, built from all its sources into
-# drivers/.drv beside the library, where audio.lib finds it.
+# drivers/.drv beside the library, where audio.lib finds it. The
+# ISA card code the DMA drivers share (isa/: buffers, DMA, interrupts), the
+# AD1848 codec's (codec/: Windows Sound System boards, the UltraSound
+# MAX), the GUS's GF1 code (gus/) and the synthesizers' arithmetic (math/)
+# are linked into each driver that uses them (DRV_EXTRA_).
#
# The mixer and the drivers run code at interrupt time from pinned
# memory (audioLockSpan pins from a module's first listed function to the
@@ -45,9 +49,19 @@ TARGETDIR = $(LIBSDIR)/kpunch/audio
TARGET = $(TARGETDIR)/audio.lib
DRVDIR = $(TARGETDIR)/drivers
-DRV_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/*/*.c))
+ISA_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard isa/*.c))
+CODEC_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard codec/*.c))
+GUS_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard gus/*.c))
+MATH_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard math/*.c))
+DRV_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/*/*.c)) $(ISA_OBJS) $(CODEC_OBJS) $(GUS_OBJS) $(MATH_OBJS)
DRV_TARGETS = $(patsubst %,$(DRVDIR)/%.drv,$(DVX_DRIVERS))
+DRV_EXTRA_sb = $(ISA_OBJS)
+DRV_EXTRA_wss = $(ISA_OBJS) $(CODEC_OBJS)
+DRV_EXTRA_gus = $(ISA_OBJS) $(CODEC_OBJS) $(GUS_OBJS) $(MATH_OBJS)
+DRV_EXTRA_guswt = $(GUS_OBJS) $(MATH_OBJS)
+DRV_EXTRA_awe = $(OBJDIR)/drivers/sb/sbCalc.o $(MATH_OBJS)
+
.PHONY: all clean
all: $(TARGET) $(TARGETDIR)/audio.dep $(DRV_TARGETS)
diff --git a/src/libs/kpunch/audio/audio.c b/src/libs/kpunch/audio/audio.c
index 12ff624f..ff6ca260 100644
--- a/src/libs/kpunch/audio/audio.c
+++ b/src/libs/kpunch/audio/audio.c
@@ -154,6 +154,7 @@ typedef struct {
bool quiet; // no voice since quietSince
bool suspended; // output closed for silence (the driver stays chosen)
bool fmTones; // square / triangle tones on FM voices
+ bool recording; // the card records (audioDevRecordBegin): no output, no new voice
} AudioDevT;
static const char *sErrorText[AUDIO_ERR_COUNT] = {
@@ -167,8 +168,11 @@ static const char *sErrorText[AUDIO_ERR_COUNT] = {
"Too many sounds are playing",
"No sound device could be started",
"No such sound is playing",
- "The synthesizer is being used by another program",
- "There is no such synthesizer"
+ "The synthesizer or sound device is being used by another program (or is recording)",
+ "There is no such synthesizer",
+ "There is not enough synthesizer memory",
+ "The sound device cannot record from that input",
+ "The file could not be written"
};
static AudioDevT sDev;
@@ -194,6 +198,8 @@ void audioAppEnded(int32_t appId);
int32_t audioAppEnter(int32_t appId);
AudioCtxT *audioBasicContext(void);
void audioClose(AudioCtxT *ctx);
+int32_t audioDevRecordBegin(int32_t source, const AudioDriverT **drv);
+void audioDevRecordEnd(void);
int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max);
const char *audioErrorText(int32_t err);
void audioGetInfo(AudioInfoT *info);
@@ -223,13 +229,16 @@ void audioSetVolume(AudioCtxT *ctx, int32_t volume);
void audioShutdown(void);
void audioStopAll(AudioCtxT *ctx);
void audioStreamClose(AudioStreamT *stream);
+void audioStreamFlush(AudioStreamT *stream);
int32_t audioStreamOpen(AudioCtxT *ctx, const AudioFormatT *fmt, uint32_t bufferMs, AudioFillFnT fill, void *userData, AudioStreamT **out);
uint32_t audioStreamQueued(const AudioStreamT *stream);
uint32_t audioStreamSpace(const AudioStreamT *stream);
int32_t audioStreamVoice(const AudioStreamT *stream);
uint32_t audioStreamWrite(AudioStreamT *stream, const void *pcm, uint32_t frames);
+void audioSuspend(bool suspending);
int32_t audioTone(AudioCtxT *ctx, AudioWaveE wave, uint32_t hz, int32_t volume, uint32_t ms);
bool audioVoiceIsPlaying(const AudioCtxT *ctx, int32_t voice);
+int32_t audioVoicePause(AudioCtxT *ctx, int32_t voice, bool paused);
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);
@@ -259,6 +268,7 @@ static const AudioDriverT *soundOf(const DvxDriverModT *mod);
static void stepUpdate(int32_t slot);
static void streamFill(AudioStreamT *s);
static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod);
+static bool synthTry(const AudioSynthDriverT *syn, const AudioDrvConfigT *cfg);
static int32_t voiceBegin(AudioCtxT *owner, int32_t volume, int32_t pan, uint8_t kind);
static int32_t voiceGo(int32_t slot);
@@ -334,6 +344,9 @@ void audioClose(AudioCtxT *ctx) {
audioBasForget(ctx);
}
+ audioRecCtxClosed(ctx);
+ audioMidiInCtxClosed(ctx);
+
for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
if (sVoices[i].owner == ctx) {
audioMixer.voices[i].state = MIX_FREE;
@@ -384,6 +397,7 @@ void audioClose(AudioCtxT *ctx) {
}
audioSynthCtxClosed(ctx);
+ audioWtCtxClosed(ctx);
for (AudioCtxT **cp = &sCtxs; *cp; cp = &(*cp)->next) {
if (*cp == ctx) {
@@ -400,6 +414,46 @@ void audioClose(AudioCtxT *ctx) {
}
+// The card for recording from source: its output closed (as the silence
+// timeout does) and new voices refused until audioDevRecordEnd.
+int32_t audioDevRecordBegin(int32_t source, const AudioDriverT **drv) {
+ if (!sDev.mod.desc) {
+ return AUDIO_ERR_NO_DEVICE;
+ }
+
+ *drv = soundOf(&sDev.mod);
+
+ if (!(*drv)->recordStart || !(*drv)->recordStop || !(sDev.info.recSources & (1U << source))) {
+ return AUDIO_ERR_NO_RECORD;
+ }
+
+ if (!sDev.suspended) {
+ (*drv)->close();
+ sDev.suspended = true;
+ }
+
+ sDev.recording = true;
+ return AUDIO_OK;
+}
+
+
+// The voices the recording held play on: the output opens again when
+// one is there (else at the next voice).
+void audioDevRecordEnd(void) {
+ sDev.recording = false;
+
+ for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
+ if (audioMixer.voices[i].state != MIX_FREE) {
+ if (!devResume()) {
+ dvxLog("Audio: the output did not open again after recording");
+ }
+
+ return;
+ }
+ }
+}
+
+
int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max) {
DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass);
int32_t count = 0;
@@ -440,8 +494,10 @@ void audioGetInfo(AudioInfoT *info) {
info->voiceLimit = sDev.voiceLimit;
info->running = true;
info->hasDac = sDev.info.hasDac;
- info->hasSynth = sDev.info.hasSynth || sDev.synths[AUDIO_SYNTH_FM].desc || sDev.synths[AUDIO_SYNTH_MIDI].desc;
+ info->hasSynth = sDev.info.hasSynth || sDev.synths[AUDIO_SYNTH_FM].desc || sDev.synths[AUDIO_SYNTH_MIDI].desc || sDev.synths[AUDIO_SYNTH_WAVETABLE].desc;
info->hasTone = audioMixer.hwTone != NULL;
+ info->recSources = soundOf(&sDev.mod)->recordStart ? sDev.info.recSources : 0;
+ info->canRecord = info->recSources != 0;
}
@@ -709,6 +765,7 @@ void audioPump(void) {
reclaimSlots();
devIdle();
deliverNotes();
+ audioRecIdle();
AudioStreamT *s = sStreams;
@@ -903,6 +960,19 @@ void audioStreamClose(AudioStreamT *stream) {
}
+// With interrupts off: the mixer moves the tail.
+void audioStreamFlush(AudioStreamT *stream) {
+ if (!stream) {
+ return;
+ }
+
+ uint32_t flags = audioIrqOff();
+
+ stream->ring.head = stream->ring.tail;
+ audioIrqRestore(flags);
+}
+
+
int32_t audioStreamOpen(AudioCtxT *ctx, const AudioFormatT *fmt, uint32_t bufferMs, AudioFillFnT fill, void *userData, AudioStreamT **out) {
*out = NULL;
@@ -1020,6 +1090,60 @@ uint32_t audioStreamWrite(AudioStreamT *stream, const void *pcm, uint32_t frames
}
+// DVX steps aside for a DOS program (platformDosRun): every sample and
+// tone stops, the output closes (the next voice opens it again, as after
+// a silence: devResume) and the synthesizers are closed, which silences
+// them; back from it, the synthesizers are opened again, since the program
+// may have reset or reprogrammed the card. Only the desktop runs by then,
+// and music.lib, loaded after audio.lib, has let go of what it kept on the
+// synthesizers first.
+void audioSuspend(bool suspending) {
+ if (!sDev.mod.desc) {
+ return;
+ }
+
+ if (suspending) {
+ audioRecSuspend();
+ audioMidiInSuspend();
+
+ for (AudioCtxT *ctx = sCtxs; ctx; ctx = ctx->next) {
+ audioStopAll(ctx);
+ }
+
+ if (!sDev.suspended) {
+ soundOf(&sDev.mod)->close();
+ sDev.suspended = true;
+ }
+
+ for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
+ if (sDev.synths[k].desc) {
+ synthOf(&sDev.synths[k])->close();
+ }
+ }
+
+ dvxLog("Audio: sound device let go for a DOS program");
+ return;
+ }
+
+ PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ AudioDrvConfigT cfg;
+
+ devConfig(prefs, &cfg);
+
+ for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
+ AudioSynthDrvInfoT info;
+
+ memset(&info, 0, sizeof(info));
+
+ if (sDev.synths[k].desc && !synthOf(&sDev.synths[k])->open(&cfg, &info)) {
+ dvxLog("Audio: synth driver '%s' could not open again", sDev.synths[k].desc->name);
+ }
+ }
+
+ prefsClose(prefs);
+}
+
+
int32_t audioTone(AudioCtxT *ctx, AudioWaveE wave, uint32_t hz, int32_t volume, uint32_t ms) {
if (!ctx || (wave != AUDIO_WAVE_SQUARE && wave != AUDIO_WAVE_TRIANGLE && wave != AUDIO_WAVE_NOISE) || hz < AUDIO_TONE_MIN_HZ || hz > AUDIO_TONE_MAX_HZ) {
return AUDIO_ERR_ARG;
@@ -1062,6 +1186,22 @@ bool audioVoiceIsPlaying(const AudioCtxT *ctx, int32_t voice) {
}
+int32_t audioVoicePause(AudioCtxT *ctx, int32_t voice, bool paused) {
+ int32_t slot = lookupVoice(ctx, voice);
+
+ if (slot < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ if (audioMixer.voices[slot].kind != MIX_KIND_SAMPLE && audioMixer.voices[slot].kind != MIX_KIND_STREAM) {
+ return AUDIO_ERR_ARG;
+ }
+
+ audioMixer.voices[slot].paused = paused;
+ return AUDIO_OK;
+}
+
+
int32_t audioVoiceSetFrequency(AudioCtxT *ctx, int32_t voice, uint32_t hz) {
int32_t slot = lookupVoice(ctx, voice);
@@ -1195,7 +1335,9 @@ static void devClose(void) {
static void devCloseSynths(void) {
audioFmDetach();
audioMidiDetach();
+ audioWtDetach();
audioSynthLock(false);
+ audioWtLock(false);
for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
if (sDev.synths[k].desc) {
@@ -1326,33 +1468,24 @@ static bool devOpen(void) {
// (highest priority first) that finds its hardware. Kept modules are
// taken out of mods so dvxDriversFree leaves them loaded.
static void devOpenSynths(DvxDriverModT *mods, const AudioDrvConfigT *cfg) {
- for (int32_t i = 0; i < (int32_t)arrlen(mods); i++) {
- const AudioSynthDriverT *syn = synthOf(&mods[i]);
- AudioSynthDrvInfoT info;
+ const AudioSynthDriverT *own = sDev.info.synth;
- if (!syn || sDev.synths[syn->kind].desc) {
- continue;
- }
-
- memset(&info, 0, sizeof(info));
-
- if (!syn->open(cfg, &info)) {
- dvxLog("Audio: synth driver '%s' found no device", syn->hdr.name);
- continue;
- }
-
- sDev.synths[syn->kind] = dvxDriverKeep(mods, i);
-
- if (syn->kind == AUDIO_SYNTH_FM) {
- audioFmAttach(syn, &info);
- } else {
- audioMidiAttach(syn, &info);
- }
-
- dvxLog("Audio: using '%s': %s", syn->hdr.name, info.description);
+ // The sound card's own synthesizer first; it has no module of its own
+ if (own && synthTry(own, cfg)) {
+ sDev.synths[own->kind].handle = NULL;
+ sDev.synths[own->kind].desc = &own->hdr;
+ sDev.synths[own->kind].kind = DRV_KIND_SYNTH;
}
- if (!audioSynthLock(true)) {
+ for (int32_t i = 0; i < (int32_t)arrlen(mods); i++) {
+ const AudioSynthDriverT *syn = synthOf(&mods[i]);
+
+ if (syn && !sDev.synths[syn->kind].desc && synthTry(syn, cfg)) {
+ sDev.synths[syn->kind] = dvxDriverKeep(mods, i);
+ }
+ }
+
+ if (!audioSynthLock(true) || !audioWtLock(true)) {
dvxLog("Audio: could not pin the synthesizer code in memory");
}
}
@@ -1458,7 +1591,9 @@ static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) {
const AudioSynthDriverT *syn = (const AudioSynthDriverT *)desc;
- return syn->open && syn->close && ((syn->kind == AUDIO_SYNTH_FM && syn->write) || (syn->kind == AUDIO_SYNTH_MIDI && syn->put));
+ bool wave = syn->kind == AUDIO_SYNTH_WAVETABLE && syn->upload && syn->start && syn->update && syn->release && syn->stop && syn->active;
+
+ return syn->open && syn->close && ((syn->kind == AUDIO_SYNTH_FM && syn->write) || (syn->kind == AUDIO_SYNTH_MIDI && syn->put) || wave);
}
@@ -1647,12 +1782,37 @@ static void streamFill(AudioStreamT *s) {
}
-// The synth driver of a module (NULL: none, or a sound driver).
static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod) {
return mod->kind == DRV_KIND_SYNTH ? (const AudioSynthDriverT *)mod->desc : NULL;
}
+// The synth driver of a module (NULL: none, or a sound driver).
+// Open a synthesizer and attach it as its kind; false when it found no
+// device.
+static bool synthTry(const AudioSynthDriverT *syn, const AudioDrvConfigT *cfg) {
+ AudioSynthDrvInfoT info;
+
+ memset(&info, 0, sizeof(info));
+
+ if (!syn->open(cfg, &info)) {
+ dvxLog("Audio: synth driver '%s' found no device", syn->hdr.name);
+ return false;
+ }
+
+ if (syn->kind == AUDIO_SYNTH_FM) {
+ audioFmAttach(syn, &info);
+ } else if (syn->kind == AUDIO_SYNTH_MIDI) {
+ audioMidiAttach(syn, &info, audioSettingMidiIn(cfg->prefs) != 0);
+ } else {
+ audioWtAttach(syn, &info);
+ }
+
+ dvxLog("Audio: using '%s': %s", syn->hdr.name, info.description);
+ return true;
+}
+
+
// Claim a free slot for a new voice of owner and fill in everything but
// the source; the voice limit may drop the quietest sample or tone first
// (or refuse this one when it would be the quietest). Returns the slot
@@ -1662,6 +1822,10 @@ static int32_t voiceBegin(AudioCtxT *owner, int32_t volume, int32_t pan, uint8_t
return AUDIO_ERR_ARG;
}
+ if (sDev.recording) {
+ return AUDIO_ERR_BUSY;
+ }
+
if (sDev.suspended && !devResume()) {
return AUDIO_ERR_NO_DEVICE;
}
diff --git a/src/libs/kpunch/audio/audio.dhs b/src/libs/kpunch/audio/audio.dhs
index 5d75ecf8..c6f82cc0 100644
--- a/src/libs/kpunch/audio/audio.dhs
+++ b/src/libs/kpunch/audio/audio.dhs
@@ -34,6 +34,13 @@
.index audioTone
.index audioStreamOpen
.index audioStreamWrite
+.index audioStreamFlush
+.index audioVoicePause
+.index audioWavParse
+.index audioDecode
+.index SoundPause
+.index SoundResume
+.index SoundStreamFlush
.index audioSampleLoad
.index audioGetInfo
.index AudioInfoT
@@ -49,6 +56,12 @@
.index audioFmWrite
.index audioFmRawAcquire
.index audioMidiAcquire
+.index audioMidiInOpen
+.index audioMidiInRead
+.index AudioMidiMsgT
+.index MIDI input
+.index MidiInOpen
+.index MidiInMessage
.index AudioSynthDriverT
.index OPL
.index MPU-401
@@ -63,8 +76,37 @@
.index audioDeskSetFile
.index audioSettingsStore
.index audioMidiChoices
+.index audioModulesChoices
+.index audioSettingModules
.index audioPercentToVolume
.index SoundSettingsStore
+.index SoundModulesLabel$
+.index audioWtAlloc
+.index audioWtStart
+.index audioWtUpload
+.index audioWtMemAlloc
+.index AudioWtVoiceT
+.index audioWtNoteInit
+.index AUDIO_WT_PITCH_RATE
+.index Wavetable synthesizer
+.index Windows Sound System
+.index Gravis UltraSound
+.index Sound Blaster AWE32
+.index EMU8000
+.index Covox
+.index Disney Sound Source
+.index Stereo-on-1
+.index WtInfo$
+.index WtStart
+.index audioRecOpen
+.index audioRecFile
+.index audioRecRead
+.index audioRecClose
+.index audioRecPush
+.index audioWavHeader
+.index Recording
+.index RecordStart
+.index RecordStop
.h1 Sound Library
@@ -104,7 +146,7 @@ audioSampleLoad (file), audioSampleLoadMem (memory), and audioSampleCreate (raw
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.
+audioVoiceSetVolume, audioVoiceSetPan, audioVoiceSetPitch, audioVoicePause, 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. audioVoicePause(ctx, voice, paused) holds a sample's or a stream's voice where it is (it stays playing, silent, and takes nothing from its stream) or lets it go on; a tone or a note queue gives AUDIO_ERR_ARG. 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.
@@ -133,7 +175,25 @@ 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.
+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. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.
+
+audioDrv.h also declares, for audio.lib and the libraries built on it (music.lib), audioDecode (a WAV or VOC file in memory as 16-bit frames), audioConvert (8- or 16-bit frames as 16-bit ones), and audioWavParse(head, headBytes, fileBytes, &fmt, &dataAt, &dataBytes) (a WAV file's format and where its frames are, from its first bytes: the frames are clamped to the file and whole; AUDIO_ERR_FORMAT for anything but a RIFF WAVE file, AUDIO_ERR_UNSUPPORTED, AUDIO_ERR_CORRUPT).
+
+.h2 Recording
+
+.code
+AudioFormatT fmt = { 22050, 16, 1 };
+AudioRecT *rec = NULL;
+
+if (audioRecFile(audio, "TALK.WAV", &fmt, AUDIO_REC_MIC, &rec) == AUDIO_OK) {
+ // ... later
+ audioRecClose(rec);
+}
+.endcode
+
+audioRecOpen(ctx, fmt, source, bufferMs, &rec) records from the card's microphone, line, or CD input (AUDIO_REC_MIC, AUDIO_REC_LINE, AUDIO_REC_CD; AudioInfoT.canRecord, and recSources with bit 1 << source for each the card has) into a buffer of bufferMs (0: AUDIO_REC_MS_DEFAULT, 2 seconds; AUDIO_REC_MS_MIN to AUDIO_REC_MS_MAX). fmt asks for a rate, 8 or 16 bits, and mono or stereo (0: AUDIO_RATE_DEFAULT, AUDIO_REC_BITS_DEFAULT, mono) and comes back as what the card records: its nearest rate, and its own bits and channels when it cannot do the ones asked (a card before the SB Pro records 8-bit mono). The program takes the frames with audioRecRead(rec, pcm, frames) before the buffer fills; audioRecQueued says how many wait, audioRecLost how many were dropped because the buffer was full (whole blocks, so the frames stay in order), audioRecFrames how many were taken, and audioRecPeak the loudest sample since it was last asked, in percent (a level meter). audioRecFile(ctx, path, fmt, source, &rec) records into a WAV file instead: the library writes the frames as they come (audioPump and the shell's idle time) and the file's sizes at audioRecClose; the file starts with an empty header, so one cut short still reads. Everything that can be checked (the arguments, another recording, the card and its input) is checked before the file is made, so a recording that cannot start leaves a file of that name alone. audioRecClose stops and returns AUDIO_ERR_WRITE when the file could not all be written. audioWavHeader(out, fmt, dataBytes) writes the AUDIO_WAV_HEADER_BYTES of a WAV header for programs writing their own files.
+
+The Sound Blasters (sb), Windows Sound System boards (wss), and the Gravis UltraSound (gus) record; the DACs on the printer port, the PC speaker, and null do not. Most cards record or play, not both, so recording takes the card: its output closes (as after a silence), voices already playing hold where they are, and a new voice gets AUDIO_ERR_BUSY (the desktop's sounds are skipped) until the recording ends and the held voices play on. One recording at a time for all of DVX (another gets AUDIO_ERR_BUSY). Closing the context (the program's end) and a DOS program run with DVX stepped aside (audioSuspend) end the recording.
.h2 FM Synthesizer
@@ -152,10 +212,32 @@ audioFmRawAcquire(ctx) gives one program the whole chip: every voice the others
audioFmAlloc, audioFmFree, audioFmWrite, and audioFmRawWrite may also be called from a timer callback (platformTimerAdd); music.lib plays MIDI that way.
+.h2 Wavetable Synthesizer
+
+.code
+uint32_t addr = 0;
+int32_t v = audioWtAlloc(audio); // AUDIO_ERR_NO_VOICE: all taken
+AudioWtVoiceT note;
+
+audioWtMemAlloc(audio, frames * 2, 16, &addr);
+audioWtUpload(audio, addr, pcm, frames, 16);
+audioWtNoteInit(¬e); // a plain note: no envelope, the filter open
+note.addr = addr;
+note.end = frames;
+note.bits = 16;
+audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_RATE (44,100) frames a second
+.endcode
+
+A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.
+
.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 MIDI Input
+
+With DVX.INI [audio] midiIn = mpu401 the same port also brings in what a MIDI keyboard or another instrument plays (the port is shared with MIDI music when midi = mpu401 too). One program reads it at a time: audioMidiInOpen(ctx) (AUDIO_ERR_NO_SYNTH when there is no MIDI input, AUDIO_ERR_BUSY when another program reads it), audioMidiInClose(ctx) (closing the context does it too, and a DOS program run with DVX stepped aside closes it). A timer reads the port AUDIO_MIDI_IN_HZ (1,000) times a second into a buffer of AUDIO_MIDI_IN_BYTES with the time each byte came; audioMidiInRead(ctx, &msg) gives the next whole message, oldest first (false when none waits): AudioMidiMsgT has its status (a channel message 80h..EFh, the kind and the channel, or system common F1h..F3h), data1, data2, length (1 to 3 bytes), and ms, when its first byte came in milliseconds since audioMidiInOpen. Running status is filled in; clock and active-sensing bytes are dropped and system exclusive messages skipped. audioMidiInClock(ctx) is the time now on that clock, and audioMidiInLost(ctx) the bytes dropped because the buffer was full (the program read too late). audioMidiGetInfo says whether there is MIDI input (inPresent) and whether a program reads it (inOwned). music.lib plays what comes in (musicMidiSend) and records it into MIDI files (musicMidiRecordNew); the Sound Recorder does both.
+
.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.
@@ -166,7 +248,7 @@ WAV: uncompressed PCM, 8-bit unsigned or 16-bit signed, mono or stereo, 4000 to
.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).
+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 (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). A 486's multiplies are slow, so the mixer does none whose result is known: a sound at the device's rate is copied frame for frame without interpolating, a mono one is worked out once for both sides, and a centred mono one scaled once. Measured on an emulated 486DX2-66 at 22,050 Hz, eight mono sounds at the device's rate take about 19 percent of the CPU, eight at half of it (interpolated) about 30 percent.
.h2 Settings
@@ -175,7 +257,7 @@ DVX.INI section [audio]:
.table
Key Meaning
--- -------
- driver auto (default: find the hardware), or a driver name: sb, speaker, null
+ driver auto (default: find the hardware), or a driver name: wss, sb, gus, lpt, 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
@@ -184,13 +266,21 @@ DVX.INI section [audio]:
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
+ midi where MIDI music plays (music.lib): auto (default: a wavetable synthesizer with its SoundFont, else the FM chip, else the software synthesizer), mpu401, fm, software, softwave (the SoundFont in software)
+ soundFont the SoundFont for wavetable MIDI (music.lib): a name in the banks folders or a path, or an UltraSound's ULTRASND.INI; not set, the standard one, KPUNCH\DVXGM.SF2 (Control Panel's Sounds page picks it)
+ wssPort the Windows Sound System board's port, hex (530, 604, E80, or F40; not set: each is tried)
+ wssIrq its IRQ: 7 (default), 9, 10, or 11
+ wssDma its DMA channel: 0, 1 (default), or 3
+ gusVoices voices the Gravis UltraSound runs, 14 to 32 (default 28: 22,050 Hz output; fewer, a higher rate)
+ lptDac the parallel port DAC: auto (default: a Disney Sound Source, else a Stereo-on-1), covox, disney, stereo
+ lptPort its port, hex (not set: LPT1)
+ midiIn where MIDI input comes from: none (default) or mpu401 (the MPU-401 port)
+ mpuPort the MPU-401's port, hex (default 330); the port is only looked for with midi or midiIn = 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): its name in SHARED\BANKS or the user's BANKS folder (an add-on's as VENDOR\NAME), or a path from the DVX folder; not set, the bundled one (Control Panel's Sounds page picks it)
.endtable
-A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, volume) writes all three (an empty driver stores auto, a midi index out of range stores auto). audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.
+A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingMidiIn(prefs) (an index in audioMidiInChoices, none when missing or unknown), audioSettingModules(prefs) (an index in audioModulesChoices, wavetable when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, modules, volume) writes all four (an empty driver stores auto, a midi index out of range stores auto, a modules index out of range wavetable); audioSettingMidiInStore(prefs, midiIn) writes midiIn (an index out of range stores none), which takes effect when the device next starts. audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h); audioMidiInChoices[AUDIO_MIDI_IN_CHOICE_COUNT] the midiIn choices, audioModulesChoices[AUDIO_MODULES_CHOICE_COUNT] the modules choices (wavetable: on a wavetable synthesizer's voices when it is free and the module fits there; software: always mixed in software). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.
.h2 Desktop Sounds
@@ -215,29 +305,34 @@ The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h)
.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.
+ 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). It records with the same modes' input commands (sbRecordPlan): before the Pro the microphone only, 8-bit mono up to 13,000 Hz (15,000 on DSP 2.01, high-speed above 13,000); the Pro and the 16 also the line and CD inputs (mixer 0Ch; 3Dh / 3Eh with the microphone at full on the 16), recording what they play; the mixer registers it changes are put back when it stops. 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.
+ wss A Windows Sound System board (an AD1848 codec or its kin: Microsoft's, Crystal CS4231 / CS4248, Aztech, OPTi in WSS mode) at wssPort: 8- or 16-bit, mono or stereo, the codec's nearest rate, double-buffered by auto-init DMA on wssIrq and wssDma (set through the board's configuration register; a Plug and Play board set up by its own utility must be on the ones DVX.INI names). It records the same way (the codec's capture on the same DMA channel): the line input, AUX1 (the CD on WSS boards), or the microphone with its 20 dB boost, putting back the inputs it found. Tried before the Sound Blaster, which such a board often also has.
+ gus A Gravis UltraSound, found through the ULTRASND variable (port, DMA channels, IRQs: SET ULTRASND=220,1,1,11,5): digital sound on two of its voices from a ring at the top of its memory, refilled by DMA at each half, and its wavetable synthesizer on the rest (gusVoices active in all). It records from the microphone or the line input on ULTRASND's second DMA channel (the first when the second is not given): an UltraSound MAX (board version 10 or 11) through its CS4231 codec, as a Windows Sound System board records (8- or 16-bit, mono or stereo, the codec's rates), any other board with the GF1's own sampling (8-bit, mono or stereo, 5,510 to 44,100 Hz, each half started from the terminal count of the one before, since the GF1 does not run on by itself); no emulator DVX is tested on records on a GUS, so this follows Linux's drivers. Tried after a Sound Blaster beside it, which then plays the digital sound while the GUS plays music (guswt).
+ lpt A DAC on the parallel port (lptDac, lptPort): a Covox Speech Thing (used only when lptDac = covox: it cannot be found), a Disney Sound Source (a FIFO at a fixed 7 kHz, topped up by the timer), or a Stereo-on-1. 8-bit at most 11,048 Hz; the Covox and the Stereo-on-1 take a timer interrupt for every frame.
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.
+ opl (synthesizer) The OPL2 or OPL3 FM chip of an AdLib or Sound Blaster at 388h (oplPort), found by the AdLib timer test (timer 1 must overflow at its fastest count and not at its slowest, which also tells the AdLib-compatible timers of a Gravis UltraSound, which has no FM, from a chip); 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.
+ awe (synthesizer) The EMU8000 of a Sound Blaster AWE32 or AWE64 (BLASTER's E, else A + 400h), set up as Creative's programmer's guide gives: 30 voices (two carry the FM chip's sound), its memory in 512 KB steps, envelopes, LFOs, filter, reverb and chorus in hardware, and its 1 MB General MIDI sample ROM.
+ gus, guswt (synthesizer) The Gravis UltraSound's voices playing samples from its memory, with the library running the envelopes and LFOs: gus.drv's own when it plays the digital sound too, else guswt.drv (the GUS beside another sound device, all its voices for music).
+ mpu401 (synthesizer) An MPU-401 MIDI port in UART mode at mpuPort, only with midi or midiIn = 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 waiting then is kept for get, the MIDI input's timer (64 bytes; more is dropped while nobody reads it). 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; AUDIO_DRIVERS_MAX is the most DVX's own callers ask for).
-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.
+Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Windows Sound System, Sound Blaster, Gravis UltraSound, parallel port DAC, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: the sound device's own first (the GUS's), then of each kind (FM chip, MIDI port, wavetable) the first that finds its hardware, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.
.h2 Writing a Driver
-A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
+A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). 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.
+A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM, AUDIO_SYNTH_MIDI, or AUDIO_SYNTH_WAVETABLE, .open = ..., .close = ..., then .write, .put, or the wavetable functions). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo; for a wavetable synthesizer its voices, memory, AUDIO_WT_* flags, and level, 0 when it is as loud as the digital sound or not known) 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; a MIDI driver whose port receives also has get(&byte), the next byte the port has (false when none), which the MIDI input's timer calls (mpu401.drv keeps what put finds waiting for get). A wavetable driver has upload(addr, frames, count, bits) (program time; addresses as audioWtMemAlloc gives them) and start, update, release, stop, and active for its voices (numbered from 0, as audio.lib hands them out); it plays a note as AudioWtVoiceT gives it, in its own units. write, put, and the voice functions run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver. A sound driver whose card also has a synthesizer (the GUS) points AudioDrvInfoT.synth at its own AudioSynthDriverT, which is opened before the others.
.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.
-For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingVolume(prefs), and SoundSettingsStore(prefs, driver, midi, volume) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
+For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundMidiInCount and SoundMidiInLabel$(index) (audioMidiInChoices); SoundModulesCount and SoundModulesLabel$(index) (audioModulesChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingMidiIn(prefs), SoundSettingModules(prefs), SoundSettingVolume(prefs), SoundSettingsStore(prefs, driver, midi, modules, volume), and SoundSettingMidiInStore(prefs, midiIn) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
-The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running" True / False; "voices" = voiceLimit, "rate", "bits", "channels"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions; the result is kept for SoundError$). Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamClose. FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
+The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$. Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamFlush (audioStreamFlush), SoundStreamClose. Recording, one per program (audioRecFile): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordStop (audioRecClose; True when the file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources". FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory", "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc, WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (a plain note: no envelope or LFO, the filter open; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree. The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).
diff --git a/src/libs/kpunch/audio/audio.h b/src/libs/kpunch/audio/audio.h
index 7f80e085..f7237ccc 100644
--- a/src/libs/kpunch/audio/audio.h
+++ b/src/libs/kpunch/audio/audio.h
@@ -42,6 +42,9 @@
// 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.
+// Wave- a sample-playing synthesizer (an AWE32's EMU8000, a Gravis
+// table UltraSound), when there is one: voices and sample memory
+// shared by every program (music.lib plays SoundFonts on it).
// Desktop the sounds DVX plays for its own events (starting, message
// sounds boxes, ...), chosen in DVX.INI [sounds].
//
@@ -131,11 +134,15 @@
#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
+#define AUDIO_ERR_NO_ROOM (-12) // not enough synthesizer sample memory
+#define AUDIO_ERR_NO_RECORD (-13) // the sound device cannot record (or not from that source)
+#define AUDIO_ERR_WRITE (-14) // the file could not be written
+#define AUDIO_ERR_COUNT 15 // AUDIO_OK and the fourteen errors
// Opaque handles
typedef struct AudioCtxT AudioCtxT;
typedef struct AudioNotesT AudioNotesT;
+typedef struct AudioRecT AudioRecT;
typedef struct AudioSampleT AudioSampleT;
typedef struct AudioStreamT AudioStreamT;
@@ -173,6 +180,8 @@ typedef struct {
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
+ bool canRecord; // records sound (audioRecOpen)
+ uint32_t recSources; // what it records from: bit 1 << AUDIO_REC_* for each
} AudioInfoT;
// Mixer counters since the device started (audioGetStats).
@@ -276,6 +285,11 @@ 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);
+// Hold a sample's or a stream's voice where it is (paused true: it is
+// silent and does not move on; a stream's fill is not called) and let it
+// go on (false); AUDIO_ERR_ARG for a tone or a note queue.
+int32_t audioVoicePause(AudioCtxT *ctx, int32_t voice, bool paused);
+
// True while the voice of ctx is still playing.
bool audioVoiceIsPlaying(const AudioCtxT *ctx, int32_t voice);
@@ -297,12 +311,84 @@ void audioStreamClose(AudioStreamT *stream);
uint32_t audioStreamQueued(const AudioStreamT *stream);
uint32_t audioStreamSpace(const AudioStreamT *stream);
+// Drop the frames buffered and not yet played (a seek or a stop is heard
+// at once; a fill function fills the room again). NULL is ignored.
+void audioStreamFlush(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);
+// ============================================================
+// Recording
+// ============================================================
+//
+// Sound from the card's microphone, line, or CD input. Most sound cards
+// record or play, not both, so while a program records nothing plays:
+// voices already playing hold where they are and go on afterwards, and a
+// new one is refused (AUDIO_ERR_BUSY). One recording at a time for the
+// whole of DVX (another gets AUDIO_ERR_BUSY too). Closing the context
+// (the program's end) closes its recording.
+
+// Sources (audioRecOpen; AudioInfoT.recSources has bit 1 << source set
+// for each the card has)
+#define AUDIO_REC_MIC 0
+#define AUDIO_REC_LINE 1
+#define AUDIO_REC_CD 2
+#define AUDIO_REC_SOURCES 3
+
+// A recording's buffer (milliseconds): the default and the limits
+#define AUDIO_REC_MS_DEFAULT 2000
+#define AUDIO_REC_MS_MIN 250
+#define AUDIO_REC_MS_MAX 10000
+
+// What a 0 in the format asked for means (with AUDIO_RATE_DEFAULT, mono)
+#define AUDIO_REC_BITS_DEFAULT 16
+
+// Size of the header audioWavHeader writes
+#define AUDIO_WAV_HEADER_BYTES 44
+
+// Start recording from source in fmt (rate; bits 8 or 16; channels 1 or
+// 2; 0 for AUDIO_RATE_DEFAULT, AUDIO_REC_BITS_DEFAULT, mono) into a
+// buffer of bufferMs (0: AUDIO_REC_MS_DEFAULT). fmt is set to what the
+// card records: the nearest rate it has, and its bits and channels when
+// it cannot do the ones asked. Take the frames with audioRecRead before
+// the buffer fills (audioRecLost counts what was dropped).
+int32_t audioRecOpen(AudioCtxT *ctx, AudioFormatT *fmt, int32_t source, uint32_t bufferMs, AudioRecT **out);
+
+// The same into a WAV file at path (made, or replaced): the frames are
+// written as they come (audioPump, the shell's idle time) and the file's
+// sizes at audioRecClose; audioRecRead takes nothing from it.
+int32_t audioRecFile(AudioCtxT *ctx, const char *path, AudioFormatT *fmt, int32_t source, AudioRecT **out);
+
+// Stop recording (a file is completed and closed) and give the card back
+// for playing. AUDIO_OK, or AUDIO_ERR_WRITE when a recording into a file
+// could not all be written. NULL is ignored.
+int32_t audioRecClose(AudioRecT *rec);
+
+// The recording's format, and the frames recorded so far (taken by
+// audioRecRead, or written to the file).
+void audioRecFormat(const AudioRecT *rec, AudioFormatT *fmt);
+uint32_t audioRecFrames(const AudioRecT *rec);
+
+// Frames dropped because the buffer was full; frames recorded and not
+// yet taken.
+uint32_t audioRecLost(const AudioRecT *rec);
+uint32_t audioRecQueued(const AudioRecT *rec);
+
+// The loudest sample taken since the last call, 0..100 percent of full
+// scale: a level meter.
+int32_t audioRecPeak(AudioRecT *rec);
+
+// Take up to frames frames in the recording's format; returns how many.
+uint32_t audioRecRead(AudioRecT *rec, void *pcm, uint32_t frames);
+
+// Write the AUDIO_WAV_HEADER_BYTES of a WAV file's header for dataBytes
+// of sound in fmt into out (for a program writing its own WAV files).
+void audioWavHeader(uint8_t *out, const AudioFormatT *fmt, uint32_t dataBytes);
+
// ============================================================
// Note queues
// ============================================================
@@ -428,16 +514,19 @@ 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 (an MPU-401 in UART mode, used when DVX.INI [audio]
+// midi or midiIn = mpu401) belongs to one program at a time for output
+// and one for input (it may be another). 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
+ bool inPresent; // it receives MIDI (midiIn): audioMidiInOpen works
+ bool inOwned; // a program is reading its input
} AudioMidiInfoT;
int32_t audioMidiAcquire(AudioCtxT *ctx);
@@ -445,6 +534,207 @@ void audioMidiGetInfo(AudioMidiInfoT *info);
int32_t audioMidiPut(AudioCtxT *ctx, uint8_t byte);
int32_t audioMidiRelease(AudioCtxT *ctx);
+// ============================================================
+// MIDI input
+// ============================================================
+//
+// What a keyboard or another instrument plays into the MIDI port's input
+// (DVX.INI [audio] midiIn = mpu401), as whole messages with the time each
+// came. One program reads it at a time; closing its context closes it.
+// A timer reads the port AUDIO_MIDI_IN_HZ times a second into a buffer of
+// AUDIO_MIDI_IN_BYTES bytes (a MIDI cable brings at most about 3 a
+// millisecond); the program takes the messages from it. Running status
+// is filled in, real-time bytes (clock, active sensing) are dropped, and
+// system exclusive messages are skipped.
+
+#define AUDIO_MIDI_IN_HZ 1000
+#define AUDIO_MIDI_IN_BYTES 1024
+
+// A message as BASIC passes it in a LONG (MidiInMessage, and music.lib's
+// MusicMidiSend and MidiRecordAdd): status + data1 * 256 + data2 *
+// 65536, or AUDIO_MIDI_PACKED_NONE for none.
+#define AUDIO_MIDI_PACKED_NONE (-1)
+#define AUDIO_MIDI_PACKED_DATA1_SHIFT 8
+#define AUDIO_MIDI_PACKED_DATA2_SHIFT 16
+
+// A MIDI message (audioMidiInRead).
+typedef struct {
+ uint32_t ms; // when it came: milliseconds since audioMidiInOpen
+ uint8_t status; // 80h..EFh a channel message (kind | channel), F1h..F3h system common
+ uint8_t data1;
+ uint8_t data2;
+ uint8_t length; // status and data bytes: 1 to 3
+} AudioMidiMsgT;
+
+// Stop reading the MIDI input for ctx (nothing when ctx does not read
+// it); start reading it: AUDIO_ERR_NO_SYNTH when there is none (midiIn
+// not set, or no port), AUDIO_ERR_BUSY when another program reads it.
+void audioMidiInClose(AudioCtxT *ctx);
+int32_t audioMidiInOpen(AudioCtxT *ctx);
+
+// The next message that came, oldest first; false when none waits (or
+// ctx is not reading).
+bool audioMidiInRead(AudioCtxT *ctx, AudioMidiMsgT *msg);
+
+// Bytes dropped because the buffer was full (the program read too
+// late).
+uint32_t audioMidiInLost(const AudioCtxT *ctx);
+
+// Milliseconds since audioMidiInOpen now, on the clock the messages' times
+// are on (0 when ctx is not reading).
+uint32_t audioMidiInClock(const AudioCtxT *ctx);
+
+// ============================================================
+// Wavetable synthesizer
+// ============================================================
+//
+// A synthesizer that plays samples from its own memory: its voices and
+// its sample memory are shared by every program. A program takes memory
+// (audioWtMemAlloc), uploads samples into it (audioWtUpload), takes a
+// voice (audioWtAlloc) and starts it on a sample (audioWtStart) with
+// everything the note needs, in SoundFont units (AudioWtVoiceT). The
+// chip does what its flags say it can (AUDIO_WT_*: envelopes, LFOs, the
+// filter, the effects); the program does the rest, by changing the
+// voice's pitch and volume as the note goes on (audioWtUpdate) and
+// stopping it when its own envelope has ended. Closing the context gives
+// back its voices and its memory.
+//
+// audioWtAlloc, audioWtFree, audioWtStart, audioWtUpdate,
+// audioWtRelease, audioWtStop and audioWtActive may also be called from a
+// timer callback, as music playback does; the memory calls may not.
+
+// What a wavetable synthesizer does itself (AudioWtInfoT flags)
+#define AUDIO_WT_ENVELOPES 0x01 // the volume and modulation envelopes
+#define AUDIO_WT_LFOS 0x02 // the modulation and vibrato LFOs
+#define AUDIO_WT_FILTER 0x04 // a resonant low-pass filter
+#define AUDIO_WT_EFFECTS 0x08 // reverb and chorus sends
+#define AUDIO_WT_8BIT 0x10 // plays 8-bit samples (each frame one byte of memory)
+#define AUDIO_WT_BANK_256K 0x20 // a 16-bit sample must not cross a 256 KB boundary of the memory
+#define AUDIO_WT_ROM 0x40 // a General MIDI sample ROM (a SoundFont's ROM samples play from it)
+
+// How a sample loops (AudioWtVoiceT loop)
+#define AUDIO_WT_LOOP_NONE 0 // plays once
+#define AUDIO_WT_LOOP_ON 1 // loops for as long as it sounds
+#define AUDIO_WT_LOOP_RELEASE 3 // loops while the key is down, then plays out
+
+// Envelope stages (AudioWtVoiceT volEnv / modEnv)
+#define AUDIO_WT_ENV_DELAY 0
+#define AUDIO_WT_ENV_ATTACK 1
+#define AUDIO_WT_ENV_HOLD 2
+#define AUDIO_WT_ENV_DECAY 3
+#define AUDIO_WT_ENV_SUSTAIN 4
+#define AUDIO_WT_ENV_RELEASE 5
+#define AUDIO_WT_ENV_STAGES 6
+
+// What audioWtUpdate changes
+#define AUDIO_WT_SET_PITCH 0x01
+#define AUDIO_WT_SET_VOLUME 0x02
+#define AUDIO_WT_SET_PAN 0x04
+#define AUDIO_WT_SET_FILTER 0x08
+#define AUDIO_WT_SET_EFFECTS 0x10
+#define AUDIO_WT_SET_LFO 0x20 // the LFOs' depths
+
+// Units: pitch in cents (0 plays the sample at AUDIO_WT_PITCH_RATE
+// frames a second, 1200 an octave higher); loudness as attenuation in
+// centibels (0 full, AUDIO_WT_ATTEN_MAX silent); times in timecents (1200
+// log2 seconds); LFO rates in absolute cents (8.176 Hz x 2^(cents /
+// 1200)).
+#define AUDIO_WT_PITCH_RATE 44100
+#define AUDIO_WT_ATTEN_MAX 1440
+#define AUDIO_WT_VOICES_MAX 32 // the most voices a synthesizer gives
+#define AUDIO_WT_PAN_MAX 500 // -500 left .. 500 right
+
+// A note on a voice. The sample is in the synthesizer's memory from
+// addr (bytes), or in its ROM with AUDIO_WT_ADDR_ROM (bytes from the
+// ROM's start); its frames from there: the note starts offset in (before
+// the loop or inside it), the loop loopStart to loopEnd - 1, the end one
+// past the last. The envelopes and LFOs are used where the chip has them
+// (AUDIO_WT_ENVELOPES, _LFOS).
+#define AUDIO_WT_ADDR_ROM 0x80000000U
+
+// A plain note's values (audioWtNoteInit): the shortest time (an
+// envelope stage or an LFO delay that is not there), the filter open
+#define AUDIO_WT_TC_NONE (-12000)
+#define AUDIO_WT_FC_OPEN 13500
+
+typedef struct {
+ uint32_t addr;
+ uint32_t offset;
+ uint32_t loopStart;
+ uint32_t loopEnd;
+ uint32_t end;
+ int32_t pitch; // cents
+ int32_t attenuation; // centibels
+ int32_t pan; // -AUDIO_WT_PAN_MAX .. AUDIO_WT_PAN_MAX
+ int32_t filterFc; // absolute cents (13500: open)
+ int32_t filterQ; // centibels
+ int32_t reverb; // tenths of a percent
+ int32_t chorus;
+ int16_t volEnv[AUDIO_WT_ENV_STAGES]; // timecents; sustain in centibels below the peak
+ int16_t modEnv[AUDIO_WT_ENV_STAGES]; // timecents; sustain in tenths of a percent below the peak
+ int16_t modEnvToPitch; // cents
+ int16_t modEnvToFilter; // cents
+ int16_t modLfoDelay; // timecents
+ int16_t modLfoFreq; // absolute cents
+ int16_t modLfoToPitch; // cents
+ int16_t modLfoToFilter; // cents
+ int16_t modLfoToVolume; // centibels
+ int16_t vibLfoDelay; // timecents
+ int16_t vibLfoFreq; // absolute cents
+ int16_t vibLfoToPitch; // cents
+ uint8_t bits; // 8 or 16
+ uint8_t loop; // AUDIO_WT_LOOP_*
+} AudioWtVoiceT;
+
+// The wavetable synthesizer (audioWtGetInfo).
+typedef struct {
+ char description[AUDIO_DESC_MAX]; // e.g. "AWE32 EMU8000 with 512 KB"; "" when none
+ uint32_t voices;
+ uint32_t freeVoices;
+ uint32_t memory; // bytes of sample memory
+ uint32_t freeMemory; // the largest free block
+ uint32_t flags; // AUDIO_WT_*
+ int32_t level; // centibels a voice at full volume is louder than full-scale digital sound (measured; 0: as loud, or not known)
+ bool present;
+} AudioWtInfoT;
+
+void audioWtGetInfo(AudioWtInfoT *info);
+
+// v made a plain note: no envelope or LFO shapes it (the shortest times,
+// full sustain, no depths), the filter open, no effects, centred, at full
+// volume, at AUDIO_WT_PITCH_RATE; the caller sets the sample (addr,
+// offset, loop, end, bits) and anything else it wants.
+void audioWtNoteInit(AudioWtVoiceT *v);
+
+// Take a free voice for ctx (its id, > 0), the least recently freed
+// first; AUDIO_ERR_NO_VOICE when all are taken, AUDIO_ERR_NO_SYNTH
+// without the synthesizer. Free stops it and gives it back.
+int32_t audioWtAlloc(AudioCtxT *ctx);
+int32_t audioWtFree(const AudioCtxT *ctx, int32_t voice);
+
+// audioWtStart starts the voice on a note; Update changes it (what:
+// AUDIO_WT_SET_*); Release lets the key up (the chip's envelopes release,
+// a sample that loops while the key is down plays out); Stop stops it at
+// once. audioWtActive is false once the voice has fallen silent (its
+// envelope ended, or its sample did).
+bool audioWtActive(const AudioCtxT *ctx, int32_t voice);
+int32_t audioWtRelease(const AudioCtxT *ctx, int32_t voice);
+int32_t audioWtStart(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v);
+int32_t audioWtStop(const AudioCtxT *ctx, int32_t voice);
+int32_t audioWtUpdate(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v, uint32_t what);
+
+// bytes of sample memory for ctx at *addr, for samples of bits bits (a
+// 16-bit one kept inside a 256 KB boundary where the chip needs it);
+// AUDIO_ERR_NO_ROOM when no free block is large enough. Free gives it
+// back.
+int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t *addr);
+int32_t audioWtMemFree(AudioCtxT *ctx, uint32_t addr);
+
+// count frames into ctx's memory at addr, stored as bits-bit samples
+// (from 16-bit ones). AUDIO_ERR_ARG when they do not lie in a block of
+// ctx's.
+int32_t audioWtUpload(AudioCtxT *ctx, uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
+
// ============================================================
// Desktop sounds
// ============================================================
@@ -498,9 +788,19 @@ int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max);
// [audio] midi: where MIDI music plays, in dropdown order (label for
// people, iniName stored; value unused). Default AUDIO_MIDI_AUTO.
-#define AUDIO_MIDI_CHOICE_COUNT 4
+#define AUDIO_MIDI_CHOICE_COUNT 5
extern const PrefsChoiceT audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT];
+// [audio] midiIn: where MIDI input comes from, the same way. Default
+// AUDIO_MIDI_IN_NONE.
+#define AUDIO_MIDI_IN_CHOICE_COUNT 2
+extern const PrefsChoiceT audioMidiInChoices[AUDIO_MIDI_IN_CHOICE_COUNT];
+
+// [audio] modules: where tracker modules play, the same way. Default
+// AUDIO_MODULES_WAVETABLE.
+#define AUDIO_MODULES_CHOICE_COUNT 2
+extern const PrefsChoiceT audioModulesChoices[AUDIO_MODULES_CHOICE_COUNT];
+
// The sound file of an event in [sounds] ("" for none or an event out of
// range), and setting it ("" or NULL: none).
const char *audioDeskFile(PrefsHandleT *prefs, DvxSoundE event);
@@ -510,15 +810,25 @@ void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *p
int32_t audioPercentToVolume(int32_t percent);
// [audio] driver (AUDIO_DRIVER_AUTO when missing), midi (the index in
-// audioMidiChoices; auto when missing or unknown) and volume (percent,
-// 0..100, 100 when missing).
+// audioMidiChoices; auto when missing or unknown), midiIn (the index in
+// audioMidiInChoices; none when missing or unknown), modules (the index
+// in audioModulesChoices; wavetable when missing or unknown), and volume
+// (percent, 0..100, 100 when missing).
const char *audioSettingDriver(PrefsHandleT *prefs);
int32_t audioSettingMidi(PrefsHandleT *prefs);
+int32_t audioSettingMidiIn(PrefsHandleT *prefs);
+int32_t audioSettingModules(PrefsHandleT *prefs);
int32_t audioSettingVolume(PrefsHandleT *prefs);
// Store [audio] driver, midi (an audioMidiChoices index; out of range:
-// auto) and volume (percent, clamped).
-void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t volume);
+// auto), modules (an audioModulesChoices index; out of range: wavetable)
+// and volume (percent, clamped).
+void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume);
+
+// [audio] midiIn (an audioMidiInChoices index, the index read by
+// audioSettingMidiIn; out of range: none). Takes effect when the sound
+// device next starts (DVX's next start).
+void audioSettingMidiInStore(PrefsHandleT *prefs, int32_t midiIn);
// ============================================================
// BASIC (src/include/basic/sound.bas)
@@ -528,9 +838,12 @@ void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, i
// and pitches are in percent (volume 0..100, pan -100..100, pitch
// 25..400); a voice, sound or stream number is 0 on failure (SoundError$
// says why). Sounds and streams are numbered per program and freed with
-// its context. SoundCreate and SoundStreamWrite take BINARY STRING PCM
-// (bytes and byte count). The Midi... functions are declared in
-// music.bas.
+// its context. SoundCreate, SoundStreamWrite and WtUpload take BINARY
+// STRING PCM (bytes and byte count). The Wt... functions are the
+// wavetable synthesizer's in its own units (cents, centibels; pan
+// -100..100; WtMemAlloc -1 for no room), each started note plain (no
+// envelope or LFO, the filter open). The Midi... functions are declared
+// in music.bas.
int32_t FmAlloc(void);
void FmFree(int32_t voice);
const char *FmInfo(const char *field);
@@ -539,6 +852,12 @@ void FmRawRelease(void);
int32_t FmRawWrite(int32_t reg, int32_t value);
int32_t FmWrite(int32_t voice, int32_t reg, int32_t value);
int32_t MidiAcquire(void);
+int32_t MidiInClock(void);
+void MidiInClose(void);
+int32_t MidiInLost(void);
+int32_t MidiInMessage(void);
+int32_t MidiInOpen(void);
+int32_t MidiInTime(void);
const char *MidiPortInfo(const char *field);
int32_t MidiPut(int32_t byteValue);
void MidiRelease(void);
@@ -547,6 +866,15 @@ int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume);
int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume);
int32_t PlayWave(const char *fileName);
int32_t PlayWaveLoop(const char *fileName);
+int32_t RecordBits(void);
+int32_t RecordChannels(void);
+int32_t Recording(void);
+int32_t RecordLength(void);
+int32_t RecordLevel(void);
+int32_t RecordLost(void);
+int32_t RecordRate(void);
+int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source);
+int32_t RecordStop(void);
int32_t SoundCaps(const char *field);
int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels);
const char *SoundDriverAuto(void);
@@ -566,10 +894,16 @@ int32_t SoundLength(int32_t sound);
int32_t SoundLoad(const char *fileName);
int32_t SoundMasterVolume(void);
int32_t SoundMidiCount(void);
+int32_t SoundMidiInCount(void);
+const char *SoundMidiInLabel(int32_t index);
const char *SoundMidiLabel(int32_t index);
+int32_t SoundModulesCount(void);
+const char *SoundModulesLabel(int32_t index);
+void SoundPause(int32_t voice);
int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt);
void SoundPlayEvent(int32_t eventIdx, int32_t wait);
int32_t SoundPlaying(int32_t voice);
+void SoundResume(int32_t voice);
void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName);
void SoundSetFrequency(int32_t voice, int32_t frequency);
void SoundSetMasterVolume(int32_t percent);
@@ -577,10 +911,14 @@ void SoundSetPan(int32_t voice, int32_t pan);
void SoundSetPitch(int32_t voice, int32_t percent);
const char *SoundSettingDriver(int32_t prefs);
int32_t SoundSettingMidi(int32_t prefs);
-void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t volume);
+int32_t SoundSettingMidiIn(int32_t prefs);
+void SoundSettingMidiInStore(int32_t prefs, int32_t midiIn);
+int32_t SoundSettingModules(int32_t prefs);
+void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume);
int32_t SoundSettingVolume(int32_t prefs);
void SoundSetVolume(int32_t voice, int32_t volume);
void SoundStreamClose(int32_t stream);
+void SoundStreamFlush(int32_t stream);
int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs);
int32_t SoundStreamQueued(int32_t stream);
int32_t SoundStreamSpace(int32_t stream);
@@ -590,5 +928,18 @@ int32_t SoundTestFile(const char *fileName);
void SoundVolume(int32_t volume);
void StopAllSounds(void);
void StopWave(int32_t voice);
+int32_t WtActive(int32_t voice);
+int32_t WtAlloc(void);
+void WtFree(int32_t voice);
+const char *WtInfo(const char *field);
+int32_t WtMemAlloc(int32_t bytes, int32_t bits);
+void WtMemFree(int32_t address);
+void WtRelease(int32_t voice);
+int32_t WtSetAttenuation(int32_t voice, int32_t attenuation);
+int32_t WtSetPan(int32_t voice, int32_t pan);
+int32_t WtSetPitch(int32_t voice, int32_t pitch);
+int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan);
+void WtStop(int32_t voice);
+int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits);
#endif // AUDIO_LIB_H
diff --git a/src/libs/kpunch/audio/audioBas.c b/src/libs/kpunch/audio/audioBas.c
index d76fa541..593c839c 100644
--- a/src/libs/kpunch/audio/audioBas.c
+++ b/src/libs/kpunch/audio/audioBas.c
@@ -40,6 +40,7 @@
#include "dvxBas.h"
#include
+#include
#include
#define PERCENT_MAX 100
@@ -48,6 +49,12 @@
#define MS_PER_SEC 1000
#define BITS_PER_BYTE 8
#define BYTE_MAX_VALUE 0xFF
+#define SAMPLE_BYTES 2 // WtUpload's samples: 16-bit
+
+#define BAS_WT_MEM_NONE (-1) // WtMemAlloc: no room
+#define BAS_WT_BITS_8 8
+#define BAS_WT_BITS_16 16
+#define BAS_WT_RUN 256 // WtUpload's samples copied at a time
// Percent ranges of the BASIC pan and pitch (sound.bas SOUND_PAN_* and
// SOUND_PITCH_*), from the library's own units.
@@ -66,6 +73,8 @@ typedef enum {
CapRateE,
CapBitsE,
CapChannelsE,
+ CapRecordE,
+ CapRecordSourcesE,
CapCountE
} SoundCapE;
@@ -79,14 +88,42 @@ typedef enum {
FmFieldCountE
} FmFieldE;
+// WtInfo$ fields: the description and counts, then what the chip does
+// itself (its AUDIO_WT_* flags, sWtFlags)
+typedef enum {
+ WtDescriptionE,
+ WtVoicesE,
+ WtFreeE,
+ WtMemoryE,
+ WtFreeMemoryE,
+ WtLevelE,
+ WtEnvelopesE,
+ WtLfosE,
+ WtFilterE,
+ WtEffectsE,
+ Wt8BitE,
+ WtRomE,
+ WtFieldCountE
+} WtFieldE;
+
// MidiPortInfo$ fields
typedef enum {
MidiDescriptionE,
MidiPresentE,
MidiOwnedE,
+ MidiInputE,
+ MidiInputOwnedE,
MidiFieldCountE
} MidiFieldE;
+// A note a BASIC program started on a wavetable voice, for audioWtUpdate
+// (which takes the whole note) when it changes
+typedef struct {
+ AudioWtVoiceT note;
+ const AudioCtxT *ctx; // NULL: free
+ int32_t voice;
+} BasWtNoteT;
+
// A numbered sound or stream of a BASIC program.
typedef struct {
void *obj; // AudioSampleT / AudioStreamT; NULL: free
@@ -95,16 +132,22 @@ typedef struct {
uint32_t frameBytes; // streams: bytes per frame of its format
} BasSlotT;
-static const char *const sCapNames[CapCountE] = { "dac", "synth", "tone", "voices", "running", "rate", "bits", "channels" };
+static const char *const sCapNames[CapCountE] = { "dac", "synth", "tone", "voices", "running", "rate", "bits", "channels", "record", "recordsources" };
static const char *const sFmNames[FmFieldCountE] = { "description", "voices", "free", "stereo", "owned" };
-static const char *const sMidiNames[MidiFieldCountE] = { "description", "present", "owned" };
+static const char *const sMidiNames[MidiFieldCountE] = { "description", "present", "owned", "input", "inputowned" };
+static const char *const sWtNames[WtFieldCountE] = { "description", "voices", "free", "memory", "freememory", "level", "envelopes", "lfos", "filter", "effects", "8bit", "rom" };
+static const uint32_t sWtFlags[WtFieldCountE] = { [WtEnvelopesE] = AUDIO_WT_ENVELOPES, [WtLfosE] = AUDIO_WT_LFOS, [WtFilterE] = AUDIO_WT_FILTER, [WtEffectsE] = AUDIO_WT_EFFECTS, [Wt8BitE] = AUDIO_WT_8BIT, [WtRomE] = AUDIO_WT_ROM };
-static char sInfo[BAS_INFO_MAX];
-static int32_t sLastError = AUDIO_OK;
-static char sDrivers[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX];
-static int32_t sDriverCount = -1; // -1: not read yet
-static BasSlotT sSamples[BAS_SLOTS];
-static BasSlotT sStreams[BAS_SLOTS];
+static char sInfo[BAS_INFO_MAX];
+static int32_t sLastError = AUDIO_OK;
+static char sDrivers[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX];
+static int32_t sDriverCount = -1; // -1: not read yet
+static BasSlotT sSamples[BAS_SLOTS];
+static BasSlotT sStreams[BAS_SLOTS];
+static BasWtNoteT sWtNotes[AUDIO_WT_VOICES_MAX];
+static uint32_t sMidiInMs = 0; // the time of the message MidiInMessage gave last
+static AudioRecT *sRec = NULL; // the recording a BASIC program runs (one at a time in DVX)
+static AudioCtxT *sRecCtx = NULL; // ... and that program's context
// ============================================================
// Prototypes
@@ -116,6 +159,7 @@ static int32_t basField(const char *field, const char *const *names, int32_t
static int32_t basNumber(BasSlotT *slots, void *obj, const AudioCtxT *ctx, uint32_t frameBytes);
static int32_t basOk(int32_t rc);
static int32_t basPercent(int32_t volume);
+static AudioRecT *basRec(AudioFormatT *fmt);
static int32_t basResult(int32_t rc);
static int32_t basScale(int32_t percent, int32_t lo, int32_t hi, int32_t unit);
static BasSlotT *basSlot(BasSlotT *slots, int32_t handle);
@@ -124,6 +168,8 @@ static int32_t basTone(AudioWaveE wave, int32_t frequency, int32_t milliseco
static int32_t basTruth(bool value);
static void basVoiceResult(int32_t rc);
static int32_t basWave(const char *fileName, bool loop);
+static BasWtNoteT *basWtNote(const AudioCtxT *ctx, int32_t voice);
+static int32_t basWtSet(int32_t voice, uint32_t what, int32_t value);
int32_t FmAlloc(void);
void FmFree(int32_t voice);
const char *FmInfo(const char *field);
@@ -132,6 +178,12 @@ void FmRawRelease(void);
int32_t FmRawWrite(int32_t reg, int32_t value);
int32_t FmWrite(int32_t voice, int32_t reg, int32_t value);
int32_t MidiAcquire(void);
+int32_t MidiInClock(void);
+void MidiInClose(void);
+int32_t MidiInLost(void);
+int32_t MidiInMessage(void);
+int32_t MidiInOpen(void);
+int32_t MidiInTime(void);
const char *MidiPortInfo(const char *field);
int32_t MidiPut(int32_t byteValue);
void MidiRelease(void);
@@ -140,6 +192,15 @@ int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t vol
int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume);
int32_t PlayWave(const char *fileName);
int32_t PlayWaveLoop(const char *fileName);
+int32_t RecordBits(void);
+int32_t RecordChannels(void);
+int32_t Recording(void);
+int32_t RecordLength(void);
+int32_t RecordLevel(void);
+int32_t RecordLost(void);
+int32_t RecordRate(void);
+int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source);
+int32_t RecordStop(void);
int32_t SoundCaps(const char *field);
int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels);
const char *SoundDriverAuto(void);
@@ -159,10 +220,16 @@ int32_t SoundLength(int32_t sound);
int32_t SoundLoad(const char *fileName);
int32_t SoundMasterVolume(void);
int32_t SoundMidiCount(void);
+int32_t SoundMidiInCount(void);
+const char *SoundMidiInLabel(int32_t index);
const char *SoundMidiLabel(int32_t index);
+int32_t SoundModulesCount(void);
+const char *SoundModulesLabel(int32_t index);
+void SoundPause(int32_t voice);
int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt);
void SoundPlayEvent(int32_t eventIdx, int32_t wait);
int32_t SoundPlaying(int32_t voice);
+void SoundResume(int32_t voice);
void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName);
void SoundSetFrequency(int32_t voice, int32_t frequency);
void SoundSetMasterVolume(int32_t percent);
@@ -170,10 +237,14 @@ void SoundSetPan(int32_t voice, int32_t pan);
void SoundSetPitch(int32_t voice, int32_t percent);
const char *SoundSettingDriver(int32_t prefs);
int32_t SoundSettingMidi(int32_t prefs);
-void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t volume);
+int32_t SoundSettingMidiIn(int32_t prefs);
+void SoundSettingMidiInStore(int32_t prefs, int32_t midiIn);
+int32_t SoundSettingModules(int32_t prefs);
+void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume);
int32_t SoundSettingVolume(int32_t prefs);
void SoundSetVolume(int32_t voice, int32_t volume);
void SoundStreamClose(int32_t stream);
+void SoundStreamFlush(int32_t stream);
int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs);
int32_t SoundStreamQueued(int32_t stream);
int32_t SoundStreamSpace(int32_t stream);
@@ -183,11 +254,29 @@ int32_t SoundTestFile(const char *fileName);
void SoundVolume(int32_t volume);
void StopAllSounds(void);
void StopWave(int32_t voice);
+int32_t WtActive(int32_t voice);
+int32_t WtAlloc(void);
+void WtFree(int32_t voice);
+const char *WtInfo(const char *field);
+int32_t WtMemAlloc(int32_t bytes, int32_t bits);
+void WtMemFree(int32_t address);
+void WtRelease(int32_t voice);
+int32_t WtSetAttenuation(int32_t voice, int32_t attenuation);
+int32_t WtSetPan(int32_t voice, int32_t pan);
+int32_t WtSetPitch(int32_t voice, int32_t pitch);
+int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan);
+void WtStop(int32_t voice);
+int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits);
// ctx is closing (it frees its samples and streams itself): forget their
// numbers.
void audioBasForget(const AudioCtxT *ctx) {
+ if (sRecCtx == ctx) {
+ sRec = NULL;
+ sRecCtx = NULL;
+ }
+
for (int32_t i = 0; i < BAS_SLOTS; i++) {
if (sSamples[i].ctx == ctx) {
sSamples[i].obj = NULL;
@@ -199,6 +288,12 @@ void audioBasForget(const AudioCtxT *ctx) {
sStreams[i].ctx = NULL;
}
}
+
+ for (int32_t i = 0; i < AUDIO_WT_VOICES_MAX; i++) {
+ if (sWtNotes[i].ctx == ctx) {
+ sWtNotes[i].ctx = NULL;
+ }
+ }
}
@@ -257,6 +352,19 @@ static int32_t basPercent(int32_t volume) {
}
+// The calling program's recording and its format, or NULL (fmt zeroed).
+static AudioRecT *basRec(AudioFormatT *fmt) {
+ memset(fmt, 0, sizeof(*fmt));
+
+ if (!sRec || sRecCtx != audioBasicContext()) {
+ return NULL;
+ }
+
+ audioRecFormat(sRec, fmt);
+ return sRec;
+}
+
+
// A voice id, or 0 with the error kept for SoundError$.
static int32_t basResult(int32_t rc) {
sLastError = rc < 0 ? rc : AUDIO_OK;
@@ -330,6 +438,40 @@ static int32_t basWave(const char *fileName, bool loop) {
}
+// The note voice of ctx plays, or NULL.
+static BasWtNoteT *basWtNote(const AudioCtxT *ctx, int32_t voice) {
+ for (int32_t i = 0; ctx && i < AUDIO_WT_VOICES_MAX; i++) {
+ if (sWtNotes[i].ctx == ctx && sWtNotes[i].voice == voice) {
+ return &sWtNotes[i];
+ }
+ }
+
+ return NULL;
+}
+
+
+// One value of a started note changed (what: AUDIO_WT_SET_PITCH, _VOLUME,
+// _PAN), True when it worked.
+static int32_t basWtSet(int32_t voice, uint32_t what, int32_t value) {
+ AudioCtxT *ctx = audioBasicContext();
+ BasWtNoteT *w = basWtNote(ctx, voice);
+
+ if (!w) {
+ return basOk(AUDIO_ERR_VOICE);
+ }
+
+ if (what == AUDIO_WT_SET_PITCH) {
+ w->note.pitch = value;
+ } else if (what == AUDIO_WT_SET_VOLUME) {
+ w->note.attenuation = value < 0 ? 0 : (value > AUDIO_WT_ATTEN_MAX ? AUDIO_WT_ATTEN_MAX : value);
+ } else {
+ w->note.pan = basScale(value, BAS_PAN_MIN, BAS_PAN_MAX, AUDIO_WT_PAN_MAX);
+ }
+
+ return basOk(audioWtUpdate(ctx, voice, &w->note, what));
+}
+
+
// A shared two-operator FM voice (FmFree gives it back), or 0.
int32_t FmAlloc(void) {
AudioCtxT *ctx = audioBasicContext();
@@ -433,6 +575,52 @@ int32_t MidiAcquire(void) {
}
+// Milliseconds since MidiInOpen, on the clock MidiInTime is on.
+int32_t MidiInClock(void) {
+ return (int32_t)audioMidiInClock(audioBasicContext());
+}
+
+
+void MidiInClose(void) {
+ audioMidiInClose(audioBasicContext());
+}
+
+
+// Bytes the MIDI input dropped because the program read too late.
+int32_t MidiInLost(void) {
+ return (int32_t)audioMidiInLost(audioBasicContext());
+}
+
+
+// The next message from the MIDI input as status + data1 * 256 + data2 *
+// 65536 (MidiInTime is when it came), or -1 when none waits.
+int32_t MidiInMessage(void) {
+ AudioMidiMsgT msg;
+
+ if (!audioMidiInRead(audioBasicContext(), &msg)) {
+ return AUDIO_MIDI_PACKED_NONE;
+ }
+
+ sMidiInMs = msg.ms;
+ return (int32_t)msg.status | ((int32_t)msg.data1 << AUDIO_MIDI_PACKED_DATA1_SHIFT) | ((int32_t)msg.data2 << AUDIO_MIDI_PACKED_DATA2_SHIFT);
+}
+
+
+// Read the MIDI input (True), until MidiInClose; False with SoundError$.
+int32_t MidiInOpen(void) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ return basOk(ctx ? audioMidiInOpen(ctx) : AUDIO_ERR_NO_DEVICE);
+}
+
+
+// When the message MidiInMessage gave last came (milliseconds since
+// MidiInOpen).
+int32_t MidiInTime(void) {
+ return (int32_t)sMidiInMs;
+}
+
+
// The MIDI port: "description" (text, "" when there is none), "present",
// "owned" (True / False as text); "" for another field.
const char *MidiPortInfo(const char *field) {
@@ -452,6 +640,12 @@ const char *MidiPortInfo(const char *field) {
case MidiOwnedE:
return basText(basTruth(info.owned));
+ case MidiInputE:
+ return basText(basTruth(info.inPresent));
+
+ case MidiInputOwnedE:
+ return basText(basTruth(info.inOwned));
+
default:
return "";
}
@@ -507,9 +701,119 @@ int32_t PlayWaveLoop(const char *fileName) {
}
+// The recording's sample size and channels (0 when the program records
+// nothing).
+int32_t RecordBits(void) {
+ AudioFormatT fmt;
+
+ basRec(&fmt);
+ return (int32_t)fmt.bits;
+}
+
+
+int32_t RecordChannels(void) {
+ AudioFormatT fmt;
+
+ basRec(&fmt);
+ return (int32_t)fmt.channels;
+}
+
+
+// True while the program records.
+int32_t Recording(void) {
+ AudioFormatT fmt;
+
+ return basTruth(basRec(&fmt) != NULL);
+}
+
+
+// Milliseconds recorded so far (written to the file, or about to be).
+int32_t RecordLength(void) {
+ AudioFormatT fmt;
+ AudioRecT *rec = basRec(&fmt);
+
+ return rec ? (int32_t)((uint64_t)(audioRecFrames(rec) + audioRecQueued(rec)) * MS_PER_SEC / fmt.rate) : 0;
+}
+
+
+// The loudest sample since the last call, percent (a level meter).
+int32_t RecordLevel(void) {
+ AudioFormatT fmt;
+ AudioRecT *rec = basRec(&fmt);
+
+ return rec ? audioRecPeak(rec) : 0;
+}
+
+
+// Frames dropped because DVX was too busy to write them in time.
+int32_t RecordLost(void) {
+ AudioFormatT fmt;
+ AudioRecT *rec = basRec(&fmt);
+
+ return rec ? (int32_t)audioRecLost(rec) : 0;
+}
+
+
+// The rate the card records at (the nearest it has to the one asked).
+int32_t RecordRate(void) {
+ AudioFormatT fmt;
+
+ basRec(&fmt);
+ return (int32_t)fmt.rate;
+}
+
+
+// Record from source (RECORD_MIC, RECORD_LINE, RECORD_CD) into a WAV file
+// in rate, bits and channels (0: the default). True when it started;
+// False with SoundError$ saying why.
+int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source) {
+ AudioCtxT *ctx = audioBasicContext();
+ AudioRecT *rec = NULL;
+ AudioFormatT fmt;
+
+ if (!ctx) {
+ return basOk(AUDIO_ERR_NO_DEVICE);
+ }
+
+ if (rate < 0 || bits < 0 || channels < 0) {
+ return basOk(AUDIO_ERR_ARG);
+ }
+
+ fmt.rate = (uint32_t)rate;
+ fmt.bits = (uint32_t)bits;
+ fmt.channels = (uint32_t)channels;
+
+ int32_t rc = audioRecFile(ctx, fileName, &fmt, source, &rec);
+
+ if (rc == AUDIO_OK) {
+ sRec = rec;
+ sRecCtx = ctx;
+ }
+
+ return basOk(rc);
+}
+
+
+// Stop recording; the file is complete. True when it was all written.
+int32_t RecordStop(void) {
+ AudioFormatT fmt;
+ AudioRecT *rec = basRec(&fmt);
+
+ if (!rec) {
+ return basOk(AUDIO_ERR_ARG);
+ }
+
+ sRec = NULL;
+ sRecCtx = NULL;
+ return basOk(audioRecClose(rec));
+}
+
+
// What the sound device can do (opens the program's context, so the
-// device is started): "dac", "synth", "tone", "running" True / False;
-// "voices", "rate", "bits", "channels" numbers; 0 for another field.
+// device is started): "dac", "synth", "tone", "running", "record" True /
+// False; "voices", "rate", "bits", "channels" numbers; "recordsources"
+// the inputs it records from (bit RECORD_MIC, RECORD_LINE, RECORD_CD
+// set for each: 1, 2, 4); 0 for another field.
int32_t SoundCaps(const char *field) {
AudioInfoT info;
@@ -541,6 +845,12 @@ int32_t SoundCaps(const char *field) {
case CapChannelsE:
return (int32_t)info.channels;
+ case CapRecordE:
+ return basTruth(info.canRecord);
+
+ case CapRecordSourcesE:
+ return (int32_t)info.recSources;
+
default:
return 0;
}
@@ -736,12 +1046,43 @@ int32_t SoundMidiCount(void) {
}
+int32_t SoundMidiInCount(void) {
+ return AUDIO_MIDI_IN_CHOICE_COUNT;
+}
+
+
+// Where MIDI input can come from, for people ("" out of range).
+const char *SoundMidiInLabel(int32_t index) {
+ return (index >= 0 && index < AUDIO_MIDI_IN_CHOICE_COUNT) ? audioMidiInChoices[index].label : "";
+}
+
+
// Where MIDI music can play, for people ("" out of range).
const char *SoundMidiLabel(int32_t index) {
return (index >= 0 && index < AUDIO_MIDI_CHOICE_COUNT) ? audioMidiChoices[index].label : "";
}
+int32_t SoundModulesCount(void) {
+ return AUDIO_MODULES_CHOICE_COUNT;
+}
+
+
+// Where tracker modules can play, for people ("" out of range).
+const char *SoundModulesLabel(int32_t index) {
+ return (index >= 0 && index < AUDIO_MODULES_CHOICE_COUNT) ? audioModulesChoices[index].label : "";
+}
+
+
+// Hold a sound's or a stream's voice where it is (SoundResume lets it go
+// on); the result is kept for SoundError$.
+void SoundPause(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ basVoiceResult(ctx ? audioVoicePause(ctx, voice, true) : AUDIO_ERR_NO_DEVICE);
+}
+
+
// Play a loaded sound on a new voice: volume percent, pan -100 (left) ..
// 100 (right).
int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt) {
@@ -773,6 +1114,13 @@ int32_t SoundPlaying(int32_t voice) {
}
+void SoundResume(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ basVoiceResult(ctx ? audioVoicePause(ctx, voice, false) : AUDIO_ERR_NO_DEVICE);
+}
+
+
void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName) {
audioDeskSetFile(prefsBasHandle(prefs), (DvxSoundE)eventIdx, fileName);
}
@@ -823,11 +1171,30 @@ int32_t SoundSettingMidi(int32_t prefs) {
}
-void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t volume) {
+int32_t SoundSettingMidiIn(int32_t prefs) {
+ return audioSettingMidiIn(prefsBasHandle(prefs));
+}
+
+
+void SoundSettingMidiInStore(int32_t prefs, int32_t midiIn) {
PrefsHandleT *h = prefsBasHandle(prefs);
if (h) {
- audioSettingsStore(h, driver, midi, volume);
+ audioSettingMidiInStore(h, midiIn);
+ }
+}
+
+
+int32_t SoundSettingModules(int32_t prefs) {
+ return audioSettingModules(prefsBasHandle(prefs));
+}
+
+
+void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume) {
+ PrefsHandleT *h = prefsBasHandle(prefs);
+
+ if (h) {
+ audioSettingsStore(h, driver, midi, modules, volume);
}
}
@@ -856,6 +1223,16 @@ void SoundStreamClose(int32_t stream) {
}
+// What the stream holds and has not played yet dropped.
+void SoundStreamFlush(int32_t stream) {
+ BasSlotT *s = basSlot(sStreams, stream);
+
+ if (s) {
+ audioStreamFlush((AudioStreamT *)s->obj);
+ }
+}
+
+
// A stream the program writes PCM into (SoundStreamWrite); it plays at
// once on its own voice (SoundStreamVoice). bufferMs 0: the default.
int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs) {
@@ -958,3 +1335,185 @@ void StopWave(int32_t voice) {
audioVoiceStop(ctx, voice);
}
}
+
+
+int32_t WtActive(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ return basTruth(ctx && audioWtActive(ctx, voice));
+}
+
+
+int32_t WtAlloc(void) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ return basResult(ctx ? audioWtAlloc(ctx) : AUDIO_ERR_NO_DEVICE);
+}
+
+
+void WtFree(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+ BasWtNoteT *w = basWtNote(ctx, voice);
+
+ if (w) {
+ w->ctx = NULL;
+ }
+
+ basVoiceResult(ctx ? audioWtFree(ctx, voice) : AUDIO_ERR_NO_DEVICE);
+}
+
+
+const char *WtInfo(const char *field) {
+ AudioWtInfoT info;
+ int32_t f;
+
+ audioBasicContext();
+ audioWtGetInfo(&info);
+ f = basField(field, sWtNames, WtFieldCountE);
+
+ switch ((WtFieldE)f) {
+ case WtDescriptionE:
+ snprintf(sInfo, sizeof(sInfo), "%s", info.description);
+ return sInfo;
+
+ case WtVoicesE:
+ return basText((int32_t)info.voices);
+
+ case WtFreeE:
+ return basText((int32_t)info.freeVoices);
+
+ case WtMemoryE:
+ return basText((int32_t)info.memory);
+
+ case WtFreeMemoryE:
+ return basText((int32_t)info.freeMemory);
+
+ case WtLevelE:
+ return basText(info.level);
+
+ default:
+ return f < 0 ? "" : basText(basTruth((info.flags & sWtFlags[f]) != 0));
+ }
+}
+
+
+// Sample memory's address, or BAS_WT_MEM_NONE (0 is an address).
+int32_t WtMemAlloc(int32_t bytes, int32_t bits) {
+ AudioCtxT *ctx = audioBasicContext();
+ uint32_t addr = 0;
+ int32_t rc = AUDIO_ERR_NO_DEVICE;
+
+ if (ctx) {
+ rc = bytes > 0 && bits > 0 ? audioWtMemAlloc(ctx, (uint32_t)bytes, (uint32_t)bits, &addr) : AUDIO_ERR_ARG;
+ }
+
+ basVoiceResult(rc);
+ return rc == AUDIO_OK ? (int32_t)addr : BAS_WT_MEM_NONE;
+}
+
+
+void WtMemFree(int32_t address) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ basVoiceResult(ctx && address >= 0 ? audioWtMemFree(ctx, (uint32_t)address) : AUDIO_ERR_ARG);
+}
+
+
+void WtRelease(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ basVoiceResult(ctx ? audioWtRelease(ctx, voice) : AUDIO_ERR_NO_DEVICE);
+}
+
+
+int32_t WtSetAttenuation(int32_t voice, int32_t attenuation) {
+ return basWtSet(voice, AUDIO_WT_SET_VOLUME, attenuation);
+}
+
+
+int32_t WtSetPan(int32_t voice, int32_t pan) {
+ return basWtSet(voice, AUDIO_WT_SET_PAN, pan);
+}
+
+
+int32_t WtSetPitch(int32_t voice, int32_t pitch) {
+ return basWtSet(voice, AUDIO_WT_SET_PITCH, pitch);
+}
+
+
+// A plain note (audioWtNoteInit) on a sample in the memory, kept for the
+// Set functions.
+int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan) {
+ AudioCtxT *ctx = audioBasicContext();
+ BasWtNoteT *w = basWtNote(ctx, voice);
+
+ if (!ctx) {
+ return basOk(AUDIO_ERR_NO_DEVICE);
+ }
+
+ if (address < 0 || startFrame < 0 || startFrame > endFrame || loopStart < 0 || loopEnd < loopStart || loopEnd > endFrame || (loopMode != AUDIO_WT_LOOP_NONE && loopMode != AUDIO_WT_LOOP_ON && loopMode != AUDIO_WT_LOOP_RELEASE) || (bits != BAS_WT_BITS_8 && bits != BAS_WT_BITS_16)) {
+ return basOk(AUDIO_ERR_ARG);
+ }
+
+ for (int32_t i = 0; !w && i < AUDIO_WT_VOICES_MAX; i++) {
+ w = sWtNotes[i].ctx ? NULL : &sWtNotes[i];
+ }
+
+ if (!w) {
+ return basOk(AUDIO_ERR_NO_VOICE);
+ }
+
+ AudioWtVoiceT *n = &w->note;
+
+ audioWtNoteInit(n);
+ n->addr = (uint32_t)address;
+ n->offset = (uint32_t)startFrame;
+ n->end = (uint32_t)endFrame;
+ n->loopStart = (uint32_t)loopStart;
+ n->loopEnd = (uint32_t)loopEnd;
+ n->loop = (uint8_t)loopMode;
+ n->bits = (uint8_t)bits;
+ n->pitch = pitch;
+ n->attenuation = attenuation < 0 ? 0 : (attenuation > AUDIO_WT_ATTEN_MAX ? AUDIO_WT_ATTEN_MAX : attenuation);
+ n->pan = basScale(pan, BAS_PAN_MIN, BAS_PAN_MAX, AUDIO_WT_PAN_MAX);
+
+ int32_t rc = audioWtStart(ctx, voice, n);
+
+ w->ctx = rc == AUDIO_OK ? ctx : NULL;
+ w->voice = voice;
+ return basOk(rc);
+}
+
+
+void WtStop(int32_t voice) {
+ AudioCtxT *ctx = audioBasicContext();
+
+ basVoiceResult(ctx ? audioWtStop(ctx, voice) : AUDIO_ERR_NO_DEVICE);
+}
+
+
+// 16-bit samples (signed, little-endian, mono) into the memory at
+// address, stored at bits: copied into whole samples a run at a time.
+int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits) {
+ AudioCtxT *ctx = audioBasicContext();
+ uint32_t frames = len > 0 ? (uint32_t)len / SAMPLE_BYTES : 0;
+ int32_t rc = AUDIO_OK;
+ int16_t run[BAS_WT_RUN];
+
+ if (!ctx) {
+ return basOk(AUDIO_ERR_NO_DEVICE);
+ }
+
+ if (address < 0 || frames == 0 || (bits != BAS_WT_BITS_8 && bits != BAS_WT_BITS_16)) {
+ return basOk(AUDIO_ERR_ARG);
+ }
+
+ for (uint32_t done = 0; done < frames && rc == AUDIO_OK; done += BAS_WT_RUN) {
+ uint32_t n = frames - done < BAS_WT_RUN ? frames - done : BAS_WT_RUN;
+
+ memcpy(run, pcm + done * SAMPLE_BYTES, n * SAMPLE_BYTES);
+ rc = audioWtUpload(ctx, (uint32_t)address + done * ((uint32_t)bits / BITS_PER_BYTE), run, n, (uint32_t)bits);
+ }
+
+ return basOk(rc);
+}
diff --git a/src/libs/kpunch/audio/audioDrv.h b/src/libs/kpunch/audio/audioDrv.h
index 43eac24e..790a83fe 100644
--- a/src/libs/kpunch/audio/audioDrv.h
+++ b/src/libs/kpunch/audio/audioDrv.h
@@ -40,7 +40,18 @@
// 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
+// again.
+//
+// Recording (audio.h): a driver whose card can record gives recordStart
+// and recordStop (NULL otherwise) and the sources it records from in
+// AudioDrvInfoT.recSources. recordStart is called with the output
+// closed (open found the card earlier; close, or no play, followed): it
+// takes the card for recording (its own DMA buffer and interrupt), sets
+// the source and the format (got: what the card records, the nearest it
+// can to want), and from then on its interrupt calls audioRecPush with
+// each block recorded. recordStop gives everything back as close
+// does; audio.lib opens the output again (open, play) when something
+// plays. 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
@@ -70,21 +81,42 @@
#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.
+// DVX.INI [audio] midi: where MIDI music plays. auto: a wavetable
+// synthesizer with a SoundFont when there is one, else the FM chip, else
+// the FM 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: the FM chip
+// (else the FM software synthesizer); software: the FM software
+// synthesizer; softwave: the SoundFont software synthesizer (music.lib
+// decides all but mpu401).
#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_MIDI_SOFTWAVE "softwave"
#define AUDIO_MPU_PORT_DEFAULT "330"
+// DVX.INI [audio] midiIn: where MIDI input comes from (audio.h, "MIDI
+// input"): none, or mpu401 (the MPU-401 port at mpuPort, opened for it
+// even when music plays elsewhere).
+#define AUDIO_INI_MIDI_IN "midiIn"
+#define AUDIO_MIDI_IN_NONE "none"
+
+// DVX.INI [audio] modules: where tracker modules play. wavetable: on a
+// wavetable synthesizer's voices when it is free and the module fits
+// there, else mixed in software; software: always mixed in software
+// (music.lib decides).
+#define AUDIO_INI_MODULES "modules"
+#define AUDIO_MODULES_WAVETABLE "wavetable"
+#define AUDIO_MODULES_SOFTWARE "software"
+
// Driver priorities (tried from the highest down)
+#define AUDIO_PRIORITY_CODEC 110 // a WSS codec: before the Sound Blaster mode the same board may have
#define AUDIO_PRIORITY_CARD 100 // sound cards
+#define AUDIO_PRIORITY_GUS 90 // a GUS: after a Sound Blaster beside it, which leaves it all its voices for music
+#define AUDIO_PRIORITY_LPT 50 // DACs on the parallel port
#define AUDIO_PRIORITY_SPEAKER 20 // the PC speaker
#define AUDIO_PRIORITY_NULL 0 // plays nothing
@@ -105,15 +137,21 @@ typedef struct {
// time.
typedef void (*AudioToneFnT)(uint32_t hz);
-// What a driver's open found.
+struct AudioSynthDriverT;
+
+// What a driver's open found. synth: a synthesizer on the same card that
+// only this driver can run (a GUS's other voices beside its digital
+// sound), opened with the synth drivers and before them; NULL for none.
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
+ 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
+ const struct AudioSynthDriverT *synth; // the card's own synthesizer, or NULL
+ bool hasDac; // plays sampled sound
+ bool hasSynth; // has a hardware synthesizer
+ uint32_t recSources; // recordStart's sources: bit 1 << AUDIO_REC_* (0: cannot record)
} AudioDrvInfoT;
// The descriptor every driver exports as AUDIO_DRV_SYMBOL.
@@ -124,6 +162,8 @@ typedef struct {
bool (*open)(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
bool (*play)(void);
void (*close)(void);
+ bool (*recordStart)(const AudioFormatT *want, int32_t source, AudioFormatT *got);
+ void (*recordStop)(void);
} AudioDriverT;
// Write a driver's descriptor: AUDIO_DRIVER("x", AUDIO_PRIORITY_CARD,
@@ -146,21 +186,29 @@ typedef struct {
// 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).
+// programmed by register writes), a MIDI port (AUDIO_SYNTH_MIDI, sent
+// bytes), or a wavetable synthesizer (AUDIO_SYNTH_WAVETABLE: samples
+// uploaded to its memory, voices started on them; audio.h, "Wavetable
+// synthesizer"). 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, get (MIDI input, polled from the timer service), and a
+// wavetable driver's voice functions (start, update, release, stop,
+// active), 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). A MIDI driver with get keeps what put has to read past
+// for get, so no input is lost while output goes on. A wavetable driver's upload runs at
+// program time.
#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
+#define AUDIO_SYNTH_WAVETABLE 2 // sample-playing synthesizer (EMU8000, GF1)
+#define AUDIO_SYNTH_KINDS 3
// FM register numbers passed to write: bank (0, or 1 on an OPL3) times
// AUDIO_FM_BANK_REGS plus the register index.
@@ -169,20 +217,30 @@ typedef struct {
// 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)
+ uint32_t voices; // FM: two-operator voices (9 on an OPL2, 18 on an OPL3); wavetable: voices
+ uint32_t memory; // wavetable: bytes of sample memory
+ uint32_t flags; // wavetable: what the chip does itself (AUDIO_WT_*)
+ int32_t level; // wavetable: AudioWtInfoT's level
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 {
+typedef struct AudioSynthDriverT {
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
+ bool (*get)(uint8_t *byte); // MIDI: a byte the port received; false when none waits
+ bool (*upload)(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits); // wavetable: frames into memory at addr (program time)
+ void (*start)(uint32_t voice, const AudioWtVoiceT *v); // wavetable: the voices (0 .. voices - 1)
+ void (*update)(uint32_t voice, const AudioWtVoiceT *v, uint32_t what);
+ void (*release)(uint32_t voice);
+ void (*stop)(uint32_t voice);
+ bool (*active)(uint32_t voice);
} AudioSynthDriverT;
// Write a synth driver's descriptor: AUDIO_SYNTH_DRIVER("x", priority,
@@ -194,6 +252,11 @@ typedef struct {
// stereo interleaved). Interrupt-safe; never blocks.
void audioMixRender(void *dst, uint32_t frames);
+// A block the card recorded: bytes of whole frames in the format
+// recordStart gave. Interrupt-safe; never blocks; a block that does not
+// fit in the recording's buffer is dropped (counted lost).
+void audioRecPush(const void *src, uint32_t bytes);
+
// 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).
@@ -206,6 +269,18 @@ void audioClockStop(void);
// covers every function listed). Returns false when pinning failed.
bool audioLockSpan(const AudioLockItemT *items, int32_t count, bool lock);
+// Sound files (audioLoad.c), for audio.lib and the libraries built on it
+// (music.lib streams WAV and VOC files). audioDecode: a WAV or VOC
+// image as 16-bit frames (malloc'd, frames * fmt->channels values;
+// fmt->bits is what the file had); AUDIO_ERR_FORMAT when it is neither.
+// audioWavParse: a WAV file's format and where its sound is (*dataAt,
+// *dataBytes: whole frames, no further than the file's fileBytes), from
+// its first headBytes bytes, which must reach the data chunk's header.
+// audioConvert: count 8-bit unsigned or 16-bit signed samples as 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);
+int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileBytes, AudioFormatT *fmt, uint32_t *dataAt, uint32_t *dataBytes);
+
// 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
diff --git a/src/libs/kpunch/audio/audioInt.h b/src/libs/kpunch/audio/audioInt.h
index 9e5a08be..5d51a835 100644
--- a/src/libs/kpunch/audio/audioInt.h
+++ b/src/libs/kpunch/audio/audioInt.h
@@ -131,6 +131,7 @@ typedef struct {
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
+ volatile bool paused; // samples, streams: held where they are (audioVoicePause)
bool draining; // the source ran out: cur is its last frame
bool timed; // tones: stop when remain reaches 0
} MixVoiceT;
@@ -152,6 +153,23 @@ typedef struct {
extern AudioMixT audioMixer;
+// The recording's ring (audioRec.c): bytes in the recording's format, a
+// power of two long; head counts the bytes the interrupt wrote
+// (audioRecPush), tail those the program side took. A block that does
+// not fit is dropped whole (lost counts its bytes), so the ring never
+// holds part of a frame. audioRecRing is the recording running (NULL:
+// none), pinned with the mixer.
+typedef struct {
+ uint8_t *data;
+ uint32_t mask; // capacity in bytes - 1
+ uint32_t frameBytes; // the recording's frame size (program side)
+ volatile uint32_t head;
+ volatile uint32_t tail;
+ volatile uint32_t lost;
+} AudioRecRingT;
+
+extern AudioRecRingT *volatile audioRecRing;
+
// 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.
@@ -172,20 +190,33 @@ 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 audioMidiAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info, bool inWanted);
void audioMidiDetach(void);
+
+// The MIDI port's input: its driver's get, NULL when there is none or
+// DVX.INI [audio] midiIn did not choose it (audioSynth.c, attached with
+// inWanted). audioMidiIn.c reads it from a timer; Owned says a program
+// does, CtxClosed ends a closing context's reading, Suspend ends any
+// (a DOS program runs).
+typedef bool (*AudioMidiGetFnT)(uint8_t *byte);
+
+void audioMidiInCtxClosed(const AudioCtxT *ctx);
+bool audioMidiInOwned(void);
+AudioMidiGetFnT audioMidiInPort(void);
+void audioMidiInSuspend(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 wavetable synthesizer (audioWt.c): the same, for the wavetable
+// driver the device opened.
+void audioWtAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info);
+void audioWtCtxClosed(AudioCtxT *ctx);
+void audioWtDetach(void);
+bool audioWtLock(bool lock);
-// 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 library's end of a program's life, of the shell's idle time, of a
+// DOS program run with DVX stepped aside (audioSuspend, a platform suspend
+// hook) and of its own unloading (audio.c; audioShell.c hooks them up),
// the calling BASIC program's context, and making appId the current
// program (returns the one before). audioBasForget (audioBas.c) drops the
// BASIC numbers of a closing BASIC context's sounds and streams.
@@ -195,6 +226,19 @@ void audioBasForget(const AudioCtxT *ctx);
AudioCtxT *audioBasicContext(void);
void audioIdle(void *unused);
void audioShutdown(void);
+void audioSuspend(bool suspending);
+
+// Recording (audioRec.c): a closing context's recording ends with it;
+// the pump writes a recording into a file; a DOS program ends it. And
+// the device's side (audio.c): Begin takes the card for recording from
+// source (AUDIO_ERR_NO_DEVICE, AUDIO_ERR_NO_RECORD): the output is
+// closed and new voices are refused until End, which plays on the
+// voices it held.
+int32_t audioDevRecordBegin(int32_t source, const AudioDriverT **drv);
+void audioDevRecordEnd(void);
+void audioRecCtxClosed(const AudioCtxT *ctx);
+void audioRecIdle(void);
+void audioRecSuspend(void);
// Desktop sounds (audioDesk.c): Init connects them to DVX (the sound
// hook), Shutdown disconnects them before the library closes its
diff --git a/src/libs/kpunch/audio/audioLoad.c b/src/libs/kpunch/audio/audioLoad.c
index f8f651b9..3992a98b 100644
--- a/src/libs/kpunch/audio/audioLoad.c
+++ b/src/libs/kpunch/audio/audioLoad.c
@@ -27,7 +27,8 @@
// 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
+// and all but 'fmt ' and 'data' are skipped (audioWavParse, which also
+// serves a caller holding only the start of the file). 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
@@ -115,6 +116,7 @@ typedef struct {
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);
+int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileBytes, AudioFormatT *fmt, uint32_t *dataAt, uint32_t *dataBytes);
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);
@@ -158,6 +160,79 @@ int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t
}
+// The chunks walked from the start (a chunk that claims more than the
+// file has is damaged, but for the data chunk: a recorder that stopped
+// early leaves one, and what is there plays). The 'fmt ' chunk must lie
+// within head; the data chunk only starts there.
+int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileBytes, AudioFormatT *fmt, uint32_t *dataAt, uint32_t *dataBytes) {
+ const uint8_t *fmtChunk = NULL;
+ uint32_t fmtBytes = 0;
+ bool haveData = false;
+ uint32_t pos = RIFF_HEADER_BYTES;
+
+ *dataAt = 0;
+ *dataBytes = 0;
+
+ if (headBytes < RIFF_HEADER_BYTES || headBytes > fileBytes || memcmp(head, "RIFF", CHUNK_ID_BYTES) != 0 || memcmp(head + RIFF_FORM_OFFSET, "WAVE", CHUNK_ID_BYTES) != 0) {
+ return AUDIO_ERR_FORMAT;
+ }
+
+ while (pos + CHUNK_HEADER_BYTES <= headBytes && !(fmtChunk && haveData)) {
+ const uint8_t *id = head + pos;
+ uint32_t len = rd32(head + pos + CHUNK_SIZE_OFFSET);
+ uint32_t avail = fileBytes - pos - CHUNK_HEADER_BYTES;
+
+ if (memcmp(id, "data", CHUNK_ID_BYTES) == 0) {
+ *dataAt = pos + CHUNK_HEADER_BYTES;
+ *dataBytes = len < avail ? len : avail;
+ haveData = true;
+ } else if (len > avail) {
+ return AUDIO_ERR_CORRUPT;
+ } else if (memcmp(id, "fmt ", CHUNK_ID_BYTES) == 0) {
+ if (len > headBytes - pos - CHUNK_HEADER_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ fmtChunk = id + CHUNK_HEADER_BYTES;
+ fmtBytes = len;
+ }
+
+ if (len >= avail) {
+ break;
+ }
+
+ pos += CHUNK_HEADER_BYTES + len + (len & 1);
+ }
+
+ if (!fmtChunk || !haveData || 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;
+ }
+
+ *dataBytes = *dataBytes / frameBytes * frameBytes;
+ return AUDIO_OK;
+}
+
+
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;
@@ -317,59 +392,15 @@ static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint
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;
+ uint32_t dataAt;
+ uint32_t dataBytes;
+ int32_t rc = audioWavParse(data, size, size, fmt, &dataAt, &dataBytes);
- 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 (rc != AUDIO_OK) {
+ return rc;
}
- 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;
+ uint32_t count = dataBytes / (fmt->bits / BITS_PER_BYTE);
*pcm = (int16_t *)malloc(count * sizeof(int16_t));
@@ -377,7 +408,7 @@ static int32_t decodeWav(const uint8_t *data, uint32_t size, int16_t **pcm, uint
return AUDIO_ERR_NO_MEMORY;
}
- audioConvert(dataChunk, *pcm, count, fmt->bits);
+ audioConvert(data + dataAt, *pcm, count, fmt->bits);
*frames = count / fmt->channels;
return AUDIO_OK;
}
diff --git a/src/libs/kpunch/audio/audioMidiIn.c b/src/libs/kpunch/audio/audioMidiIn.c
new file mode 100644
index 00000000..0c54a8c3
--- /dev/null
+++ b/src/libs/kpunch/audio/audioMidiIn.c
@@ -0,0 +1,294 @@
+// 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.
+
+// audioMidiIn.c -- MIDI input (see audio.h, "MIDI input")
+//
+// A timer at AUDIO_MIDI_IN_HZ (one call a millisecond) reads what the
+// MIDI port received (its driver's get) into a ring of bytes and the
+// times they came, counted by the timer itself; the program side puts
+// the bytes together into messages (audioMidiInRead): running status
+// filled in, the data bytes each status takes, real-time bytes (which
+// may come between any two) dropped, system exclusive skipped to its
+// end. The timer and the ring are pinned: the timer runs inside the
+// timer interrupt.
+
+#include "audioInt.h"
+
+#include
+
+#define MIDI_STATUS_BIT 0x80
+#define MIDI_KIND_MASK 0xF0
+#define MIDI_PROGRAM 0xC0 // program change: one data byte
+#define MIDI_PRESSURE 0xD0 // channel pressure: one data byte
+#define MIDI_SYSTEM 0xF0 // system messages from here
+#define MIDI_SYSEX 0xF0
+#define MIDI_TIME_CODE 0xF1 // one data byte
+#define MIDI_SONG_POSITION 0xF2 // two
+#define MIDI_SONG_SELECT 0xF3 // one
+#define MIDI_SYSEX_END 0xF7
+#define MIDI_REALTIME 0xF8 // real-time bytes from here
+#define MIDI_MSG_MAX 3
+#define POLL_BYTES_MAX 8 // read per timer call at most (a cable brings about 3 a millisecond)
+#define MS_PER_SEC 1000
+#define TIMER_NONE (-1)
+
+// The bytes the timer read and when (AUDIO_MIDI_IN_BYTES, a power of two)
+typedef struct {
+ uint8_t data[AUDIO_MIDI_IN_BYTES];
+ uint32_t time[AUDIO_MIDI_IN_BYTES];
+ AudioMidiGetFnT get;
+ volatile uint32_t head; // bytes the timer stored
+ volatile uint32_t tail; // bytes the program side took
+ volatile uint32_t lost; // bytes dropped: the ring was full
+ volatile uint32_t ms; // the timer's milliseconds since open
+} MidiInRingT;
+
+// The message being put together (program side)
+typedef struct {
+ uint8_t bytes[MIDI_MSG_MAX];
+ uint32_t ms; // when its first byte came
+ uint8_t have;
+ uint8_t need; // data bytes its status takes
+ uint8_t running; // the running status (0: none)
+ bool sysex; // inside a system exclusive message
+} MidiInMsgT;
+
+static MidiInRingT sIn;
+static MidiInMsgT sMsg;
+static AudioCtxT *sInOwner = NULL;
+static int32_t sInTimer = TIMER_NONE;
+static bool sInLocked = false;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+uint32_t audioMidiInClock(const AudioCtxT *ctx);
+void audioMidiInClose(AudioCtxT *ctx);
+void audioMidiInCtxClosed(const AudioCtxT *ctx);
+uint32_t audioMidiInLost(const AudioCtxT *ctx);
+int32_t audioMidiInOpen(AudioCtxT *ctx);
+bool audioMidiInOwned(void);
+bool audioMidiInRead(AudioCtxT *ctx, AudioMidiMsgT *msg);
+void audioMidiInSuspend(void);
+static uint8_t dataBytes(uint8_t status);
+static void inPoll(void *unused);
+static bool msgDone(AudioMidiMsgT *msg);
+
+
+uint32_t audioMidiInClock(const AudioCtxT *ctx) {
+ return (ctx && ctx == sInOwner) ? sIn.ms : 0;
+}
+
+
+void audioMidiInClose(AudioCtxT *ctx) {
+ if (!ctx || ctx != sInOwner) {
+ return;
+ }
+
+ platformTimerRemove(sInTimer);
+ sInTimer = TIMER_NONE;
+ sInOwner = NULL;
+ dvxLog("Audio: MIDI input closed (%lu bytes lost)", (unsigned long)sIn.lost);
+}
+
+
+void audioMidiInCtxClosed(const AudioCtxT *ctx) {
+ if (ctx && ctx == sInOwner) {
+ audioMidiInClose(sInOwner);
+ }
+}
+
+
+uint32_t audioMidiInLost(const AudioCtxT *ctx) {
+ return (ctx && ctx == sInOwner) ? sIn.lost : 0;
+}
+
+
+// The ring and the timer's code stay pinned once pinned (a few KB).
+int32_t audioMidiInOpen(AudioCtxT *ctx) {
+ if (!ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ AudioMidiGetFnT get = audioMidiInPort();
+
+ if (!get) {
+ return AUDIO_ERR_NO_SYNTH;
+ }
+
+ if (sInOwner) {
+ return sInOwner == ctx ? AUDIO_OK : AUDIO_ERR_BUSY;
+ }
+
+ if (!sInLocked) {
+ const AudioLockItemT items[] = {
+ { (const void *)inPoll, 0 },
+ { &sIn, sizeof(sIn) }
+ };
+
+ if (!audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), true)) {
+ return AUDIO_ERR_NO_MEMORY;
+ }
+
+ sInLocked = true;
+ }
+
+ memset(&sIn, 0, sizeof(sIn));
+ memset(&sMsg, 0, sizeof(sMsg));
+ sIn.get = get;
+ sInTimer = platformTimerAdd(AUDIO_MIDI_IN_HZ, inPoll, NULL, 0);
+
+ if (sInTimer < 0) {
+ sInTimer = TIMER_NONE;
+ return AUDIO_ERR_BUSY;
+ }
+
+ sInOwner = ctx;
+ dvxLog("Audio: MIDI input open");
+ return AUDIO_OK;
+}
+
+
+bool audioMidiInOwned(void) {
+ return sInOwner != NULL;
+}
+
+
+// The bytes that came, put together into the next whole message.
+bool audioMidiInRead(AudioCtxT *ctx, AudioMidiMsgT *msg) {
+ if (!ctx || ctx != sInOwner || !msg) {
+ return false;
+ }
+
+ while (sIn.tail != sIn.head) {
+ uint32_t at = sIn.tail & (AUDIO_MIDI_IN_BYTES - 1);
+ uint8_t byte = sIn.data[at];
+ uint32_t ms = sIn.time[at];
+
+ sIn.tail++;
+
+ if (byte >= MIDI_REALTIME) {
+ continue;
+ }
+
+ if (byte & MIDI_STATUS_BIT) {
+ sMsg.sysex = byte == MIDI_SYSEX;
+ sMsg.running = byte < MIDI_SYSTEM ? byte : 0;
+ sMsg.have = 0;
+
+ if (byte == MIDI_SYSEX || byte == MIDI_SYSEX_END) {
+ continue;
+ }
+
+ sMsg.bytes[sMsg.have++] = byte;
+ sMsg.need = dataBytes(byte);
+ sMsg.ms = ms;
+
+ if (sMsg.need == 0) {
+ return msgDone(msg);
+ }
+
+ continue;
+ }
+
+ if (sMsg.sysex) {
+ continue;
+ }
+
+ // A data byte with no status before it takes the running status.
+ if (sMsg.have == 0) {
+ if (!sMsg.running) {
+ continue;
+ }
+
+ sMsg.bytes[sMsg.have++] = sMsg.running;
+ sMsg.need = dataBytes(sMsg.running);
+ sMsg.ms = ms;
+ }
+
+ sMsg.bytes[sMsg.have++] = byte;
+
+ if (sMsg.have == sMsg.need + 1) {
+ return msgDone(msg);
+ }
+ }
+
+ return false;
+}
+
+
+// A DOS program is about to run (audioSuspend): reading stops.
+void audioMidiInSuspend(void) {
+ audioMidiInClose(sInOwner);
+}
+
+
+// The data bytes after a status byte (system exclusive aside).
+static uint8_t dataBytes(uint8_t status) {
+ uint8_t kind = status & MIDI_KIND_MASK;
+
+ if (status < MIDI_SYSTEM) {
+ return (kind == MIDI_PROGRAM || kind == MIDI_PRESSURE) ? 1 : 2;
+ }
+
+ if (status == MIDI_TIME_CODE || status == MIDI_SONG_SELECT) {
+ return 1;
+ }
+
+ return status == MIDI_SONG_POSITION ? 2 : 0;
+}
+
+
+// The timer: what the port has, with the time it came. Interrupt time.
+static void inPoll(void *unused) {
+ uint8_t byte;
+
+ (void)unused;
+ sIn.ms += MS_PER_SEC / AUDIO_MIDI_IN_HZ;
+
+ for (int32_t i = 0; i < POLL_BYTES_MAX && sIn.get(&byte); i++) {
+ if (sIn.head - sIn.tail >= AUDIO_MIDI_IN_BYTES) {
+ sIn.lost++;
+ continue;
+ }
+
+ uint32_t at = sIn.head & (AUDIO_MIDI_IN_BYTES - 1);
+
+ sIn.data[at] = byte;
+ sIn.time[at] = sIn.ms;
+ sIn.head++;
+ }
+}
+
+
+// The message put together, handed out; the next starts empty (running
+// status stays).
+static bool msgDone(AudioMidiMsgT *msg) {
+ msg->ms = sMsg.ms;
+ msg->status = sMsg.bytes[0];
+ msg->data1 = sMsg.have > 1 ? sMsg.bytes[1] : 0;
+ msg->data2 = sMsg.have > 2 ? sMsg.bytes[2] : 0;
+ msg->length = sMsg.have;
+ sMsg.have = 0;
+ return true;
+}
diff --git a/src/libs/kpunch/audio/audioMix.c b/src/libs/kpunch/audio/audioMix.c
index b7b4794d..70b5b8da 100644
--- a/src/libs/kpunch/audio/audioMix.c
+++ b/src/libs/kpunch/audio/audioMix.c
@@ -70,7 +70,8 @@ typedef struct {
uint8_t buf[CLOCK_MAX_FRAMES * CLOCK_FRAME_BYTES]; // output nobody plays
} MixClockT;
-AudioMixT audioMixer;
+AudioMixT audioMixer;
+AudioRecRingT *volatile audioRecRing = NULL;
static MixClockT sClock = { .timerId = CLOCK_TIMER_NONE };
@@ -85,12 +86,15 @@ 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);
+void audioRecPush(const void *src, uint32_t bytes);
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 mixSample(MixVoiceT *v, int32_t *acc, uint32_t n);
static void mixSource(MixVoiceT *v, int32_t *acc, uint32_t n);
+static void mixStraight(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);
@@ -142,18 +146,22 @@ bool audioLockSpan(const AudioLockItemT *items, int32_t count, bool lock) {
bool audioMixLock(bool lock) {
const AudioLockItemT items[] = {
{ (const void *)audioMixRender, 0 },
+ { (const void *)audioRecPush, 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 *)mixStraight, 0 },
+ { (const void *)mixSample, 0 },
{ (const void *)mixTone, 0 },
{ (const void *)toneSample, 0 },
{ (const void *)toneSelect, 0 },
{ (const void *)writeOut, 0 },
{ &audioMixer, sizeof(audioMixer) },
- { &sClock, sizeof(sClock) }
+ { &sClock, sizeof(sClock) },
+ { (const void *)&audioRecRing, sizeof(audioRecRing) }
};
return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
@@ -198,7 +206,7 @@ void audioMixRender(void *dst, uint32_t frames) {
for (int32_t i = 0; i < AUDIO_VOICES_MAX; i++) {
MixVoiceT *v = &audioMixer.voices[i];
- if (v->state != MIX_PLAYING) {
+ if (v->state != MIX_PLAYING || v->paused) {
continue;
}
@@ -206,6 +214,8 @@ void audioMixRender(void *dst, uint32_t frames) {
mixTone(v, audioMixer.acc, n);
} else if (v->kind == MIX_KIND_NOTES) {
mixNotes(v, audioMixer.acc, n);
+ } else if (v->kind == MIX_KIND_SAMPLE) {
+ mixSample(v, audioMixer.acc, n);
} else {
mixSource(v, audioMixer.acc, n);
}
@@ -240,6 +250,31 @@ void audioMixReset(uint32_t rate, uint32_t bits, uint32_t channels, AudioToneFnT
}
+// Copied whole into the recording's ring, or dropped whole when it does
+// not fit (lost), so the ring never holds part of a frame.
+void audioRecPush(const void *src, uint32_t bytes) {
+ AudioRecRingT *r = audioRecRing;
+
+ if (!r) {
+ return;
+ }
+
+ uint32_t head = r->head;
+ const uint8_t *s = (const uint8_t *)src;
+
+ if (bytes > r->mask + 1 - (head - r->tail)) {
+ r->lost += bytes;
+ return;
+ }
+
+ for (uint32_t i = 0; i < bytes; i++) {
+ r->data[(head + i) & r->mask] = s[i];
+ }
+
+ r->head = head + bytes;
+}
+
+
static int32_t clipSample(int32_t s) {
if (s > MIX_SAMPLE_MAX) {
audioMixer.clipped++;
@@ -277,7 +312,9 @@ static void endVoice(MixVoiceT *v, AudioEndE reason) {
// 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).
+// silence (counted as an underrun once it has had samples). A source has
+// one or two channels (audio.c checks): a frame's index is a shift, not
+// a multiply.
static bool loadNext(MixVoiceT *v) {
if (v->kind == MIX_KIND_STREAM) {
AudioRingT *r = v->ring;
@@ -293,7 +330,7 @@ static bool loadNext(MixVoiceT *v) {
return true;
}
- const int16_t *f = &r->data[(r->tail & r->mask) * r->channels];
+ const int16_t *f = &r->data[(r->tail & r->mask) << (r->channels - 1)];
v->nextL = f[0];
v->nextR = f[r->channels - 1];
@@ -309,7 +346,7 @@ static bool loadNext(MixVoiceT *v) {
v->pos = 0;
}
- const int16_t *f = &v->data[v->pos * v->channels];
+ const int16_t *f = &v->data[v->pos << (v->channels - 1)];
v->nextL = f[0];
v->nextR = f[v->channels - 1];
@@ -360,36 +397,208 @@ static void mixNotes(MixVoiceT *v, int32_t *acc, uint32_t n) {
}
-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;
+// A sample (see the top): as mixSource, with its data walked by a
+// pointer instead of a loadNext call a source frame; what it mixes and
+// where it leaves the voice are the same. One at the output rate is
+// mixStraight's.
+static void mixSample(MixVoiceT *v, int32_t *acc, uint32_t n) {
+ uint32_t step = v->step;
+
+ if (step == MIX_FRAC_ONE && v->frac == 0) {
+ mixStraight(v, acc, n);
+ return;
+ }
+
+ int32_t gainL = v->gainL;
+ int32_t gainR = v->gainR;
+ uint32_t frac = v->frac;
+ uint32_t shift = v->channels - 1u;
+ const int16_t *data = v->data;
+ const int16_t *end = data + (v->frames << shift);
+ const int16_t *p = data + (v->pos << shift);
+ int32_t curL = v->curL;
+ int32_t curR = v->curR;
+ int32_t nextL = v->nextL;
+ int32_t nextR = v->nextR;
+ bool draining = v->draining;
+ bool mono = shift == 0;
+ bool shared = mono && gainL == gainR;
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);
+ int32_t w = (int32_t)(frac >> (MIX_FRAC_BITS - MIX_LERP_SHIFT));
+ int32_t l = curL;
+ int32_t r = curR;
- acc[i * MIX_ACC_CHANNELS] += l * gainL;
- acc[i * MIX_ACC_CHANNELS + 1] += r * gainR;
- v->frac += step;
+ if (w) {
+ l += ((nextL - curL) * w) >> MIX_LERP_SHIFT;
+ r = mono ? l : r + (((nextR - curR) * w) >> MIX_LERP_SHIFT);
+ }
- while (v->frac >= MIX_FRAC_ONE) {
- v->frac -= MIX_FRAC_ONE;
+ if (shared) {
+ int32_t g = l * gainL;
+
+ acc[i * MIX_ACC_CHANNELS] += g;
+ acc[i * MIX_ACC_CHANNELS + 1] += g;
+ } else {
+ acc[i * MIX_ACC_CHANNELS] += l * gainL;
+ acc[i * MIX_ACC_CHANNELS + 1] += r * gainR;
+ }
+
+ frac += step;
+
+ while (frac >= MIX_FRAC_ONE) {
+ frac -= MIX_FRAC_ONE;
+
+ if (draining) {
+ endVoice(v, AUDIO_END_FINISHED);
+ return;
+ }
+
+ curL = nextL;
+ curR = nextR;
+
+ if (p >= end) {
+ if (!v->loop) {
+ draining = true;
+ continue;
+ }
+
+ p = data;
+ }
+
+ nextL = p[0];
+ nextR = p[shift];
+ p += shift + 1;
+ }
+ }
+
+ v->frac = frac;
+ v->pos = (uint32_t)(p - data) >> shift;
+ v->curL = curL;
+ v->curR = curR;
+ v->nextL = nextL;
+ v->nextR = nextR;
+ v->draining = draining;
+}
+
+
+// A stream (see the top), its position kept in locals for the chunk. The
+// multiplies are what a 486 feels, so none is done whose result is known:
+// a weight of 0 (a stream at the output rate never has another) leaves
+// cur as it is, a mono source's right is its left, and a mono source with
+// equal gains (centred) is scaled once.
+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;
+ uint32_t frac = v->frac;
+ int32_t curL = v->curL;
+ int32_t curR = v->curR;
+ bool mono = v->channels == 1;
+ bool shared = mono && gainL == gainR;
+
+ for (uint32_t i = 0; i < n; i++) {
+ int32_t w = (int32_t)(frac >> (MIX_FRAC_BITS - MIX_LERP_SHIFT));
+ int32_t l = curL;
+ int32_t r = curR;
+
+ if (w) {
+ l += ((v->nextL - curL) * w) >> MIX_LERP_SHIFT;
+ r = mono ? l : r + (((v->nextR - curR) * w) >> MIX_LERP_SHIFT);
+ }
+
+ if (shared) {
+ int32_t g = l * gainL;
+
+ acc[i * MIX_ACC_CHANNELS] += g;
+ acc[i * MIX_ACC_CHANNELS + 1] += g;
+ } else {
+ acc[i * MIX_ACC_CHANNELS] += l * gainL;
+ acc[i * MIX_ACC_CHANNELS + 1] += r * gainR;
+ }
+
+ frac += step;
+
+ while (frac >= MIX_FRAC_ONE) {
+ frac -= MIX_FRAC_ONE;
if (v->draining) {
endVoice(v, AUDIO_END_FINISHED);
return;
}
- v->curL = v->nextL;
- v->curR = v->nextR;
+ curL = v->nextL;
+ curR = v->nextR;
if (!loadNext(v)) {
v->draining = true;
}
}
}
+
+ v->frac = frac;
+ v->curL = curL;
+ v->curR = curR;
+}
+
+
+// A sample at the output rate (step 1.0 and no fraction: each output
+// frame is a source frame, no weights): mixSample's loop without the
+// position arithmetic, which a 486 feels at every frame (registers run
+// out). Its fetch is mixSample's.
+static void mixStraight(MixVoiceT *v, int32_t *acc, uint32_t n) {
+ int32_t gainL = v->gainL;
+ int32_t gainR = v->gainR;
+ uint32_t shift = v->channels - 1u;
+ const int16_t *data = v->data;
+ const int16_t *end = data + (v->frames << shift);
+ const int16_t *p = data + (v->pos << shift);
+ int32_t curL = v->curL;
+ int32_t curR = v->curR;
+ int32_t nextL = v->nextL;
+ int32_t nextR = v->nextR;
+ bool draining = v->draining;
+ bool shared = shift == 0 && gainL == gainR;
+
+ for (uint32_t i = 0; i < n; i++) {
+ if (shared) {
+ int32_t g = curL * gainL;
+
+ acc[i * MIX_ACC_CHANNELS] += g;
+ acc[i * MIX_ACC_CHANNELS + 1] += g;
+ } else {
+ acc[i * MIX_ACC_CHANNELS] += curL * gainL;
+ acc[i * MIX_ACC_CHANNELS + 1] += curR * gainR;
+ }
+
+ if (draining) {
+ endVoice(v, AUDIO_END_FINISHED);
+ return;
+ }
+
+ curL = nextL;
+ curR = nextR;
+
+ if (p >= end) {
+ if (!v->loop) {
+ draining = true;
+ continue;
+ }
+
+ p = data;
+ }
+
+ nextL = p[0];
+ nextR = p[shift];
+ p += shift + 1;
+ }
+
+ v->pos = (uint32_t)(p - data) >> shift;
+ v->curL = curL;
+ v->curR = curR;
+ v->nextL = nextL;
+ v->nextR = nextR;
+ v->draining = draining;
}
diff --git a/src/libs/kpunch/audio/audioRec.c b/src/libs/kpunch/audio/audioRec.c
new file mode 100644
index 00000000..45a26ec0
--- /dev/null
+++ b/src/libs/kpunch/audio/audioRec.c
@@ -0,0 +1,473 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// audioRec.c -- recording (see audio.h, "Recording")
+//
+// One recording at a time, for the whole of DVX. The driver's interrupt
+// hands each block it recorded to audioRecPush (audioMix.c, pinned with
+// the mixer), which only copies the bytes into the recording's ring; this
+// side takes them out (audioRecRead, or, for a recording into a file,
+// audioRecIdle writing them there from audioPump) and finds the loudest
+// sample on the way.
+//
+// The card records or plays, not both: audio.c's audioDevRecordBegin
+// closes the output and keeps new voices out while the recording runs;
+// audioRecClose gives the card back (the driver's recordStop) and the
+// voices that were held play on.
+
+#include "audioInt.h"
+
+#include
+#include
+
+// The ring holds the buffer asked for and a quarter more (the card's rate
+// may be a little above the one asked)
+#define RING_MARGIN_NUM 5
+#define RING_MARGIN_DEN 4
+
+#define FILE_COPY_BYTES 4096 // moved to the file per write
+#define MS_PER_SEC 1000
+#define BITS_PER_BYTE 8
+#define BITS_8 8
+#define BITS_16 16
+#define CHANNELS_MONO 1
+#define CHANNELS_STEREO 2
+#define PEAK_FULL 32767
+#define PERCENT 100
+#define U8_BIAS 128
+#define U8_SCALE 256 // an 8-bit sample's step in 16-bit terms
+#define BYTE_SHIFT 8
+#define BYTE_MASK 0xFF
+#define WORD_SHIFT 16
+#define WORD_MASK 0xFFFF
+#define WORD_BYTES 2
+
+// WAV: RIFF, the fmt chunk (PCM), the data chunk's header
+#define WAV_RIFF_EXTRA 36 // the RIFF size is the data bytes and this
+#define WAV_FMT_BYTES 16
+#define WAV_FORMAT_PCM 1
+#define WAV_AT_RIFF_SIZE 4
+#define WAV_AT_WAVE 8
+#define WAV_AT_FMT 12
+#define WAV_AT_FMT_SIZE 16
+#define WAV_AT_FORMAT 20
+#define WAV_AT_CHANNELS 22
+#define WAV_AT_RATE 24
+#define WAV_AT_BYTE_RATE 28
+#define WAV_AT_ALIGN 32
+#define WAV_AT_BITS 34
+#define WAV_AT_DATA 36
+#define WAV_AT_DATA_SIZE 40
+#define FOURCC_BYTES 4
+
+struct AudioRecT {
+ AudioRecRingT ring; // first: pinned with the struct (the interrupt writes it)
+ AudioCtxT *owner;
+ AudioFormatT fmt;
+ FILE *fp; // audioRecFile: where the frames go (else NULL)
+ uint32_t fileBytes; // sound bytes written to fp
+ uint32_t taken; // frames read or written so far
+ int32_t peak; // the loudest sample taken since audioRecPeak (0..PEAK_FULL)
+ int32_t error; // AUDIO_ERR_WRITE: a write to fp failed
+};
+
+static AudioRecT *sRec = NULL;
+static const AudioDriverT *sRecDrv = NULL;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+int32_t audioRecClose(AudioRecT *rec);
+void audioRecCtxClosed(const AudioCtxT *ctx);
+int32_t audioRecFile(AudioCtxT *ctx, const char *path, AudioFormatT *fmt, int32_t source, AudioRecT **out);
+void audioRecFormat(const AudioRecT *rec, AudioFormatT *fmt);
+uint32_t audioRecFrames(const AudioRecT *rec);
+void audioRecIdle(void);
+uint32_t audioRecLost(const AudioRecT *rec);
+int32_t audioRecOpen(AudioCtxT *ctx, AudioFormatT *fmt, int32_t source, uint32_t bufferMs, AudioRecT **out);
+int32_t audioRecPeak(AudioRecT *rec);
+uint32_t audioRecQueued(const AudioRecT *rec);
+uint32_t audioRecRead(AudioRecT *rec, void *pcm, uint32_t frames);
+void audioRecSuspend(void);
+void audioWavHeader(uint8_t *out, const AudioFormatT *fmt, uint32_t dataBytes);
+static uint32_t frameBytesOf(const AudioFormatT *fmt);
+static void put16(uint8_t *p, uint32_t v);
+static void put32(uint8_t *p, uint32_t v);
+static void putTag(uint8_t *p, const char *tag);
+static AudioRecT *recAlloc(uint32_t bytes);
+static int32_t recBegin(AudioCtxT *ctx, AudioFormatT *fmt, int32_t source, uint32_t bufferMs, const char *path, AudioRecT **out);
+static void recDrain(AudioRecT *rec);
+static void recFree(AudioRecT *rec);
+static void recPeak(AudioRecT *rec, const uint8_t *p, uint32_t bytes);
+static uint32_t recTake(AudioRecT *rec, uint8_t *dst, uint32_t bytes);
+
+
+// The card stops first, so nothing is pushed while the ring is emptied
+// and freed; the held voices play on (audioDevRecordEnd).
+int32_t audioRecClose(AudioRecT *rec) {
+ if (!rec) {
+ return AUDIO_OK;
+ }
+
+ sRecDrv->recordStop();
+ audioRecRing = NULL;
+
+ if (rec->fp) {
+ uint8_t head[AUDIO_WAV_HEADER_BYTES];
+
+ recDrain(rec);
+ audioWavHeader(head, &rec->fmt, rec->fileBytes);
+
+ if (fseek(rec->fp, 0, SEEK_SET) != 0 || fwrite(head, sizeof(head), 1, rec->fp) != 1) {
+ rec->error = AUDIO_ERR_WRITE;
+ }
+
+ if (fclose(rec->fp) != 0) {
+ rec->error = AUDIO_ERR_WRITE;
+ }
+
+ rec->fp = NULL;
+ }
+
+ int32_t err = rec->error;
+
+ dvxLog("Audio: recording ended: %lu frames, %lu lost", (unsigned long)rec->taken, (unsigned long)audioRecLost(rec));
+ recFree(rec);
+ audioDevRecordEnd();
+ return err;
+}
+
+
+// A closing context's recording goes with it.
+void audioRecCtxClosed(const AudioCtxT *ctx) {
+ if (sRec && sRec->owner == ctx) {
+ audioRecClose(sRec);
+ }
+}
+
+
+int32_t audioRecFile(AudioCtxT *ctx, const char *path, AudioFormatT *fmt, int32_t source, AudioRecT **out) {
+ *out = NULL;
+
+ if (!path || !path[0]) {
+ return AUDIO_ERR_ARG;
+ }
+
+ return recBegin(ctx, fmt, source, 0, path, out);
+}
+
+
+void audioRecFormat(const AudioRecT *rec, AudioFormatT *fmt) {
+ *fmt = rec->fmt;
+}
+
+
+uint32_t audioRecFrames(const AudioRecT *rec) {
+ return rec->taken;
+}
+
+
+// The shell's idle time (audioPump): a recording into a file is written.
+void audioRecIdle(void) {
+ if (sRec && sRec->fp) {
+ recDrain(sRec);
+ }
+}
+
+
+uint32_t audioRecLost(const AudioRecT *rec) {
+ return rec->ring.lost / rec->ring.frameBytes;
+}
+
+
+int32_t audioRecOpen(AudioCtxT *ctx, AudioFormatT *fmt, int32_t source, uint32_t bufferMs, AudioRecT **out) {
+ *out = NULL;
+ return recBegin(ctx, fmt, source, bufferMs, NULL, out);
+}
+
+
+int32_t audioRecPeak(AudioRecT *rec) {
+ int32_t peak = rec->peak;
+
+ rec->peak = 0;
+ return peak * PERCENT / PEAK_FULL;
+}
+
+
+uint32_t audioRecQueued(const AudioRecT *rec) {
+ return (rec->ring.head - rec->ring.tail) / rec->ring.frameBytes;
+}
+
+
+uint32_t audioRecRead(AudioRecT *rec, void *pcm, uint32_t frames) {
+ if (!rec || !pcm || rec->fp) {
+ return 0;
+ }
+
+ uint32_t got = recTake(rec, (uint8_t *)pcm, frames * rec->ring.frameBytes) / rec->ring.frameBytes;
+
+ rec->taken += got;
+ return got;
+}
+
+
+// A DOS program is about to run (audioSuspend): the recording ends.
+void audioRecSuspend(void) {
+ audioRecClose(sRec);
+}
+
+
+void audioWavHeader(uint8_t *out, const AudioFormatT *fmt, uint32_t dataBytes) {
+ uint32_t align = frameBytesOf(fmt);
+
+ putTag(out, "RIFF");
+ put32(out + WAV_AT_RIFF_SIZE, dataBytes + WAV_RIFF_EXTRA);
+ putTag(out + WAV_AT_WAVE, "WAVE");
+ putTag(out + WAV_AT_FMT, "fmt ");
+ put32(out + WAV_AT_FMT_SIZE, WAV_FMT_BYTES);
+ put16(out + WAV_AT_FORMAT, WAV_FORMAT_PCM);
+ put16(out + WAV_AT_CHANNELS, fmt->channels);
+ put32(out + WAV_AT_RATE, fmt->rate);
+ put32(out + WAV_AT_BYTE_RATE, fmt->rate * align);
+ put16(out + WAV_AT_ALIGN, align);
+ put16(out + WAV_AT_BITS, fmt->bits);
+ putTag(out + WAV_AT_DATA, "data");
+ put32(out + WAV_AT_DATA_SIZE, dataBytes);
+}
+
+
+static uint32_t frameBytesOf(const AudioFormatT *fmt) {
+ return fmt->channels * fmt->bits / BITS_PER_BYTE;
+}
+
+
+static void put16(uint8_t *p, uint32_t v) {
+ p[0] = (uint8_t)(v & BYTE_MASK);
+ p[1] = (uint8_t)((v >> BYTE_SHIFT) & BYTE_MASK);
+}
+
+
+static void put32(uint8_t *p, uint32_t v) {
+ put16(p, v & WORD_MASK);
+ put16(p + WORD_BYTES, v >> WORD_SHIFT);
+}
+
+
+// A chunk's four-character tag (no terminator).
+static void putTag(uint8_t *p, const char *tag) {
+ for (int32_t i = 0; i < FOURCC_BYTES; i++) {
+ p[i] = (uint8_t)tag[i];
+ }
+}
+
+
+// A recording with a ring of bytes, both pinned (the interrupt writes
+// them); NULL when out of memory.
+static AudioRecT *recAlloc(uint32_t bytes) {
+ AudioRecT *rec = (AudioRecT *)calloc(1, sizeof(AudioRecT));
+ uint8_t *data = (uint8_t *)malloc(bytes);
+
+ if (!rec || !data || platformLockRegion(rec, sizeof(*rec), false) != 0) {
+ free(data);
+ free(rec);
+ return NULL;
+ }
+
+ if (platformLockRegion(data, bytes, false) != 0) {
+ platformUnlockRegion(rec, sizeof(*rec), false);
+ free(data);
+ free(rec);
+ return NULL;
+ }
+
+ rec->ring.data = data;
+ rec->ring.mask = bytes - 1;
+ return rec;
+}
+
+
+// Fill in what a 0 asks for and check everything that can be checked --
+// the arguments, no other recording, the card's record source -- before
+// the file (path, NULL for none) is made, so a recording that cannot
+// start leaves a file of that name alone. The ring is set up for the
+// format asked before the card starts, so no block it records is lost.
+// The file starts with a header for no sound yet: one cut short (DVX
+// ended) still reads as an empty WAV file.
+static int32_t recBegin(AudioCtxT *ctx, AudioFormatT *fmt, int32_t source, uint32_t bufferMs, const char *path, AudioRecT **out) {
+ AudioFormatT want = *fmt;
+
+ want.rate = want.rate ? want.rate : AUDIO_RATE_DEFAULT;
+ want.bits = want.bits ? want.bits : AUDIO_REC_BITS_DEFAULT;
+ want.channels = want.channels ? want.channels : CHANNELS_MONO;
+
+ if (!ctx || source < 0 || source >= AUDIO_REC_SOURCES || want.rate < AUDIO_RATE_MIN || want.rate > AUDIO_RATE_MAX || (want.bits != BITS_8 && want.bits != BITS_16) || want.channels > CHANNELS_STEREO) {
+ return AUDIO_ERR_ARG;
+ }
+
+ if (sRec) {
+ return AUDIO_ERR_BUSY;
+ }
+
+ const AudioDriverT *drv = NULL;
+ int32_t rc = audioDevRecordBegin(source, &drv);
+
+ if (rc != AUDIO_OK) {
+ return rc;
+ }
+
+ uint32_t ms = bufferMs ? bufferMs : AUDIO_REC_MS_DEFAULT;
+ uint32_t bytes = 1;
+
+ ms = ms < AUDIO_REC_MS_MIN ? AUDIO_REC_MS_MIN : (ms > AUDIO_REC_MS_MAX ? AUDIO_REC_MS_MAX : ms);
+
+ uint64_t need = (uint64_t)want.rate * frameBytesOf(&want) * ms * RING_MARGIN_NUM / ((uint64_t)MS_PER_SEC * RING_MARGIN_DEN);
+
+ while (bytes < need) {
+ bytes <<= 1;
+ }
+
+ FILE *fp = path ? fopen(path, "wb") : NULL;
+ AudioRecT *rec = (!path || fp) ? recAlloc(bytes) : NULL;
+
+ if (!rec) {
+ if (fp) {
+ fclose(fp);
+ remove(path);
+ }
+
+ audioDevRecordEnd();
+ return path && !fp ? AUDIO_ERR_WRITE : AUDIO_ERR_NO_MEMORY;
+ }
+
+ rec->owner = ctx;
+ rec->fp = fp;
+ sRec = rec;
+ sRecDrv = drv;
+ audioRecRing = &rec->ring;
+
+ if (!drv->recordStart(&want, source, &rec->fmt)) {
+ audioRecRing = NULL;
+ recFree(rec);
+
+ if (fp) {
+ fclose(fp);
+ remove(path);
+ }
+
+ audioDevRecordEnd();
+ dvxLog("Audio: the sound device did not start recording");
+ return AUDIO_ERR_NO_RECORD;
+ }
+
+ rec->ring.frameBytes = frameBytesOf(&rec->fmt);
+
+ if (fp) {
+ uint8_t head[AUDIO_WAV_HEADER_BYTES];
+
+ audioWavHeader(head, &rec->fmt, 0);
+
+ if (fwrite(head, sizeof(head), 1, fp) != 1) {
+ rec->fp = NULL;
+ audioRecClose(rec);
+ fclose(fp);
+ remove(path);
+ return AUDIO_ERR_WRITE;
+ }
+ }
+
+ *fmt = rec->fmt;
+ *out = rec;
+ dvxLog("Audio: recording at %lu Hz, %lu-bit, %s", (unsigned long)rec->fmt.rate, (unsigned long)rec->fmt.bits, rec->fmt.channels == CHANNELS_STEREO ? "stereo" : "mono");
+ return AUDIO_OK;
+}
+
+
+// Everything recorded so far into the file.
+static void recDrain(AudioRecT *rec) {
+ static uint8_t buf[FILE_COPY_BYTES];
+ uint32_t n;
+
+ while ((n = recTake(rec, buf, sizeof(buf))) > 0) {
+ if (rec->error == AUDIO_OK && fwrite(buf, n, 1, rec->fp) != 1) {
+ rec->error = AUDIO_ERR_WRITE;
+ }
+
+ rec->fileBytes += n;
+ rec->taken += n / rec->ring.frameBytes;
+ }
+}
+
+
+static void recFree(AudioRecT *rec) {
+ platformUnlockRegion(rec->ring.data, rec->ring.mask + 1, false);
+ platformUnlockRegion(rec, sizeof(*rec), false);
+ free(rec->ring.data);
+ free(rec);
+
+ if (sRec == rec) {
+ sRec = NULL;
+ }
+}
+
+
+// The loudest sample of bytes of the recording's format at p, kept.
+static void recPeak(AudioRecT *rec, const uint8_t *p, uint32_t bytes) {
+ int32_t peak = rec->peak;
+
+ if (rec->fmt.bits == BITS_16) {
+ for (uint32_t i = 0; i + 1 < bytes; i += WORD_BYTES) {
+ int32_t v = (int16_t)(p[i] | (p[i + 1] << BYTE_SHIFT));
+
+ v = v < 0 ? -v : v;
+ peak = v > peak ? v : peak;
+ }
+ } else {
+ for (uint32_t i = 0; i < bytes; i++) {
+ int32_t v = ((int32_t)p[i] - U8_BIAS) * U8_SCALE;
+
+ v = v < 0 ? -v : v;
+ peak = v > peak ? v : peak;
+ }
+ }
+
+ rec->peak = peak > PEAK_FULL ? PEAK_FULL : peak;
+}
+
+
+// Up to bytes of whole frames out of the ring into dst.
+static uint32_t recTake(AudioRecT *rec, uint8_t *dst, uint32_t bytes) {
+ AudioRecRingT *r = &rec->ring;
+ uint32_t tail = r->tail;
+ uint32_t avail = r->head - tail;
+ uint32_t n = bytes < avail ? bytes : avail;
+
+ n -= n % r->frameBytes;
+
+ for (uint32_t i = 0; i < n; i++) {
+ dst[i] = r->data[(tail + i) & r->mask];
+ }
+
+ recPeak(rec, dst, n);
+ r->tail = tail + n;
+ return n;
+}
diff --git a/src/libs/kpunch/audio/audioSet.c b/src/libs/kpunch/audio/audioSet.c
index 7a869d16..05c53016 100644
--- a/src/libs/kpunch/audio/audioSet.c
+++ b/src/libs/kpunch/audio/audioSet.c
@@ -29,10 +29,21 @@
#define PERCENT_MAX 100
const PrefsChoiceT audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] = {
- { "Automatic", AUDIO_MIDI_AUTO, 0 },
- { "MIDI port (MPU-401)", AUDIO_MIDI_MPU401, 1 },
- { "FM synthesizer chip", AUDIO_MIDI_FM, 2 },
- { "Built-in synthesizer", AUDIO_MIDI_SOFTWARE, 3 }
+ { "Automatic", AUDIO_MIDI_AUTO, 0 },
+ { "MIDI port (MPU-401)", AUDIO_MIDI_MPU401, 1 },
+ { "FM synthesizer chip", AUDIO_MIDI_FM, 2 },
+ { "Built-in synthesizer", AUDIO_MIDI_SOFTWARE, 3 },
+ { "Built-in SoundFont synthesizer", AUDIO_MIDI_SOFTWAVE, 4 }
+};
+
+const PrefsChoiceT audioMidiInChoices[AUDIO_MIDI_IN_CHOICE_COUNT] = {
+ { "None", AUDIO_MIDI_IN_NONE, 0 },
+ { "MIDI port", AUDIO_MIDI_MPU401, 1 }
+};
+
+const PrefsChoiceT audioModulesChoices[AUDIO_MODULES_CHOICE_COUNT] = {
+ { "Wavetable synthesizer when free", AUDIO_MODULES_WAVETABLE, 0 },
+ { "Mixed in software", AUDIO_MODULES_SOFTWARE, 1 }
};
// ============================================================
@@ -42,7 +53,10 @@ const PrefsChoiceT audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] = {
int32_t audioPercentToVolume(int32_t percent);
const char *audioSettingDriver(PrefsHandleT *prefs);
int32_t audioSettingMidi(PrefsHandleT *prefs);
-void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t volume);
+int32_t audioSettingMidiIn(PrefsHandleT *prefs);
+void audioSettingMidiInStore(PrefsHandleT *prefs, int32_t midiIn);
+int32_t audioSettingModules(PrefsHandleT *prefs);
+void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume);
int32_t audioSettingVolume(PrefsHandleT *prefs);
static int32_t clampPercent(int32_t percent);
@@ -62,14 +76,38 @@ int32_t audioSettingMidi(PrefsHandleT *prefs) {
}
-void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t volume) {
+int32_t audioSettingMidiIn(PrefsHandleT *prefs) {
+ return prefsGetChoice(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI_IN, audioMidiInChoices, AUDIO_MIDI_IN_CHOICE_COUNT, AUDIO_MIDI_IN_NONE);
+}
+
+
+void audioSettingMidiInStore(PrefsHandleT *prefs, int32_t midiIn) {
+ if (midiIn < 0 || midiIn >= AUDIO_MIDI_IN_CHOICE_COUNT) {
+ midiIn = prefsChoiceIndex(audioMidiInChoices, AUDIO_MIDI_IN_CHOICE_COUNT, AUDIO_MIDI_IN_NONE);
+ }
+
+ prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI_IN, audioMidiInChoices[midiIn].iniName);
+}
+
+
+int32_t audioSettingModules(PrefsHandleT *prefs) {
+ return prefsGetChoice(prefs, AUDIO_INI_SECTION, AUDIO_INI_MODULES, audioModulesChoices, AUDIO_MODULES_CHOICE_COUNT, AUDIO_MODULES_WAVETABLE);
+}
+
+
+void audioSettingsStore(PrefsHandleT *prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume) {
if (midi < 0 || midi >= AUDIO_MIDI_CHOICE_COUNT) {
midi = prefsChoiceIndex(audioMidiChoices, AUDIO_MIDI_CHOICE_COUNT, AUDIO_MIDI_AUTO);
}
+ if (modules < 0 || modules >= AUDIO_MODULES_CHOICE_COUNT) {
+ modules = prefsChoiceIndex(audioModulesChoices, AUDIO_MODULES_CHOICE_COUNT, AUDIO_MODULES_WAVETABLE);
+ }
+
prefsSetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, clampPercent(volume));
prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, (driver && driver[0]) ? driver : AUDIO_DRIVER_AUTO);
prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, audioMidiChoices[midi].iniName);
+ prefsSetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MODULES, audioModulesChoices[modules].iniName);
}
diff --git a/src/libs/kpunch/audio/audioShell.c b/src/libs/kpunch/audio/audioShell.c
index e6a0feaf..939e0d06 100644
--- a/src/libs/kpunch/audio/audioShell.c
+++ b/src/libs/kpunch/audio/audioShell.c
@@ -25,7 +25,8 @@
// 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
+// when DVX steps aside for a DOS program (the sound device is let go),
+// 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.
@@ -40,12 +41,14 @@ static void audioShellShutdown(void) __attribute__((destructor));
static void audioShellInit(void) {
shellRegisterIdle(audioIdle, NULL);
shellRegisterAppCleanup(audioAppEnded);
+ platformSuspendHookAdd(audioSuspend);
audioDeskInit();
}
static void audioShellShutdown(void) {
audioShutdown();
+ platformSuspendHookRemove(audioSuspend);
shellUnregisterAppCleanup(audioAppEnded);
shellUnregisterIdle(audioIdle, NULL);
}
diff --git a/src/libs/kpunch/audio/audioSynth.c b/src/libs/kpunch/audio/audioSynth.c
index 66c80bf4..319bb790 100644
--- a/src/libs/kpunch/audio/audioSynth.c
+++ b/src/libs/kpunch/audio/audioSynth.c
@@ -106,7 +106,9 @@ typedef struct {
typedef struct {
bool (*put)(uint8_t byte); // NULL: no MIDI port
+ bool (*get)(uint8_t *byte); // NULL: no MIDI input
AudioCtxT *owner;
+ bool inWanted; // DVX.INI chose its input (midiIn)
char description[AUDIO_DESC_MAX];
} MidiPortT;
@@ -131,7 +133,7 @@ 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 audioMidiAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info, bool inWanted);
void audioMidiDetach(void);
void audioMidiGetInfo(AudioMidiInfoT *info);
int32_t audioMidiPut(AudioCtxT *ctx, uint8_t byte);
@@ -413,11 +415,13 @@ int32_t audioMidiAcquire(AudioCtxT *ctx) {
}
-void audioMidiAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info) {
+void audioMidiAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info, bool inWanted) {
memset(&sMidi, 0, sizeof(sMidi));
if (drv) {
- sMidi.put = drv->put;
+ sMidi.put = drv->put;
+ sMidi.get = drv->get;
+ sMidi.inWanted = inWanted;
snprintf(sMidi.description, sizeof(sMidi.description), "%s", info->description);
}
}
@@ -431,8 +435,17 @@ void audioMidiDetach(void) {
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;
+ info->present = sMidi.put != NULL;
+ info->owned = sMidi.owner != NULL;
+ info->inPresent = audioMidiInPort() != NULL;
+ info->inOwned = audioMidiInOwned();
+}
+
+
+// The port's input (audioMidiIn.c reads it), or NULL: none, or MIDI input
+// not chosen (DVX.INI [audio] midiIn).
+AudioMidiGetFnT audioMidiInPort(void) {
+ return sMidi.inWanted ? sMidi.get : NULL;
}
diff --git a/src/libs/kpunch/audio/audioWt.c b/src/libs/kpunch/audio/audioWt.c
new file mode 100644
index 00000000..ef812a9e
--- /dev/null
+++ b/src/libs/kpunch/audio/audioWt.c
@@ -0,0 +1,430 @@
+// 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.
+
+// audioWt.c -- the wavetable synthesizer: voices and sample memory
+// shared by every program
+//
+// See audio.h ("Wavetable synthesizer"). The voice functions (alloc,
+// free, start, update, release, stop, active) may run at interrupt time
+// (music playback's timer callback), so they keep to the interrupt rules:
+// no calls out of the file but the driver's, interrupts off while they
+// change the voice table, everything pinned (audioWtLock). The memory
+// functions run at program time only.
+//
+// A voice id is generation * WT_ID_STRIDE + voice + 1, so an id of a voice
+// given back never reaches the voice's next owner; free voices go out
+// least recently freed first. Sample memory is a list of the blocks in
+// use, in address order; a new block goes in the first gap it fits,
+// aligned, and a 16-bit one inside a 256 KB boundary where the chip asks.
+
+#include "audioInt.h"
+
+#include
+#include
+
+#define WT_ID_STRIDE 64
+#define WT_GEN_MAX (INT32_MAX / WT_ID_STRIDE - 1)
+#define WT_BLOCKS_MAX 1024
+#define WT_ALIGN 32 // bytes a block starts on
+#define WT_BANK_BYTES 0x40000U // AUDIO_WT_BANK_256K
+#define BITS_8 8
+#define BITS_16 16
+#define BYTE_BITS 8
+
+typedef struct {
+ AudioCtxT *owner; // NULL: free
+ int32_t id;
+ uint32_t freedAt; // allocation clock when given back (0: never used)
+} WtVoiceT;
+
+typedef struct {
+ AudioCtxT *owner;
+ uint32_t addr;
+ uint32_t bytes;
+} WtBlockT;
+
+typedef struct {
+ const AudioSynthDriverT *drv; // NULL: no wavetable synthesizer
+ WtVoiceT voices[AUDIO_WT_VOICES_MAX];
+ uint32_t count;
+ uint32_t memory;
+ uint32_t flags;
+ int32_t level;
+ uint32_t clock;
+ uint32_t generation;
+ char description[AUDIO_DESC_MAX];
+} WtChipT;
+
+static WtChipT sWt;
+static WtBlockT sBlocks[WT_BLOCKS_MAX]; // in use, by address
+static uint32_t sBlockCount;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool audioWtActive(const AudioCtxT *ctx, int32_t voice);
+int32_t audioWtAlloc(AudioCtxT *ctx);
+void audioWtAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info);
+void audioWtCtxClosed(AudioCtxT *ctx);
+void audioWtDetach(void);
+int32_t audioWtFree(const AudioCtxT *ctx, int32_t voice);
+void audioWtGetInfo(AudioWtInfoT *info);
+bool audioWtLock(bool lock);
+int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t *addr);
+int32_t audioWtMemFree(AudioCtxT *ctx, uint32_t addr);
+void audioWtNoteInit(AudioWtVoiceT *v);
+int32_t audioWtRelease(const AudioCtxT *ctx, int32_t voice);
+int32_t audioWtStart(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v);
+int32_t audioWtStop(const AudioCtxT *ctx, int32_t voice);
+int32_t audioWtUpdate(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v, uint32_t what);
+int32_t audioWtUpload(AudioCtxT *ctx, uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
+static int32_t blockOf(const AudioCtxT *ctx, uint32_t addr, uint32_t bytes);
+static void giveBack(uint32_t voice);
+static int32_t lookup(const AudioCtxT *ctx, int32_t voice);
+static void removeBlock(uint32_t i);
+
+
+// Interrupt-safe.
+bool audioWtActive(const AudioCtxT *ctx, int32_t voice) {
+ int32_t i = lookup(ctx, voice);
+
+ return i >= 0 && sWt.drv->active((uint32_t)i);
+}
+
+
+// Interrupt-safe.
+int32_t audioWtAlloc(AudioCtxT *ctx) {
+ if (!ctx) {
+ return AUDIO_ERR_ARG;
+ }
+
+ if (!sWt.drv) {
+ return AUDIO_ERR_NO_SYNTH;
+ }
+
+ uint32_t flags = audioIrqOff();
+ int32_t best = -1;
+ int32_t rc = AUDIO_ERR_NO_VOICE;
+
+ for (uint32_t i = 0; i < sWt.count; i++) {
+ const WtVoiceT *v = &sWt.voices[i];
+
+ if (!v->owner && (best < 0 || v->freedAt < sWt.voices[best].freedAt)) {
+ best = (int32_t)i;
+ }
+ }
+
+ if (best >= 0) {
+ if (++sWt.generation > WT_GEN_MAX) {
+ sWt.generation = 1;
+ }
+
+ sWt.voices[best].owner = ctx;
+ sWt.voices[best].id = (int32_t)(sWt.generation * WT_ID_STRIDE) + best + 1;
+ rc = sWt.voices[best].id;
+ }
+
+ audioIrqRestore(flags);
+ return rc;
+}
+
+
+void audioWtAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info) {
+ memset(&sWt, 0, sizeof(sWt));
+ sBlockCount = 0;
+ sWt.drv = drv;
+ sWt.count = info->voices < AUDIO_WT_VOICES_MAX ? info->voices : AUDIO_WT_VOICES_MAX;
+ sWt.memory = info->memory;
+ sWt.flags = info->flags;
+ sWt.level = info->level;
+ snprintf(sWt.description, sizeof(sWt.description), "%s", info->description);
+}
+
+
+// Everything ctx held, given back.
+void audioWtCtxClosed(AudioCtxT *ctx) {
+ uint32_t flags = audioIrqOff();
+
+ for (uint32_t i = 0; i < sWt.count; i++) {
+ if (sWt.voices[i].owner == ctx) {
+ giveBack(i);
+ }
+ }
+
+ audioIrqRestore(flags);
+
+ for (uint32_t i = sBlockCount; i > 0; i--) {
+ if (sBlocks[i - 1].owner == ctx) {
+ removeBlock(i - 1);
+ }
+ }
+}
+
+
+void audioWtDetach(void) {
+ memset(&sWt, 0, sizeof(sWt));
+ sBlockCount = 0;
+}
+
+
+// Interrupt-safe.
+int32_t audioWtFree(const AudioCtxT *ctx, int32_t voice) {
+ uint32_t flags = audioIrqOff();
+ int32_t i = lookup(ctx, voice);
+
+ if (i >= 0) {
+ giveBack((uint32_t)i);
+ }
+
+ audioIrqRestore(flags);
+ return i >= 0 ? AUDIO_OK : AUDIO_ERR_VOICE;
+}
+
+
+void audioWtGetInfo(AudioWtInfoT *info) {
+ uint32_t largest = 0;
+ uint32_t at = 0;
+
+ memset(info, 0, sizeof(*info));
+
+ if (!sWt.drv) {
+ return;
+ }
+
+ // The largest gap between the blocks in use
+ for (uint32_t i = 0; i <= sBlockCount; i++) {
+ uint32_t next = i < sBlockCount ? sBlocks[i].addr : sWt.memory;
+
+ largest = next - at > largest ? next - at : largest;
+ at = i < sBlockCount ? sBlocks[i].addr + sBlocks[i].bytes : at;
+ }
+
+ snprintf(info->description, sizeof(info->description), "%s", sWt.description);
+ info->voices = sWt.count;
+ info->memory = sWt.memory;
+ info->freeMemory = largest;
+ info->flags = sWt.flags;
+ info->level = sWt.level;
+ info->present = true;
+
+ for (uint32_t i = 0; i < sWt.count; i++) {
+ info->freeVoices += sWt.voices[i].owner ? 0 : 1;
+ }
+}
+
+
+bool audioWtLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)audioWtActive, 0 },
+ { (const void *)audioWtAlloc, 0 },
+ { (const void *)audioWtFree, 0 },
+ { (const void *)audioWtRelease, 0 },
+ { (const void *)audioWtStart, 0 },
+ { (const void *)audioWtStop, 0 },
+ { (const void *)audioWtUpdate, 0 },
+ { (const void *)giveBack, 0 },
+ { (const void *)lookup, 0 },
+ { &sWt, sizeof(sWt) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+// First fit: each gap between the blocks in use, from its aligned start
+// (moved to the next 256 KB boundary when a 16-bit sample would cross
+// one and the chip cares).
+int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t *addr) {
+ bool bankRule = (sWt.flags & AUDIO_WT_BANK_256K) && bits == BITS_16;
+
+ if (!ctx || !addr || bytes == 0 || (bits != BITS_8 && bits != BITS_16) || (bits == BITS_8 && !(sWt.flags & AUDIO_WT_8BIT))) {
+ return AUDIO_ERR_ARG;
+ }
+
+ if (!sWt.drv) {
+ return AUDIO_ERR_NO_SYNTH;
+ }
+
+ if (sBlockCount >= WT_BLOCKS_MAX || (bankRule && bytes > WT_BANK_BYTES)) {
+ return AUDIO_ERR_NO_ROOM;
+ }
+
+ uint32_t gapStart = 0;
+
+ for (uint32_t i = 0; i <= sBlockCount; i++) {
+ uint32_t gapEnd = i < sBlockCount ? sBlocks[i].addr : sWt.memory;
+ uint32_t start = (gapStart + WT_ALIGN - 1) / WT_ALIGN * WT_ALIGN;
+
+ if (bankRule && start / WT_BANK_BYTES != (start + bytes - 1) / WT_BANK_BYTES) {
+ start = (start / WT_BANK_BYTES + 1) * WT_BANK_BYTES;
+ }
+
+ if (start <= gapEnd && bytes <= gapEnd - start) {
+ memmove(&sBlocks[i + 1], &sBlocks[i], (sBlockCount - i) * sizeof(WtBlockT));
+ sBlocks[i].owner = ctx;
+ sBlocks[i].addr = start;
+ sBlocks[i].bytes = bytes;
+ sBlockCount++;
+ *addr = start;
+ return AUDIO_OK;
+ }
+
+ gapStart = i < sBlockCount ? sBlocks[i].addr + sBlocks[i].bytes : gapStart;
+ }
+
+ return AUDIO_ERR_NO_ROOM;
+}
+
+
+int32_t audioWtMemFree(AudioCtxT *ctx, uint32_t addr) {
+ for (uint32_t i = 0; i < sBlockCount; i++) {
+ if (sBlocks[i].owner == ctx && sBlocks[i].addr == addr) {
+ removeBlock(i);
+ return AUDIO_OK;
+ }
+ }
+
+ return AUDIO_ERR_ARG;
+}
+
+
+void audioWtNoteInit(AudioWtVoiceT *v) {
+ memset(v, 0, sizeof(*v));
+ v->filterFc = AUDIO_WT_FC_OPEN;
+ v->modLfoDelay = AUDIO_WT_TC_NONE;
+ v->vibLfoDelay = AUDIO_WT_TC_NONE;
+
+ for (int32_t i = 0; i < AUDIO_WT_ENV_STAGES; i++) {
+ v->volEnv[i] = i == AUDIO_WT_ENV_SUSTAIN ? 0 : AUDIO_WT_TC_NONE;
+ v->modEnv[i] = v->volEnv[i];
+ }
+}
+
+
+// Interrupt-safe.
+int32_t audioWtRelease(const AudioCtxT *ctx, int32_t voice) {
+ int32_t i = lookup(ctx, voice);
+
+ if (i < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ sWt.drv->release((uint32_t)i);
+ return AUDIO_OK;
+}
+
+
+// Interrupt-safe.
+int32_t audioWtStart(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v) {
+ int32_t i = lookup(ctx, voice);
+
+ if (i < 0 || !v) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ sWt.drv->start((uint32_t)i, v);
+ return AUDIO_OK;
+}
+
+
+// Interrupt-safe.
+int32_t audioWtStop(const AudioCtxT *ctx, int32_t voice) {
+ int32_t i = lookup(ctx, voice);
+
+ if (i < 0) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ sWt.drv->stop((uint32_t)i);
+ return AUDIO_OK;
+}
+
+
+// Interrupt-safe.
+int32_t audioWtUpdate(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ int32_t i = lookup(ctx, voice);
+
+ if (i < 0 || !v) {
+ return AUDIO_ERR_VOICE;
+ }
+
+ sWt.drv->update((uint32_t)i, v, what);
+ return AUDIO_OK;
+}
+
+
+int32_t audioWtUpload(AudioCtxT *ctx, uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits) {
+ if (!sWt.drv) {
+ return AUDIO_ERR_NO_SYNTH;
+ }
+
+ if (!frames || (bits != BITS_8 && bits != BITS_16) || blockOf(ctx, addr, count * (bits / BYTE_BITS)) < 0) {
+ return AUDIO_ERR_ARG;
+ }
+
+ return sWt.drv->upload(addr, frames, count, bits) ? AUDIO_OK : AUDIO_ERR_ARG;
+}
+
+
+// The block of ctx holding addr .. addr + bytes - 1 (its index), -1 when
+// none does.
+static int32_t blockOf(const AudioCtxT *ctx, uint32_t addr, uint32_t bytes) {
+ for (uint32_t i = 0; i < sBlockCount; i++) {
+ const WtBlockT *b = &sBlocks[i];
+
+ if (b->owner == ctx && addr >= b->addr && bytes <= b->bytes && addr - b->addr <= b->bytes - bytes) {
+ return (int32_t)i;
+ }
+ }
+
+ return -1;
+}
+
+
+// Stop a voice and mark it free (interrupts off).
+static void giveBack(uint32_t voice) {
+ sWt.drv->stop(voice);
+ sWt.voices[voice].owner = NULL;
+ sWt.voices[voice].freedAt = ++sWt.clock;
+}
+
+
+static int32_t lookup(const AudioCtxT *ctx, int32_t voice) {
+ if (!ctx || voice <= 0 || !sWt.drv) {
+ return -1;
+ }
+
+ int32_t i = (voice - 1) % WT_ID_STRIDE;
+
+ if ((uint32_t)i >= sWt.count || sWt.voices[i].owner != ctx || sWt.voices[i].id != voice) {
+ return -1;
+ }
+
+ return i;
+}
+
+
+static void removeBlock(uint32_t i) {
+ memmove(&sBlocks[i], &sBlocks[i + 1], (sBlockCount - i - 1) * sizeof(WtBlockT));
+ sBlockCount--;
+}
diff --git a/src/libs/kpunch/audio/codec/codecCalc.c b/src/libs/kpunch/audio/codec/codecCalc.c
new file mode 100644
index 00000000..fa43209a
--- /dev/null
+++ b/src/libs/kpunch/audio/codec/codecCalc.c
@@ -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.
+
+// codecCalc.c -- the AD1848 codec's arithmetic (see codecCalc.h)
+//
+// The codec divides one of two crystals, 24.576 MHz and 16.9344 MHz, by
+// one of eight dividers; the AD1848 data sheet lists the fourteen rates
+// that work (two of the 24.576 MHz ones are not supported).
+
+#include "codecCalc.h"
+
+#define CSS_XTAL2 1 // I8 bit 0: the 16.9344 MHz crystal
+#define CFS_SHIFT 1
+#define BITS_16 16
+
+typedef struct {
+ uint32_t rate;
+ uint8_t cfs; // divider select
+ uint8_t css; // crystal select
+} CodecRateT;
+
+// The rates (24.576 MHz / 3072, 1536, 896, 768, 512, 2560; 16.9344 MHz /
+// 3072, 1536, 896, 768, 448, 384, 512, 2560)
+static const CodecRateT sRates[] = {
+ { 5513, 0, CSS_XTAL2 },
+ { 6615, 7, CSS_XTAL2 },
+ { 8000, 0, 0 },
+ { 9600, 7, 0 },
+ { 11025, 1, CSS_XTAL2 },
+ { 16000, 1, 0 },
+ { 18900, 2, CSS_XTAL2 },
+ { 22050, 3, CSS_XTAL2 },
+ { 27429, 2, 0 },
+ { 32000, 3, 0 },
+ { 33075, 6, CSS_XTAL2 },
+ { 37800, 4, CSS_XTAL2 },
+ { 44100, 5, CSS_XTAL2 },
+ { 48000, 6, 0 }
+};
+
+#define RATE_COUNT ((int32_t)(sizeof(sRates) / sizeof(sRates[0])))
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+uint8_t codecFormat(uint32_t bits, uint32_t channels);
+uint8_t codecRateCode(uint32_t want, uint32_t *rate);
+
+
+uint8_t codecFormat(uint32_t bits, uint32_t channels) {
+ return (uint8_t)((bits == BITS_16 ? CODEC_FMT_16BIT : CODEC_FMT_8BIT) | (channels > 1 ? CODEC_FMT_STEREO : 0));
+}
+
+
+// Nearest; the faster of two as near.
+uint8_t codecRateCode(uint32_t want, uint32_t *rate) {
+ int32_t best = 0;
+
+ for (int32_t i = 1; i < RATE_COUNT; i++) {
+ uint32_t d = sRates[i].rate > want ? sRates[i].rate - want : want - sRates[i].rate;
+ uint32_t dBest = sRates[best].rate > want ? sRates[best].rate - want : want - sRates[best].rate;
+
+ best = d <= dBest ? i : best;
+ }
+
+ *rate = sRates[best].rate;
+ return (uint8_t)(sRates[best].cfs << CFS_SHIFT | sRates[best].css);
+}
diff --git a/src/libs/kpunch/audio/codec/codecCalc.h b/src/libs/kpunch/audio/codec/codecCalc.h
new file mode 100644
index 00000000..3deaa6ec
--- /dev/null
+++ b/src/libs/kpunch/audio/codec/codecCalc.h
@@ -0,0 +1,48 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// codecCalc.h -- the AD1848 codec's arithmetic (no hardware access)
+//
+// The codec (the AD1848 and its kin, the CS4231 and CS4248) on Windows
+// Sound System boards and the Gravis UltraSound MAX: its rates (two
+// crystals, eight dividers each) and its data format byte, kept apart
+// from codecHw.c so the host tests run them.
+#ifndef CODEC_CALC_H
+#define CODEC_CALC_H
+
+#include
+#include
+
+// I8 (clock and data format): the format bits
+#define CODEC_FMT_8BIT 0x00 // unsigned
+#define CODEC_FMT_16BIT 0x40 // signed, little-endian
+#define CODEC_FMT_STEREO 0x10
+
+// The I8 format bits for 8 or 16 bits, 1 or 2 channels.
+uint8_t codecFormat(uint32_t bits, uint32_t channels);
+
+// The rate nearest want the codec plays or records: its I8 clock bits
+// (CFS << 1 | CSS) and *rate, that rate in Hz (rounded where the crystal
+// gives a fraction).
+uint8_t codecRateCode(uint32_t want, uint32_t *rate);
+
+#endif // CODEC_CALC_H
diff --git a/src/libs/kpunch/audio/codec/codecHw.c b/src/libs/kpunch/audio/codec/codecHw.c
new file mode 100644
index 00000000..3ff81b1a
--- /dev/null
+++ b/src/libs/kpunch/audio/codec/codecHw.c
@@ -0,0 +1,225 @@
+// 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.
+
+// codecHw.c -- the AD1848 codec and its kin (see codecHw.h)
+
+#include "codecHw.h"
+#include "audioDrv.h"
+
+#include
+
+// Direct registers
+#define R_INDEX 0
+#define R_DATA 1
+#define R_STATUS 2
+#define INDEX_INIT 0x80 // busy initializing: reads this, ignores writes
+#define INDEX_MCE 0x40 // mode change enable
+#define STATUS_INT 0x01
+
+// Indirect registers and their bits
+#define I_INPUT_LEFT 0 // ADC input control (also the read and write test)
+#define I_INPUT_RIGHT 1
+#define I_DAC_LEFT 6
+#define I_DAC_RIGHT 7
+#define I_FORMAT 8
+#define I_INTERFACE 9
+#define I_PIN 10
+#define I_INIT 11
+#define I_MODE_ID 12
+#define I_COUNT_HIGH 14
+#define I_COUNT_LOW 15
+#define DAC_UNMUTED_FULL 0x00
+#define IF_SDC 0x04 // single DMA channel
+#define IF_ACAL 0x08 // calibrate at the end of a mode change
+#define PIN_IEN 0x02 // interrupts on
+#define INIT_ACI 0x20 // calibrating
+#define ID_CRYSTAL 0x80 // CS4248 / CS4231 family (an AD1848 reads 0)
+#define TEST_A 0xAA
+#define TEST_B 0x45
+#define BYTE_BITS 8
+#define BYTE_MASK 0xFF
+
+// Waits: port reads take about a microsecond on the ISA bus
+#define INIT_POLL_MAX 100000 // the codec busy after a reset or a mode change
+#define ACI_RISE_POLL 2000 // calibration starts within a few samples
+#define ACI_POLL_MAX 200000 // and takes a few hundred
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void codecDacOn(CodecT *c);
+bool codecFound(CodecT *c, uint16_t port);
+void codecInputSet(CodecT *c, uint8_t input);
+bool codecIrq(const CodecT *c);
+bool codecIsCrystal(const CodecT *c);
+bool codecLock(bool lock);
+static uint8_t codecRead(const CodecT *c, uint8_t reg);
+static bool codecReady(uint16_t port);
+void codecSetFormat(const CodecT *c, uint8_t format);
+void codecStart(const CodecT *c, uint32_t frames, uint8_t what);
+void codecStop(CodecT *c);
+static void codecWrite(const CodecT *c, uint8_t reg, uint8_t value);
+
+
+void codecDacOn(CodecT *c) {
+ c->savedDacLeft = codecRead(c, I_DAC_LEFT);
+ c->savedDacRight = codecRead(c, I_DAC_RIGHT);
+ c->dacSaved = true;
+ codecWrite(c, I_DAC_LEFT, DAC_UNMUTED_FULL);
+ codecWrite(c, I_DAC_RIGHT, DAC_UNMUTED_FULL);
+}
+
+
+bool codecFound(CodecT *c, uint16_t port) {
+ if (!codecReady(port) || inportb(port + R_INDEX) == BYTE_MASK) {
+ return false;
+ }
+
+ c->port = port;
+ c->dacSaved = false;
+ c->inputSaved = false;
+
+ uint8_t a = codecRead(c, I_INPUT_LEFT);
+ uint8_t b = codecRead(c, I_INPUT_RIGHT);
+
+ codecWrite(c, I_INPUT_LEFT, TEST_A);
+ codecWrite(c, I_INPUT_RIGHT, TEST_B);
+
+ bool ok = codecRead(c, I_INPUT_LEFT) == TEST_A && codecRead(c, I_INPUT_RIGHT) == TEST_B;
+
+ codecWrite(c, I_INPUT_LEFT, TEST_B);
+ codecWrite(c, I_INPUT_RIGHT, TEST_A);
+ ok = ok && codecRead(c, I_INPUT_LEFT) == TEST_B && codecRead(c, I_INPUT_RIGHT) == TEST_A;
+ codecWrite(c, I_INPUT_LEFT, a);
+ codecWrite(c, I_INPUT_RIGHT, b);
+ return ok;
+}
+
+
+void codecInputSet(CodecT *c, uint8_t input) {
+ c->savedInLeft = codecRead(c, I_INPUT_LEFT);
+ c->savedInRight = codecRead(c, I_INPUT_RIGHT);
+ c->inputSaved = true;
+ codecWrite(c, I_INPUT_LEFT, input);
+ codecWrite(c, I_INPUT_RIGHT, input);
+}
+
+
+bool codecIrq(const CodecT *c) {
+ if (!(inportb(c->port + R_STATUS) & STATUS_INT)) {
+ return false;
+ }
+
+ outportb(c->port + R_STATUS, 0);
+ return true;
+}
+
+
+bool codecIsCrystal(const CodecT *c) {
+ return (codecRead(c, I_MODE_ID) & ID_CRYSTAL) != 0;
+}
+
+
+bool codecLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)codecIrq, 0 }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+static uint8_t codecRead(const CodecT *c, uint8_t reg) {
+ outportb(c->port + R_INDEX, reg);
+ return inportb(c->port + R_DATA);
+}
+
+
+// The codec is past initializing (bounded wait).
+static bool codecReady(uint16_t port) {
+ for (int32_t i = 0; i < INIT_POLL_MAX; i++) {
+ if (inportb(port + R_INDEX) != INDEX_INIT) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// ACI rises within a few samples of the mode change ending and falls when
+// the calibration is done.
+void codecSetFormat(const CodecT *c, uint8_t format) {
+ uint16_t port = c->port;
+
+ outportb(port + R_INDEX, INDEX_MCE | I_FORMAT);
+ outportb(port + R_DATA, format);
+ codecReady(port);
+ outportb(port + R_INDEX, INDEX_MCE | I_INTERFACE);
+ outportb(port + R_DATA, IF_ACAL | IF_SDC);
+ outportb(port + R_INDEX, I_INIT);
+ codecReady(port);
+
+ for (int32_t i = 0; i < ACI_RISE_POLL && !(inportb(port + R_DATA) & INIT_ACI); i++) {
+ }
+
+ for (int32_t i = 0; i < ACI_POLL_MAX && (inportb(port + R_DATA) & INIT_ACI); i++) {
+ }
+}
+
+
+// The counter holds the frames less one.
+void codecStart(const CodecT *c, uint32_t frames, uint8_t what) {
+ uint32_t count = frames - 1;
+
+ codecWrite(c, I_COUNT_LOW, (uint8_t)(count & BYTE_MASK));
+ codecWrite(c, I_COUNT_HIGH, (uint8_t)(count >> BYTE_BITS));
+ codecWrite(c, I_PIN, PIN_IEN);
+ outportb(c->port + R_STATUS, 0);
+ codecWrite(c, I_INTERFACE, (uint8_t)(codecRead(c, I_INTERFACE) | what));
+}
+
+
+void codecStop(CodecT *c) {
+ codecWrite(c, I_INTERFACE, (uint8_t)(codecRead(c, I_INTERFACE) & ~(CODEC_PLAY | CODEC_CAPTURE)));
+ codecWrite(c, I_PIN, 0);
+ outportb(c->port + R_STATUS, 0);
+
+ if (c->dacSaved) {
+ codecWrite(c, I_DAC_LEFT, c->savedDacLeft);
+ codecWrite(c, I_DAC_RIGHT, c->savedDacRight);
+ c->dacSaved = false;
+ }
+
+ if (c->inputSaved) {
+ codecWrite(c, I_INPUT_LEFT, c->savedInLeft);
+ codecWrite(c, I_INPUT_RIGHT, c->savedInRight);
+ c->inputSaved = false;
+ }
+}
+
+
+static void codecWrite(const CodecT *c, uint8_t reg, uint8_t value) {
+ outportb(c->port + R_INDEX, reg);
+ outportb(c->port + R_DATA, value);
+}
diff --git a/src/libs/kpunch/audio/codec/codecHw.h b/src/libs/kpunch/audio/codec/codecHw.h
new file mode 100644
index 00000000..ded916f4
--- /dev/null
+++ b/src/libs/kpunch/audio/codec/codecHw.h
@@ -0,0 +1,100 @@
+// 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.
+
+// codecHw.h -- the AD1848 codec and its kin, shared by the drivers of the
+// boards built on one
+//
+// wss.drv (Windows Sound System boards) and gus.drv (the UltraSound
+// MAX's CS4231) each link their own copy. The codec has four direct
+// registers at its port (index, data, status, PIO data) and its
+// indirect ones behind the index. These routines set the data format in
+// a mode change with the codec's own calibration, run playing or capture
+// over one DMA channel (single-DMA mode, the codec's sample counter
+// interrupting at the end of each block), choose the ADC's input, and
+// put back the DAC levels and inputs they found. The driver owns its DMA
+// buffer and interrupt, and waits for the first interrupt itself. Facts
+// from the AD1848 and CS4231 data sheets.
+#ifndef CODEC_HW_H
+#define CODEC_HW_H
+
+#include "codecCalc.h"
+
+#include
+#include
+
+// The ADC's source (I0 / I1 bits 7-6), and the microphone's 20 dB boost
+#define CODEC_INPUT_LINE 0x00
+#define CODEC_INPUT_AUX1 0x40
+#define CODEC_INPUT_MIC 0x80
+#define CODEC_INPUT_MIC_BOOST 0x20
+
+// What codecStart starts (I9)
+#define CODEC_PLAY 0x01
+#define CODEC_CAPTURE 0x02
+
+typedef struct {
+ uint16_t port; // the index register; data, status follow
+ uint8_t savedDacLeft; // I6, I7 as found (codecDacOn)
+ uint8_t savedDacRight;
+ uint8_t savedInLeft; // I0, I1 as found (codecInputSet)
+ uint8_t savedInRight;
+ bool dacSaved;
+ bool inputSaved;
+} CodecT;
+
+// A codec answers at port (not busy, not an empty bus) and its first two
+// indirect registers keep two patterns each way round (put back after);
+// c is set up for it then.
+bool codecFound(CodecT *c, uint16_t port);
+
+// A Crystal part (CS4231, CS4248): an AD1848 reads 0 there.
+bool codecIsCrystal(const CodecT *c);
+
+// The format byte (codecFormat | codecRateCode) in a mode change,
+// single-DMA mode, the codec calibrating as it ends (bounded waits).
+void codecSetFormat(const CodecT *c, uint8_t format);
+
+// The DAC unmuted at full, its levels kept for codecStop.
+void codecDacOn(CodecT *c);
+
+// Both ADC inputs from input (CODEC_INPUT_*), the gain at 0 dB; the
+// inputs kept for codecStop.
+void codecInputSet(CodecT *c, uint8_t input);
+
+// The sample counter set to frames (an interrupt at the end of every
+// block of them), the interrupt on and cleared, and what (CODEC_PLAY or
+// CODEC_CAPTURE) started.
+void codecStart(const CodecT *c, uint32_t frames, uint8_t what);
+
+// Playing and capture stopped, the interrupt off and cleared, and the DAC
+// levels and inputs put back where codecDacOn and codecInputSet changed
+// them.
+void codecStop(CodecT *c);
+
+// The codec's interrupt: true when it raised it, acknowledged then.
+// Interrupt time.
+bool codecIrq(const CodecT *c);
+
+// Pin or release this file's interrupt-time code.
+bool codecLock(bool lock);
+
+#endif // CODEC_HW_H
diff --git a/src/libs/kpunch/audio/drivers/awe/aweCalc.c b/src/libs/kpunch/audio/drivers/awe/aweCalc.c
new file mode 100644
index 00000000..75c26ed3
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/awe/aweCalc.c
@@ -0,0 +1,191 @@
+// 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.
+
+// aweCalc.c -- the EMU8000's units (see aweCalc.h)
+//
+// Times go through milliseconds (1000 x 2^(tc / 1200), drvPow2) where the
+// guide's register is linear in them; the attack and decay codes are
+// logarithmic, so below the guide's knee they come straight from the
+// timecents: an attack code is 8.42 - tc / 75 under 371 ms (32 + 16
+// log2(360 / ms)) and 11878 / ms above; a decay code 39 + (37 - tc) / 75
+// under 2828 ms (Creative's library, the guide's 100 ms = 0x5C) and
+// 45228 / ms above.
+
+#include "aweCalc.h"
+#include "../../math/drvMath.h"
+
+#define IP_PER_OCTAVE_NUM 1024 // 4096 IP steps per 1200 cents
+#define IP_PER_OCTAVE_DEN 300
+#define IP_MAX 0xFFFF
+#define ATTEN_NUM 4 // 0.375 dB = 3.75 cB a step
+#define ATTEN_DEN 15
+#define BYTE_MAX 0xFF
+#define CUTOFF_BASE 4721 // 125 Hz in absolute cents
+#define CUTOFF_NUM 8 // 28.125 cents a step
+#define CUTOFF_DEN 225
+#define Q_CB_STEP 12
+#define Q_MAX 15
+#define PAN_HALF 500
+#define PAN_SPAN 1000
+#define SEND_SPAN 1000
+#define TC_NONE (-12000) // 1 ms and less: no delay
+#define MS_PER_SEC 1000
+#define DELAY_NUM 40 // 725 us steps: ms x 1000 / 725
+#define DELAY_DEN 29
+#define HOLD_MAX 0x7F
+#define HOLD_MS_STEP 92
+#define CODE_MIN 1
+#define CODE_MAX 127
+#define ATTACK_KNEE_TC (-1717) // 371 ms
+#define ATTACK_FAST_NUM 25251 // 8.417 x 3000
+#define ATTACK_FAST_TC 40 // tc / 75 = tc x 40 / 3000
+#define ATTACK_FAST_DEN 3000
+#define ATTACK_SLOW_MS 11878 // code x ms
+#define DECAY_KNEE_TC 1800 // 2828 ms
+#define DECAY_FAST_BASE 2962 // 39 x 75 + 37
+#define DECAY_FAST_DEN 75
+#define DECAY_SLOW_MS 45228
+#define SUSTAIN_TOP 0x7F
+#define SUSV_NUM 2 // 0.75 dB = 7.5 cB a step
+#define SUSV_DEN 15
+#define MOD_SUSTAIN_SPAN 1000
+#define LFO_REG_X100 19467 // 8.176 Hz / 0.042 Hz x 100
+#define LFO_PERCENT 100
+#define LFO_CENTS_MAX 600 // past 10.72 Hz the register is full anyway
+#define DEPTH_MIN (-128)
+#define DEPTH_MAX 127
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+uint8_t aweAttack(int32_t timecents);
+uint8_t aweAtten(int32_t centibels);
+uint8_t aweCutoff(int32_t absCents);
+uint8_t aweDecay(int32_t timecents);
+uint16_t aweDelay(int32_t timecents);
+uint8_t aweDepth(int32_t value, int32_t stepNum, int32_t stepDen);
+uint8_t aweHold(int32_t timecents);
+uint8_t aweLfoFreq(int32_t absCents);
+uint8_t aweModSustain(int32_t tenths);
+uint8_t awePan(int32_t pan);
+uint16_t awePitch(int32_t cents);
+uint8_t aweQ(int32_t centibels);
+uint8_t aweSend(int32_t tenths);
+uint8_t aweVolSustain(int32_t centibels);
+static int32_t clampInt(int32_t v, int32_t lo, int32_t hi);
+static uint32_t msOf(int32_t timecents);
+
+
+uint8_t aweAttack(int32_t timecents) {
+ if (timecents < ATTACK_KNEE_TC) {
+ return (uint8_t)clampInt((ATTACK_FAST_NUM - timecents * ATTACK_FAST_TC) / ATTACK_FAST_DEN, CODE_MIN, CODE_MAX);
+ }
+
+ return (uint8_t)clampInt((int32_t)(((uint32_t)ATTACK_SLOW_MS * drvPow2(-timecents) / DRV_POW2_ONE) / MS_PER_SEC), CODE_MIN, CODE_MAX);
+}
+
+
+uint8_t aweAtten(int32_t centibels) {
+ return (uint8_t)clampInt(centibels * ATTEN_NUM / ATTEN_DEN, 0, BYTE_MAX);
+}
+
+
+uint8_t aweCutoff(int32_t absCents) {
+ return (uint8_t)clampInt((absCents - CUTOFF_BASE) * CUTOFF_NUM / CUTOFF_DEN, 0, BYTE_MAX);
+}
+
+
+uint8_t aweDecay(int32_t timecents) {
+ if (timecents < DECAY_KNEE_TC) {
+ return (uint8_t)clampInt((DECAY_FAST_BASE - timecents) / DECAY_FAST_DEN, CODE_MIN, CODE_MAX);
+ }
+
+ return (uint8_t)clampInt((int32_t)(((uint32_t)DECAY_SLOW_MS * drvPow2(-timecents) / DRV_POW2_ONE) / MS_PER_SEC), CODE_MIN, CODE_MAX);
+}
+
+
+uint16_t aweDelay(int32_t timecents) {
+ if (timecents <= TC_NONE) {
+ return AWE_DELAY_NONE;
+ }
+
+ return (uint16_t)clampInt(AWE_DELAY_NONE - (int32_t)(msOf(timecents) * DELAY_NUM / DELAY_DEN), 0, AWE_DELAY_NONE);
+}
+
+
+uint8_t aweDepth(int32_t value, int32_t stepNum, int32_t stepDen) {
+ return (uint8_t)(int8_t)clampInt(value * stepDen / stepNum, DEPTH_MIN, DEPTH_MAX);
+}
+
+
+uint8_t aweHold(int32_t timecents) {
+ return (uint8_t)clampInt(HOLD_MAX - (int32_t)(msOf(timecents) / HOLD_MS_STEP), 0, HOLD_MAX);
+}
+
+
+uint8_t aweLfoFreq(int32_t absCents) {
+ uint32_t pow = drvPow2(absCents < LFO_CENTS_MAX ? absCents : LFO_CENTS_MAX);
+
+ return (uint8_t)clampInt((int32_t)((uint32_t)LFO_REG_X100 * (pow >> 1) / (DRV_POW2_ONE >> 1) / LFO_PERCENT), 0, BYTE_MAX);
+}
+
+
+uint8_t aweModSustain(int32_t tenths) {
+ return (uint8_t)clampInt(SUSTAIN_TOP - tenths * SUSTAIN_TOP / MOD_SUSTAIN_SPAN, 0, SUSTAIN_TOP);
+}
+
+
+uint8_t awePan(int32_t pan) {
+ return (uint8_t)((PAN_HALF - clampInt(pan, -PAN_HALF, PAN_HALF)) * BYTE_MAX / PAN_SPAN);
+}
+
+
+uint16_t awePitch(int32_t cents) {
+ return (uint16_t)clampInt(AWE_IP_UNITY + cents * IP_PER_OCTAVE_NUM / IP_PER_OCTAVE_DEN, 0, IP_MAX);
+}
+
+
+uint8_t aweQ(int32_t centibels) {
+ return (uint8_t)clampInt(centibels / Q_CB_STEP, 0, Q_MAX);
+}
+
+
+uint8_t aweSend(int32_t tenths) {
+ return (uint8_t)clampInt(tenths * BYTE_MAX / SEND_SPAN, 0, BYTE_MAX);
+}
+
+
+uint8_t aweVolSustain(int32_t centibels) {
+ return (uint8_t)clampInt(SUSTAIN_TOP - centibels * SUSV_NUM / SUSV_DEN, 0, SUSTAIN_TOP);
+}
+
+
+static int32_t clampInt(int32_t v, int32_t lo, int32_t hi) {
+ return v < lo ? lo : (v > hi ? hi : v);
+}
+
+
+// 1000 x 2^(tc / 1200) ms (tc at most +8000: about 100 s).
+static uint32_t msOf(int32_t timecents) {
+ return (uint32_t)(((uint64_t)MS_PER_SEC * drvPow2(timecents)) >> DRV_POW2_SHIFT);
+}
diff --git a/src/libs/kpunch/audio/drivers/awe/aweCalc.h b/src/libs/kpunch/audio/drivers/awe/aweCalc.h
new file mode 100644
index 00000000..dd4145d7
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/awe/aweCalc.h
@@ -0,0 +1,77 @@
+// 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.
+
+// aweCalc.h -- the EMU8000's units (no hardware access)
+//
+// What awe.drv writes to the chip's registers for a note's SoundFont
+// values: kept apart so the host tests check them. Integer only (they
+// run at interrupt time, from music playback): 2^x from drvPow2, no
+// division by 64-bit values. Units from the EMU8000 Programmer's Guide
+// and the reference summary (section 5.4 and 7).
+#ifndef AWE_CALC_H
+#define AWE_CALC_H
+
+#include
+
+#define AWE_IP_UNITY 0xE000 // IP: 44,100 frames a second at their own pitch
+#define AWE_DELAY_NONE 0x8000 // ENVVOL, ENVVAL, LFO1VAL, LFO2VAL: no delay
+
+// IP for a pitch in cents (0: 44,100 frames a second): 4096 an octave.
+uint16_t awePitch(int32_t cents);
+
+// ATN for an attenuation in centibels: 0.375 dB a step, 0xFF at most.
+uint8_t aweAtten(int32_t centibels);
+
+// IFC for a cutoff in absolute cents (125 Hz at 0, 28.125 cents a step);
+// filter Q for a resonance in centibels (0..15).
+uint8_t aweCutoff(int32_t absCents);
+uint8_t aweQ(int32_t centibels);
+
+// PAN for -500 (left) .. 500 (right): 0xFF left, 0x00 right.
+uint8_t awePan(int32_t pan);
+
+// A reverb or chorus send in tenths of a percent as 0..0xFF.
+uint8_t aweSend(int32_t tenths);
+
+// Envelope and LFO times in timecents: an attack code (1..127), a decay
+// or release code (1..127), a delay register (0x8000 less 725 us steps),
+// a hold value (0x7F less 92 ms steps).
+uint8_t aweAttack(int32_t timecents);
+uint8_t aweDecay(int32_t timecents);
+uint16_t aweDelay(int32_t timecents);
+uint8_t aweHold(int32_t timecents);
+
+// Sustain levels: the modulation's in tenths of a percent below the
+// peak, the volume's in centibels below it (0.75 dB steps from 0x7F).
+uint8_t aweModSustain(int32_t tenths);
+uint8_t aweVolSustain(int32_t centibels);
+
+// An LFO's frequency register for absolute cents (0.042 Hz steps).
+uint8_t aweLfoFreq(int32_t absCents);
+
+// A signed depth byte (-128..127 as two's complement) for value in steps
+// of stepNum / stepDen: the guide's depths count 9.375 cents of pitch
+// (75 / 8), 56.25 cents of cutoff for the envelope (225 / 4) and 28.125
+// for the LFO (225 / 8), 0.9375 cB of volume (15 / 16).
+uint8_t aweDepth(int32_t value, int32_t stepNum, int32_t stepDen);
+
+#endif // AWE_CALC_H
diff --git a/src/libs/kpunch/audio/drivers/awe/aweDrv.c b/src/libs/kpunch/audio/drivers/awe/aweDrv.c
new file mode 100644
index 00000000..40ab79c1
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/awe/aweDrv.c
@@ -0,0 +1,842 @@
+// 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.
+
+// aweDrv.c -- the Sound Blaster AWE32 / AWE64 wavetable synthesizer
+// (awe.drv): the EMU8000 beside the card's Sound Blaster
+//
+// A synth driver (audioDrv.h "Synthesizer drivers", kind
+// AUDIO_SYNTH_WAVETABLE). Where the chip is: BLASTER's E (its base;
+// without it the Sound Blaster's A + 400h): three windows, E (data 0), E
+// + 400h (data 1 and 2) and E + 800h (data 3 and the pointer, which picks
+// a register and a channel for the data ports). Found by its signature
+// (data 3 register 7 reads ...Ch, the configuration words their fixed
+// bits), then set up in the order the EMU8000 Programmer's Guide gives:
+// every channel's envelope engine off and its registers cleared, the
+// four sets of initialization arrays (the effects program: Hall 2 reverb,
+// Chorus 3, flat equalization), audio on. Channels 30 and 31 carry the
+// FM chip's sound through the effects and refresh the sample memory, so
+// 30 voices remain; the memory is sized in 512 KB steps.
+//
+// The chip does everything a SoundFont asks: both envelopes, both LFOs,
+// the resonant filter, reverb and chorus sends (aweCalc.c turns the
+// SoundFont's units into its registers). Samples are 16-bit, uploaded
+// through channels set to memory-write mode (all 30 while nothing plays),
+// the FM channels set up again afterwards. The EMU8000 always loops: a
+// sample that does not loops in the silent frames after its end (the
+// loader leaves eight); one that loops until the key is up moves its
+// loop there at release. Every pointer and data access runs with
+// interrupts off: the timer starts notes while a program uploads.
+
+#include "audioDrv.h"
+#include "aweCalc.h"
+#include "../sb/sbCalc.h"
+#include "../../math/drvMath.h"
+
+#include
+#include
+#include
+
+// Ports, from the EMU base
+#define PORT_DATA0 0x000 // double words: the low word here, the high at + 2
+#define PORT_DATA1 0x400
+#define PORT_DATA2 0x402
+#define PORT_DATA3 0x800
+#define PORT_POINTER 0x802
+#define EMU_FROM_SB 0x400 // the base without BLASTER's E
+#define REG_SHIFT 5 // pointer: register << 5 | channel
+#define HIGH_WORD 2
+#define WORD_BITS 16
+#define WORD_MASK 0xFFFF
+
+// Registers: data port and number (pointer = number << 5 | channel)
+#define R_CPF PORT_DATA0, 0
+#define R_PTRX PORT_DATA0, 1
+#define R_CVCF PORT_DATA0, 2
+#define R_VTFT PORT_DATA0, 3
+#define R_UNKNOWN4 PORT_DATA0, 4
+#define R_UNKNOWN5 PORT_DATA0, 5
+#define R_PSST PORT_DATA0, 6
+#define R_CSL PORT_DATA0, 7
+#define R_CCCA PORT_DATA1, 0
+#define R_HW PORT_DATA1, 1 // channel: which configuration or memory register
+#define R_HW2 PORT_DATA2, 1
+#define R_INIT1 PORT_DATA1, 2
+#define R_INIT2 PORT_DATA2, 2
+#define R_INIT3 PORT_DATA1, 3
+#define R_INIT4 PORT_DATA2, 3
+#define R_ENVVOL PORT_DATA1, 4
+#define R_DCYSUSV PORT_DATA1, 5
+#define R_ENVVAL PORT_DATA1, 6
+#define R_DCYSUS PORT_DATA1, 7
+#define R_ATKHLDV PORT_DATA2, 4
+#define R_LFO1VAL PORT_DATA2, 5
+#define R_ATKHLD PORT_DATA2, 6
+#define R_LFO2VAL PORT_DATA2, 7
+#define R_IP PORT_DATA3, 0
+#define R_IFATN PORT_DATA3, 1
+#define R_PEFE PORT_DATA3, 2
+#define R_FMMOD PORT_DATA3, 3
+#define R_TREMFRQ PORT_DATA3, 4
+#define R_FM2FRQ2 PORT_DATA3, 5
+#define R_ID PORT_DATA3, 7
+
+// R_HW / R_HW2 channels
+#define HW_CF4 9
+#define HW_CF5 10
+#define HW_CF6 13
+#define HW_SMALR 20
+#define HW_SMALW 22
+#define HW_SMLD 26
+#define HW_WC 27 // on data 2
+#define HW_FM_TIMING 28
+#define HW_CF1 29
+#define HW_CF2 30
+#define HW_CF3 31
+
+// Values the guide gives
+#define CF1_INIT 0x0059
+#define CF2_INIT 0x0020
+#define CF3_AUDIO_ON 0x0004
+#define CF4_INIT 0x00000000U
+#define CF5_INIT 0x00000083U
+#define CF6_INIT 0x00008000U
+#define ID_MASK 0x000F
+#define ID_EMU 0x000C
+#define CF1_MASK 0x007E
+#define CF1_EXPECT 0x0058
+#define CF2_MASK 0x0003
+#define CF2_EXPECT 0x0003
+#define ENV_OFF 0x0080 // DCYSUSV: the envelope engine off
+#define ENV_KILL 0x807F // DCYSUSV: release at the fastest
+#define RELEASE_BIT 0x8000
+#define VTFT_SILENT 0x0000FFFFU
+#define CVCF_SILENT 0x0000FFFFU
+#define PITCH_UNITY 0x40000000U // PTRX / CPF: 0x4000 in the top word
+#define DMA_LEFT_WRITE 0x06000000U
+#define DMA_LEFT_READ 0x04000000U
+#define MEM_BUSY 0x80000000U // SMALW: FULL; SMALR: EMPTY
+#define DRAM_FIRST 0x200000U // word address of the sample memory
+#define DRAM_STEP_BYTES (512U * 1024U)
+#define DRAM_MAX_BYTES (28U * 1024U * 1024U)
+#define DRAM_PATTERN_A 0xA5B9
+#define DRAM_PATTERN_B 0x9D53
+#define INIT_ARRAYS 4
+#define INIT_WORDS 32
+#define INIT_SETS 4
+#define INIT_WAIT_SAMPLES 1024
+#define INIT_ODD_BIT 0x8000
+#define INIT1_EVEN_BASE 0x03FF
+#define INIT1_EVEN_STEP 0x0400
+#define INIT1_ODD_BASE 0x0030
+#define INIT1_ODD_SHIFT 8
+#define INIT1_ODD_MASK 15
+
+// FM pass-through on channels 30 and 31 (the reserved memory where the
+// FM chip's output appears)
+#define FM_LEFT 30
+#define FM_RIGHT 31
+#define FM_LEFT_PSST 0xFFFFFFE0U // pan full left, loop start
+#define FM_LEFT_CSL 0x00FFFFE8U
+#define FM_LEFT_CCCA 0x00FFFFE3U
+#define FM_RIGHT_PSST 0x00FFFFF0U
+#define FM_RIGHT_CSL 0x00FFFFF8U
+#define FM_RIGHT_CCCA 0x00FFFFF3U
+#define FM_RIGHT_CPF 0x00008000U
+#define FM_VTFT 0x8000FFFFU
+#define FM_TIMING 0x4828
+#define POINTER_PHASE 0x1000 // pointer bit 12: toggles with the chip's cycle
+#define PHASE_WAIT_US 2000 // each half of its cycle waited for, at most
+
+// Voices and notes
+#define AWE_VOICES 30
+#define AWE_VOICE_LEVEL 76 // centibels a voice at full volume is louder than the Sound Blaster's full-scale digital sound (86Box)
+#define AWE_CHANNELS 32
+#define ONE_SHOT_LOOP_START 4 // a one-shot's loop, frames after its end
+#define ONE_SHOT_LOOP_END 8
+#define FILTER_SHIFT 8
+#define VOL_TARGET_FULL 0xFFFF0000U
+#define CF_FULL 0x0000FF00U
+#define PAN_SHIFT 24
+#define SEND_SHIFT 24
+#define REVERB_SHIFT 8
+#define Q_SHIFT 28
+#define PITCH_TARGET_MASK 0xFFFF0000U
+#define DEPTH_SHIFT 8
+#define SUSTAIN_SHIFT 8
+#define HOLD_SHIFT 8
+#define STEP_PITCH_NUM 75 // 9.375 cents a depth step
+#define STEP_PITCH_DEN 8
+#define STEP_ENV_CUT_NUM 225 // 56.25 cents
+#define STEP_ENV_CUT_DEN 4
+#define STEP_LFO_CUT_NUM 225 // 28.125 cents
+#define STEP_LFO_CUT_DEN 8
+#define STEP_TREM_NUM 15 // 0.9375 cB
+#define STEP_TREM_DEN 16
+#define BYTES_PER_WORD 2
+#define POLL_MAX 100000
+#define UPLOAD_RUN 256 // words with interrupts off at a time
+#define FLAGS_IF 0x200
+#define KB 1024
+
+// The fourth set of initialization arrays (the guide's section 9, INIT1
+// to INIT4, 32 words each): the effects program. The other three sets
+// follow from it and a pattern (aweInitWord).
+static const uint16_t sInit4[INIT_ARRAYS * INIT_WORDS] = {
+ 0x0C10, 0x8470, 0x14FE, 0xB488, 0x167F, 0xA470, 0x18E7, 0x84B5,
+ 0x1B6E, 0x842A, 0x1F1D, 0x852A, 0x0DA3, 0x0F7C, 0x167E, 0x7254,
+ 0x0000, 0x842A, 0x0001, 0x852A, 0x18E6, 0x0BAA, 0x1B6D, 0x7234,
+ 0x229F, 0x8429, 0x2746, 0x8529, 0x1F1C, 0x06E7, 0x229E, 0x7224,
+ 0x0DA4, 0x8429, 0x2C29, 0x8529, 0x2745, 0x07F6, 0x2C28, 0x7254,
+ 0x383B, 0x8428, 0x320F, 0x8528, 0x320E, 0x0F02, 0x1341, 0x7264,
+ 0x3EB6, 0x8428, 0x3EB9, 0x8528, 0x383A, 0x0FA9, 0x3EB5, 0x7294,
+ 0x3EB7, 0x8474, 0x3EBA, 0x8575, 0x3EB8, 0x44C3, 0x3EBB, 0x45C3,
+ 0x0000, 0xA404, 0x0001, 0xA504, 0x141F, 0x0671, 0x14FD, 0x0287,
+ 0x3EBC, 0xE610, 0x3EC8, 0x0C7B, 0x031A, 0x07E6, 0x3EC8, 0x86F7,
+ 0x3EC0, 0x821E, 0x3EBE, 0xD208, 0x3EBD, 0x021F, 0x3ECA, 0x0386,
+ 0x3EC1, 0x0C03, 0x3EC9, 0x031E, 0x3ECA, 0x8C4C, 0x3EBF, 0x0C55,
+ 0x3EC9, 0xC208, 0x3EC4, 0xBC84, 0x3EC8, 0x0EAD, 0x3EC8, 0xD308,
+ 0x3EC2, 0x8F7E, 0x3ECB, 0x021E, 0x3ECB, 0xD208, 0x3EC5, 0x031F,
+ 0x3EC6, 0xC308, 0x3EC3, 0x32FF, 0x3EC9, 0x0265, 0x3EC9, 0x831E,
+ 0x1342, 0xD308, 0x3EC7, 0x33FF, 0x0000, 0x8365, 0x1420, 0x9570
+};
+
+// What a note set, for its updates and release
+typedef struct {
+ uint32_t loopStart; // word addresses
+ uint32_t loopEnd;
+ uint32_t end;
+ uint8_t cutoff; // IFATN's high byte
+ uint8_t pan;
+ uint8_t chorus;
+ uint8_t volRelease; // decay codes
+ uint8_t modRelease;
+ uint8_t loop; // AUDIO_WT_LOOP_*
+ bool released;
+} AweVoiceT;
+
+typedef struct {
+ AweVoiceT voices[AWE_VOICES];
+ uint32_t dramBytes;
+ uint16_t base; // the EMU base (data 0)
+ bool locked;
+} AweStateT;
+
+static AweStateT sAwe;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool aweActive(uint32_t voice);
+static void aweClose(void);
+static bool aweDetect(void);
+static void aweDmaClose(void);
+static void aweDmaOpen(uint32_t mode, uint32_t channels);
+static void aweFmInit(void);
+static void aweInit(void);
+static uint16_t aweInitWord(int32_t set, int32_t word);
+static bool aweLock(bool lock);
+static bool aweOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
+static uint16_t aweRead(uint16_t port, uint32_t reg, uint32_t channel);
+static uint32_t aweRead32(uint16_t port, uint32_t reg, uint32_t channel);
+static void aweRelease(uint32_t voice);
+static uint32_t aweSizeDram(void);
+static void aweStart(uint32_t voice, const AudioWtVoiceT *v);
+static void aweStop(uint32_t voice);
+static void aweUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what);
+static bool aweUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
+static void aweWaitMem(uint32_t reg);
+static void aweWaitSamples(uint32_t samples);
+static void aweWrite(uint16_t port, uint32_t reg, uint32_t channel, uint16_t value);
+static void aweWrite32(uint16_t port, uint32_t reg, uint32_t channel, uint32_t value);
+static uint32_t cliSave(void);
+static void flagsRestore(uint32_t flags);
+
+AUDIO_SYNTH_DRIVER("awe", AUDIO_PRIORITY_CARD,
+ .kind = AUDIO_SYNTH_WAVETABLE,
+ .open = aweOpen,
+ .close = aweClose,
+ .upload = aweUpload,
+ .start = aweStart,
+ .update = aweUpdate,
+ .release = aweRelease,
+ .stop = aweStop,
+ .active = aweActive
+);
+
+
+// Sounding until released and faded out (its current volume 0).
+// Interrupt time.
+static bool aweActive(uint32_t voice) {
+ return !sAwe.voices[voice].released || (aweRead32(R_CVCF, voice) >> WORD_BITS) != 0;
+}
+
+
+// Every voice stopped, the FM channels as the chip had them.
+static void aweClose(void) {
+ for (uint32_t v = 0; v < AWE_VOICES; v++) {
+ aweStop(v);
+ }
+
+ aweFmInit();
+
+ if (sAwe.locked) {
+ aweLock(false);
+ sAwe.locked = false;
+ }
+}
+
+
+// The signature: data 3 register 7's second read ends in Ch, and the
+// first two configuration words keep their fixed bits.
+static bool aweDetect(void) {
+ aweRead(R_ID, 0);
+
+ if ((aweRead(R_ID, 0) & ID_MASK) != ID_EMU) {
+ return false;
+ }
+
+ aweWrite(R_HW, HW_CF1, CF1_INIT);
+ aweWrite(R_HW, HW_CF2, CF2_INIT);
+ aweWrite(R_HW, HW_CF3, 0);
+ return (aweRead(R_HW, HW_CF1) & CF1_MASK) == CF1_EXPECT && (aweRead(R_HW, HW_CF2) & CF2_MASK) == CF2_EXPECT;
+}
+
+
+// The memory channels back to voices: no transfer, released at once.
+static void aweDmaClose(void) {
+ aweWaitMem(HW_SMALW);
+
+ for (uint32_t ch = 0; ch < AWE_VOICES; ch++) {
+ aweWrite32(R_CCCA, ch, 0);
+ aweWrite(R_DCYSUSV, ch, ENV_KILL);
+ }
+}
+
+
+// Channels 0 .. channels - 1 moving sample memory (mode: left write or
+// read), the guide's section 5 steps.
+static void aweDmaOpen(uint32_t mode, uint32_t channels) {
+ for (uint32_t ch = 0; ch < channels; ch++) {
+ aweWrite(R_DCYSUSV, ch, ENV_OFF);
+ aweWrite32(R_VTFT, ch, 0);
+ aweWrite32(R_CVCF, ch, 0);
+ aweWrite32(R_PTRX, ch, PITCH_UNITY);
+ aweWrite32(R_CPF, ch, PITCH_UNITY);
+ aweWrite32(R_PSST, ch, 0);
+ aweWrite32(R_CSL, ch, 0);
+ aweWrite32(R_CCCA, ch, mode);
+ }
+}
+
+
+// Channels 30 and 31 play the FM chip's output through the effects (and
+// so refresh the sample memory): the setup, then the timing write once
+// the pointer's bit 12 has gone round.
+static void aweFmInit(void) {
+ uint16_t ptr = (uint16_t)(sAwe.base + PORT_POINTER);
+
+ aweWrite(R_DCYSUSV, FM_LEFT, ENV_OFF);
+ aweWrite32(R_PSST, FM_LEFT, FM_LEFT_PSST);
+ aweWrite32(R_CSL, FM_LEFT, FM_LEFT_CSL);
+ aweWrite32(R_PTRX, FM_LEFT, 0);
+ aweWrite32(R_CPF, FM_LEFT, 0);
+ aweWrite32(R_CCCA, FM_LEFT, FM_LEFT_CCCA);
+ aweWrite(R_DCYSUSV, FM_RIGHT, ENV_OFF);
+ aweWrite32(R_PSST, FM_RIGHT, FM_RIGHT_PSST);
+ aweWrite32(R_CSL, FM_RIGHT, FM_RIGHT_CSL);
+ aweWrite32(R_PTRX, FM_RIGHT, 0);
+ aweWrite32(R_CPF, FM_RIGHT, FM_RIGHT_CPF);
+ aweWrite32(R_CCCA, FM_RIGHT, FM_RIGHT_CCCA);
+
+ uint32_t flags = cliSave();
+
+ outportw(ptr, (uint16_t)(1 << REG_SHIFT | FM_LEFT));
+ outportw(sAwe.base + PORT_DATA0, 0);
+
+ // Bounded by time and by reads (the clock may not move with
+ // interrupts off)
+ uint64_t until = platformMicros() + PHASE_WAIT_US;
+
+ for (int32_t i = 0; i < POLL_MAX && !(inportw(ptr) & POINTER_PHASE) && platformMicros() < until; i++) {
+ }
+
+ until = platformMicros() + PHASE_WAIT_US;
+
+ for (int32_t i = 0; i < POLL_MAX && (inportw(ptr) & POINTER_PHASE) && platformMicros() < until; i++) {
+ }
+
+ outportw(sAwe.base + PORT_DATA0, FM_TIMING);
+ outportw(ptr, (uint16_t)(1 << REG_SHIFT | HW_FM_TIMING));
+ outportw(sAwe.base + PORT_DATA1, 0);
+ flagsRestore(flags);
+ aweWrite32(R_VTFT, FM_LEFT, FM_VTFT);
+ aweWrite32(R_VTFT, FM_RIGHT, FM_VTFT);
+}
+
+
+// The guide's section 4: every channel cleared with its envelope engine
+// off (sound registers before the current ones), the memory pointers,
+// the four sets of initialization arrays (a pause after the first), the
+// effects configuration, the FM channels; audio on after the memory is
+// sized.
+static void aweInit(void) {
+ aweWrite(R_HW, HW_CF1, CF1_INIT);
+ aweWrite(R_HW, HW_CF2, CF2_INIT);
+ aweWrite(R_HW, HW_CF3, 0);
+
+ for (uint32_t ch = 0; ch < AWE_CHANNELS; ch++) {
+ aweWrite(R_DCYSUSV, ch, ENV_OFF);
+ }
+
+ for (uint32_t ch = 0; ch < AWE_CHANNELS; ch++) {
+ aweWrite(R_ENVVOL, ch, 0);
+ aweWrite(R_ENVVAL, ch, 0);
+ aweWrite(R_DCYSUS, ch, 0);
+ aweWrite(R_ATKHLDV, ch, 0);
+ aweWrite(R_LFO1VAL, ch, 0);
+ aweWrite(R_ATKHLD, ch, 0);
+ aweWrite(R_LFO2VAL, ch, 0);
+ aweWrite(R_IP, ch, 0);
+ aweWrite(R_IFATN, ch, 0);
+ aweWrite(R_PEFE, ch, 0);
+ aweWrite(R_FMMOD, ch, 0);
+ aweWrite(R_TREMFRQ, ch, 0);
+ aweWrite(R_FM2FRQ2, ch, 0);
+ aweWrite32(R_PTRX, ch, 0);
+ aweWrite32(R_VTFT, ch, 0);
+ aweWrite32(R_PSST, ch, 0);
+ aweWrite32(R_CSL, ch, 0);
+ aweWrite32(R_CCCA, ch, 0);
+ }
+
+ for (uint32_t ch = 0; ch < AWE_CHANNELS; ch++) {
+ aweWrite32(R_CPF, ch, 0);
+ aweWrite32(R_CVCF, ch, 0);
+ }
+
+ aweWrite32(R_HW, HW_SMALR, 0);
+ aweWrite32(R_HW, HW_SMALR + 1, 0);
+ aweWrite32(R_HW, HW_SMALW, 0);
+ aweWrite32(R_HW, HW_SMALW + 1, 0);
+
+ for (int32_t set = 0; set < INIT_SETS; set++) {
+ if (set == INIT_SETS - 1) {
+ aweWrite32(R_HW, HW_CF4, CF4_INIT);
+ aweWrite32(R_HW, HW_CF5, CF5_INIT);
+ aweWrite32(R_HW, HW_CF6, CF6_INIT);
+ }
+
+ for (int32_t w = 0; w < INIT_ARRAYS * INIT_WORDS; w++) {
+ uint32_t ch = (uint32_t)(w % INIT_WORDS);
+
+ switch (w / INIT_WORDS) {
+ case 0:
+ aweWrite(R_INIT1, ch, aweInitWord(set, w));
+ break;
+
+ case 1:
+ aweWrite(R_INIT2, ch, aweInitWord(set, w));
+ break;
+
+ case 2:
+ aweWrite(R_INIT3, ch, aweInitWord(set, w));
+ break;
+
+ default:
+ aweWrite(R_INIT4, ch, aweInitWord(set, w));
+ break;
+ }
+ }
+
+ if (set == 0) {
+ aweWaitSamples(INIT_WAIT_SAMPLES);
+ }
+ }
+
+ aweFmInit();
+}
+
+
+// Word w (INIT1's 32, then INIT2's ...) of set 0..3: the first set a
+// pattern (even words 03FFh up 400h a pair, odd 0030h with the pair's
+// number in the high nibble), the second the same with bit 15 on the odd
+// words, the third the fourth with it.
+static uint16_t aweInitWord(int32_t set, int32_t word) {
+ uint16_t odd = (word & 1) ? INIT_ODD_BIT : 0;
+ int32_t pair = word / 2;
+ uint16_t first = (word & 1) ? (uint16_t)(INIT1_ODD_BASE | (pair & INIT1_ODD_MASK) << INIT1_ODD_SHIFT) : (uint16_t)(INIT1_EVEN_BASE + INIT1_EVEN_STEP * pair);
+
+ switch (set) {
+ case 0:
+ return first;
+
+ case 1:
+ return (uint16_t)(first | odd);
+
+ case 2:
+ return (uint16_t)(sInit4[word] | odd);
+
+ default:
+ return sInit4[word];
+ }
+}
+
+
+// The interrupt-time code and state, aweCalc's and drvMath's.
+static bool aweLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)aweActive, 0 },
+ { (const void *)aweRead, 0 },
+ { (const void *)aweRead32, 0 },
+ { (const void *)aweRelease, 0 },
+ { (const void *)aweStart, 0 },
+ { (const void *)aweStop, 0 },
+ { (const void *)aweUpdate, 0 },
+ { (const void *)aweWrite, 0 },
+ { (const void *)aweWrite32, 0 },
+ { (const void *)cliSave, 0 },
+ { (const void *)flagsRestore, 0 },
+ { &sAwe, sizeof(sAwe) }
+ };
+ const AudioLockItemT calc[] = {
+ { (const void *)aweAtten, 0 },
+ { (const void *)aweAttack, 0 },
+ { (const void *)aweCutoff, 0 },
+ { (const void *)aweDecay, 0 },
+ { (const void *)aweDelay, 0 },
+ { (const void *)aweDepth, 0 },
+ { (const void *)aweHold, 0 },
+ { (const void *)aweLfoFreq, 0 },
+ { (const void *)aweModSustain, 0 },
+ { (const void *)awePan, 0 },
+ { (const void *)awePitch, 0 },
+ { (const void *)aweQ, 0 },
+ { (const void *)aweSend, 0 },
+ { (const void *)aweVolSustain, 0 }
+ };
+
+ bool ok = audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+
+ ok = audioLockSpan(calc, (int32_t)(sizeof(calc) / sizeof(calc[0])), lock) && ok;
+ return drvMathLock(lock) && ok;
+}
+
+
+// BLASTER's E (or A + 400h), the signature, the setup, the memory size.
+static bool aweOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) {
+ SbBlasterT bl;
+
+ (void)cfg;
+
+ if (!sbParseBlaster(getenv("BLASTER"), &bl)) {
+ return false;
+ }
+
+ sAwe.base = (uint16_t)(bl.emu != SB_NONE ? bl.emu : bl.base + EMU_FROM_SB);
+
+ if (!aweDetect()) {
+ return false;
+ }
+
+ sAwe.locked = aweLock(true);
+
+ if (!sAwe.locked) {
+ return false;
+ }
+
+ aweInit();
+ sAwe.dramBytes = aweSizeDram();
+ aweWrite(R_HW, HW_CF3, CF3_AUDIO_ON);
+
+ if (sAwe.dramBytes == 0) {
+ dvxLog("Audio: awe: no sample memory at the EMU8000 at %Xh", (unsigned)sAwe.base);
+ aweClose();
+ return false;
+ }
+
+ info->voices = AWE_VOICES;
+ info->memory = sAwe.dramBytes;
+ info->flags = AUDIO_WT_ENVELOPES | AUDIO_WT_LFOS | AUDIO_WT_FILTER | AUDIO_WT_EFFECTS | AUDIO_WT_ROM;
+ info->level = AWE_VOICE_LEVEL;
+ snprintf(info->description, sizeof(info->description), "Sound Blaster AWE EMU8000 at %Xh (%lu KB)", (unsigned)sAwe.base, (unsigned long)(sAwe.dramBytes / KB));
+ return true;
+}
+
+
+static uint16_t aweRead(uint16_t port, uint32_t reg, uint32_t channel) {
+ uint32_t flags = cliSave();
+
+ outportw(sAwe.base + PORT_POINTER, (uint16_t)(reg << REG_SHIFT | channel));
+
+ uint16_t value = inportw(sAwe.base + port);
+
+ flagsRestore(flags);
+ return value;
+}
+
+
+static uint32_t aweRead32(uint16_t port, uint32_t reg, uint32_t channel) {
+ uint32_t flags = cliSave();
+
+ outportw(sAwe.base + PORT_POINTER, (uint16_t)(reg << REG_SHIFT | channel));
+
+ uint32_t low = inportw(sAwe.base + port);
+ uint32_t high = inportw(sAwe.base + port + HIGH_WORD);
+
+ flagsRestore(flags);
+ return high << WORD_BITS | low;
+}
+
+
+// Both envelopes to their release; a loop that lasts while the key is
+// down moved into the silence after the sample (its end first, as the
+// guide says). Interrupt time.
+static void aweRelease(uint32_t voice) {
+ AweVoiceT *a = &sAwe.voices[voice];
+
+ if (a->loop == AUDIO_WT_LOOP_RELEASE) {
+ aweWrite32(R_CSL, voice, (uint32_t)a->chorus << SEND_SHIFT | (a->end + ONE_SHOT_LOOP_END - 1));
+ aweWrite32(R_PSST, voice, (uint32_t)a->pan << PAN_SHIFT | (a->end + ONE_SHOT_LOOP_START - 1));
+ }
+
+ aweWrite(R_DCYSUS, voice, (uint16_t)(RELEASE_BIT | a->modRelease));
+ aweWrite(R_DCYSUSV, voice, (uint16_t)(RELEASE_BIT | a->volRelease));
+ a->released = true;
+}
+
+
+// What the setup wrote to the memory's first word must be readable there,
+// and each further 512 KB step must hold a word without it showing at the
+// first (the memory wrapping round), with channel 0 writing and channel
+// 1 reading. The bytes the steps found.
+static uint32_t aweSizeDram(void) {
+ uint32_t bytes = 0;
+
+ aweDmaOpen(DMA_LEFT_WRITE, 1);
+ aweWrite32(R_CCCA, 1, DMA_LEFT_READ);
+ aweWrite32(R_HW, HW_SMALW, DRAM_FIRST);
+ aweWrite(R_HW, HW_SMLD, DRAM_PATTERN_A);
+ aweFmInit();
+ aweWaitMem(HW_SMALW);
+ aweWrite32(R_HW, HW_SMALR, DRAM_FIRST);
+ aweRead(R_HW, HW_SMLD);
+
+ bool found = aweRead(R_HW, HW_SMLD) == DRAM_PATTERN_A;
+
+ while (found && bytes + DRAM_STEP_BYTES <= DRAM_MAX_BYTES) {
+ uint32_t at = DRAM_FIRST + (bytes + DRAM_STEP_BYTES) / BYTES_PER_WORD;
+
+ bytes += DRAM_STEP_BYTES;
+ aweWrite32(R_HW, HW_SMALW, at);
+ aweWrite(R_HW, HW_SMLD, DRAM_PATTERN_B);
+ aweWaitMem(HW_SMALW);
+ aweWrite32(R_HW, HW_SMALR, at);
+ aweRead(R_HW, HW_SMLD);
+ found = aweRead(R_HW, HW_SMLD) == DRAM_PATTERN_B;
+ aweWrite32(R_HW, HW_SMALR, DRAM_FIRST);
+ aweRead(R_HW, HW_SMLD);
+ found = found && aweRead(R_HW, HW_SMLD) == DRAM_PATTERN_A;
+ }
+
+ aweDmaClose();
+ aweFmInit();
+ return bytes;
+}
+
+
+// The guide's start: the channel silent with its envelope engine off,
+// every envelope, LFO and filter value, the loop, pan and sends, the
+// start address, then the targets and the decay last, which starts the
+// note. A sample that does not loop loops in the silence after its end
+// (the GM ROM has it after every sample too). Interrupt time.
+static void aweStart(uint32_t voice, const AudioWtVoiceT *v) {
+ AweVoiceT *a = &sAwe.voices[voice];
+ uint32_t start = v->addr & AUDIO_WT_ADDR_ROM ? (v->addr & ~AUDIO_WT_ADDR_ROM) / BYTES_PER_WORD : DRAM_FIRST + v->addr / BYTES_PER_WORD;
+ bool loops = v->loop != AUDIO_WT_LOOP_NONE && v->loopEnd > v->loopStart;
+
+ a->end = start + v->end;
+ a->loopStart = loops ? start + v->loopStart : a->end + ONE_SHOT_LOOP_START;
+ a->loopEnd = loops ? start + v->loopEnd : a->end + ONE_SHOT_LOOP_END;
+ a->cutoff = aweCutoff(v->filterFc);
+ a->pan = awePan(v->pan);
+ a->chorus = aweSend(v->chorus);
+ a->volRelease = aweDecay(v->volEnv[AUDIO_WT_ENV_RELEASE]);
+ a->modRelease = aweDecay(v->modEnv[AUDIO_WT_ENV_RELEASE]);
+ a->loop = loops ? v->loop : AUDIO_WT_LOOP_NONE;
+ a->released = false;
+
+ aweWrite(R_DCYSUSV, voice, ENV_OFF);
+ aweWrite32(R_VTFT, voice, VTFT_SILENT);
+ aweWrite32(R_CVCF, voice, CVCF_SILENT);
+ aweWrite32(R_PTRX, voice, 0);
+ aweWrite32(R_CPF, voice, 0);
+ aweWrite(R_IP, voice, awePitch(v->pitch));
+ aweWrite(R_ENVVAL, voice, aweDelay(v->modEnv[AUDIO_WT_ENV_DELAY]));
+ aweWrite(R_ATKHLD, voice, (uint16_t)(aweHold(v->modEnv[AUDIO_WT_ENV_HOLD]) << HOLD_SHIFT | aweAttack(v->modEnv[AUDIO_WT_ENV_ATTACK])));
+ aweWrite(R_DCYSUS, voice, (uint16_t)(aweModSustain(v->modEnv[AUDIO_WT_ENV_SUSTAIN]) << SUSTAIN_SHIFT | aweDecay(v->modEnv[AUDIO_WT_ENV_DECAY])));
+ aweWrite(R_ENVVOL, voice, aweDelay(v->volEnv[AUDIO_WT_ENV_DELAY]));
+ aweWrite(R_ATKHLDV, voice, (uint16_t)(aweHold(v->volEnv[AUDIO_WT_ENV_HOLD]) << HOLD_SHIFT | aweAttack(v->volEnv[AUDIO_WT_ENV_ATTACK])));
+ aweWrite(R_IFATN, voice, (uint16_t)(a->cutoff << FILTER_SHIFT | aweAtten(v->attenuation)));
+ aweWrite(R_PEFE, voice, (uint16_t)(aweDepth(v->modEnvToPitch, STEP_PITCH_NUM, STEP_PITCH_DEN) << DEPTH_SHIFT | aweDepth(v->modEnvToFilter, STEP_ENV_CUT_NUM, STEP_ENV_CUT_DEN)));
+ aweWrite(R_LFO1VAL, voice, aweDelay(v->modLfoDelay));
+ aweWrite(R_LFO2VAL, voice, aweDelay(v->vibLfoDelay));
+ aweWrite(R_FMMOD, voice, (uint16_t)(aweDepth(v->modLfoToPitch, STEP_PITCH_NUM, STEP_PITCH_DEN) << DEPTH_SHIFT | aweDepth(v->modLfoToFilter, STEP_LFO_CUT_NUM, STEP_LFO_CUT_DEN)));
+ aweWrite(R_TREMFRQ, voice, (uint16_t)(aweDepth(v->modLfoToVolume, STEP_TREM_NUM, STEP_TREM_DEN) << DEPTH_SHIFT | aweLfoFreq(v->modLfoFreq)));
+ aweWrite(R_FM2FRQ2, voice, (uint16_t)(aweDepth(v->vibLfoToPitch, STEP_PITCH_NUM, STEP_PITCH_DEN) << DEPTH_SHIFT | aweLfoFreq(v->vibLfoFreq)));
+ aweWrite32(R_PSST, voice, (uint32_t)a->pan << PAN_SHIFT | (a->loopStart - 1));
+ aweWrite32(R_CSL, voice, (uint32_t)a->chorus << SEND_SHIFT | (a->loopEnd - 1));
+ aweWrite32(R_CCCA, voice, (uint32_t)aweQ(v->filterQ) << Q_SHIFT | (start + v->offset - 1));
+ aweWrite32(R_UNKNOWN4, voice, 0);
+ aweWrite32(R_UNKNOWN5, voice, 0);
+ aweWrite32(R_VTFT, voice, VOL_TARGET_FULL | (uint32_t)a->cutoff << FILTER_SHIFT);
+ aweWrite32(R_CVCF, voice, VOL_TARGET_FULL | CF_FULL);
+ aweWrite32(R_PTRX, voice, PITCH_UNITY | (uint32_t)aweSend(v->reverb) << REVERB_SHIFT);
+ aweWrite32(R_CPF, voice, PITCH_UNITY);
+ aweWrite(R_DCYSUSV, voice, (uint16_t)(aweVolSustain(v->volEnv[AUDIO_WT_ENV_SUSTAIN]) << SUSTAIN_SHIFT | aweDecay(v->volEnv[AUDIO_WT_ENV_DECAY])));
+}
+
+
+// The guide's hard stop. Interrupt time.
+static void aweStop(uint32_t voice) {
+ aweWrite(R_DCYSUSV, voice, ENV_OFF);
+ aweWrite32(R_VTFT, voice, VTFT_SILENT);
+ aweWrite32(R_CVCF, voice, CVCF_SILENT);
+ sAwe.voices[voice].released = true;
+}
+
+
+// Changes while the note plays (the guide's section 8). Interrupt time.
+static void aweUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ AweVoiceT *a = &sAwe.voices[voice];
+
+ if (what & AUDIO_WT_SET_PITCH) {
+ aweWrite(R_IP, voice, awePitch(v->pitch));
+ }
+
+ if (what & (AUDIO_WT_SET_VOLUME | AUDIO_WT_SET_FILTER)) {
+ a->cutoff = aweCutoff(v->filterFc);
+ aweWrite(R_IFATN, voice, (uint16_t)(a->cutoff << FILTER_SHIFT | aweAtten(v->attenuation)));
+ }
+
+ if (what & AUDIO_WT_SET_PAN) {
+ a->pan = awePan(v->pan);
+ aweWrite32(R_PSST, voice, (uint32_t)a->pan << PAN_SHIFT | (a->loopStart - 1));
+ }
+
+ if (what & AUDIO_WT_SET_EFFECTS) {
+ a->chorus = aweSend(v->chorus);
+ aweWrite32(R_CSL, voice, (uint32_t)a->chorus << SEND_SHIFT | (a->loopEnd - 1));
+ aweWrite32(R_PTRX, voice, (aweRead32(R_PTRX, voice) & PITCH_TARGET_MASK) | (uint32_t)aweSend(v->reverb) << REVERB_SHIFT);
+ }
+
+ if (what & AUDIO_WT_SET_LFO) {
+ aweWrite(R_FMMOD, voice, (uint16_t)(aweDepth(v->modLfoToPitch, STEP_PITCH_NUM, STEP_PITCH_DEN) << DEPTH_SHIFT | aweDepth(v->modLfoToFilter, STEP_LFO_CUT_NUM, STEP_LFO_CUT_DEN)));
+ aweWrite(R_FM2FRQ2, voice, (uint16_t)(aweDepth(v->vibLfoToPitch, STEP_PITCH_NUM, STEP_PITCH_DEN) << DEPTH_SHIFT | aweLfoFreq(v->vibLfoFreq)));
+ }
+}
+
+
+// 16-bit frames through all 30 voice channels set to write (nothing plays
+// while samples load), a run at a time with interrupts off (the pointer
+// must stay on the data register). The FM channels (30, 31) are not
+// touched, so they go on refreshing the memory. Program time.
+static bool aweUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits) {
+ uint16_t ptr = (uint16_t)(sAwe.base + PORT_POINTER);
+ uint16_t data = (uint16_t)(sAwe.base + PORT_DATA1);
+
+ if (bits != WORD_BITS || addr > sAwe.dramBytes || count > (sAwe.dramBytes - addr) / BYTES_PER_WORD) {
+ return false;
+ }
+
+ aweDmaOpen(DMA_LEFT_WRITE, AWE_VOICES);
+ aweWaitMem(HW_SMALW);
+ aweWrite32(R_HW, HW_SMALW, DRAM_FIRST + addr / BYTES_PER_WORD);
+
+ for (uint32_t done = 0; done < count;) {
+ uint32_t run = count - done < UPLOAD_RUN ? count - done : UPLOAD_RUN;
+ uint32_t flags = cliSave();
+
+ outportw(ptr, (uint16_t)(1 << REG_SHIFT | HW_SMLD));
+
+ for (uint32_t i = 0; i < run; i++) {
+ outportw(data, (uint16_t)frames[done + i]);
+ }
+
+ flagsRestore(flags);
+ done += run;
+ }
+
+ aweDmaClose();
+ return true;
+}
+
+
+// The memory write (or read) pointer no longer busy (bounded wait).
+static void aweWaitMem(uint32_t reg) {
+ for (int32_t i = 0; i < POLL_MAX && (aweRead32(R_HW, reg) & MEM_BUSY); i++) {
+ }
+}
+
+
+// samples sample periods by the chip's own counter (it wraps at 65536).
+static void aweWaitSamples(uint32_t samples) {
+ uint16_t from = aweRead(R_HW2, HW_WC);
+
+ for (int32_t i = 0; i < POLL_MAX && (uint16_t)(aweRead(R_HW2, HW_WC) - from) < samples; i++) {
+ }
+}
+
+
+static void aweWrite(uint16_t port, uint32_t reg, uint32_t channel, uint16_t value) {
+ uint32_t flags = cliSave();
+
+ outportw(sAwe.base + PORT_POINTER, (uint16_t)(reg << REG_SHIFT | channel));
+ outportw(sAwe.base + port, value);
+ flagsRestore(flags);
+}
+
+
+// A double word: the low word, then at once the high.
+static void aweWrite32(uint16_t port, uint32_t reg, uint32_t channel, uint32_t value) {
+ uint32_t flags = cliSave();
+
+ outportw(sAwe.base + PORT_POINTER, (uint16_t)(reg << REG_SHIFT | channel));
+ outportw(sAwe.base + port, (uint16_t)(value & WORD_MASK));
+ outportw(sAwe.base + port + HIGH_WORD, (uint16_t)(value >> WORD_BITS));
+ flagsRestore(flags);
+}
+
+
+static uint32_t cliSave(void) {
+ uint32_t flags;
+
+ __asm__ __volatile__ ("pushfl\n\tpopl %0\n\tcli" : "=r"(flags) : : "memory");
+ return flags;
+}
+
+
+static void flagsRestore(uint32_t flags) {
+ if (flags & FLAGS_IF) {
+ __asm__ __volatile__ ("sti" : : : "memory");
+ }
+}
diff --git a/src/libs/kpunch/audio/drivers/gus/gusDrv.c b/src/libs/kpunch/audio/drivers/gus/gusDrv.c
new file mode 100644
index 00000000..1aa23aae
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/gus/gusDrv.c
@@ -0,0 +1,690 @@
+// 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.
+
+// gusDrv.c -- the Gravis UltraSound sound driver (gus.drv)
+//
+// Where the card is: the ULTRASND variable (port, DRAM DMA, record DMA,
+// GF1 IRQ, MIDI IRQ: SET ULTRASND=220,1,1,11,5). The GF1 is reset with
+// DVX.INI [audio] gusVoices active (14..32, default 28: the output runs
+// at 617,400 / voices Hz, exactly 22,050 at 28), its latches set to that
+// DMA channel and IRQ, and its memory sized.
+//
+// Digital sound plays on voices 0 and 1 (left and right; one centred for
+// mono) from a ring in the GF1's memory at its top: two halves per
+// channel, 16-bit (8-bit when the configuration asks), at the mixing
+// rate, which the GF1 resamples to its output rate. The SDK's double
+// buffer (3.11): the left voice plays the first half with its end at the
+// middle and rollover on (an interrupt there, playing on), then the
+// second with its end at the end and looping on (an interrupt as it loops
+// back); the right voice loops the whole ring with no interrupt. Each
+// interrupt mixes the half just played (audio.lib's mixer, inside the
+// interrupt with interrupts enabled again) and moves it into the card by
+// DMA, left then right, the second transfer started by the first's
+// terminal count interrupt. Which half is free comes from where the left
+// voice is, so a repeated interrupt changes nothing. A loop starts one
+// sample before the ring, at a pad holding a copy of the ring's last
+// sample (the SDK's fix for the click a GF1 makes looping from end to
+// start).
+//
+// The rest of the voices and the memory below the ring are the wavetable
+// synthesizer's (guswt.drv's code, linked in here too, handed to audio.lib
+// as this driver's own: AudioDrvInfoT.synth). The card stays on while
+// either plays; opening again while the synthesizer plays (after the
+// output was closed for quiet) leaves the card as it is.
+//
+// Recording (recordStart / recordStop) uses ULTRASND's second DMA channel
+// (the first when there is no usable second), latched with the first at
+// reset. An UltraSound MAX (its board version 10 or 11) records
+// through its CS4231 codec (codec/codecHw.c, as wss.drv drives one):
+// turned on by the MAX's control register at base + 506h, found at base +
+// 10Ch, capturing in single-DMA mode over two halves of an auto-init
+// buffer, its interrupt on the GF1's line (looked at after the GF1's
+// sources). Other boards record with the GF1's own sampling: 8-bit
+// unsigned, mono or stereo, 5,510 to 44,100 Hz, from the line input or
+// the microphone (the mix register); the GF1 stops at each terminal count
+// (it has no auto-init), so each half is started from the interrupt that
+// ends the other, the next half first and then the finished one handed
+// on. Neither the CD input nor a MAX's AUX inputs are recorded. (No
+// emulator this was written against records on a GUS: Linux's gus_pcm.c
+// and gusmax.c are the reference.)
+
+#include "audioDrv.h"
+#include "../../codec/codecHw.h"
+#include "../../gus/gusHw.h"
+#include "../../gus/gusSynth.h"
+#include "../../isa/isaHw.h"
+
+#include
+#include
+#include
+#include
+
+#define PCM_LEFT 0
+#define PCM_RIGHT 1
+#define PCM_VOICES 2
+#define PCM_HALF_FRAMES 512 // about 23 ms at 22,050 Hz
+#define PCM_HALVES 2
+#define PCM_RING (PCM_HALF_FRAMES * PCM_HALVES)
+#define PCM_VOLUME GUS_VOLUME_MAX
+#define CHANNELS_MAX 2
+#define BITS_8 8
+#define BITS_16 16
+#define BYTE_BITS 8
+#define WORD_BYTES 4
+#define SILENCE_8 0x80
+#define ISR_PASSES 16 // 2X6 looked at, at most, per interrupt
+#define IRQ_SOURCE_NONE 0xC0 // 8Fh: neither a wave nor a volume interrupt
+#define IRQ_SOURCE_WAVE 0x80 // clear: a wave interrupt
+#define IRQ_SOURCE_VOICE 0x1F
+#define FIRST_IRQ_WAIT_US 500000
+#define HALF_NONE (-1)
+#define REC_HALF_FRAMES 512 // a recorded half (at most 2 KB: 16-bit stereo)
+#define REC_HALVES 2
+
+// A MAX codec's input for each source recorded (the GF1's: the mix
+// register)
+static const uint8_t sMaxInputs[] = {
+ [AUDIO_REC_MIC] = CODEC_INPUT_MIC | CODEC_INPUT_MIC_BOOST,
+ [AUDIO_REC_LINE] = CODEC_INPUT_LINE
+};
+
+typedef struct {
+ int16_t mixed[PCM_HALF_FRAMES * CHANNELS_MAX]; // one half, as the mixer gives it (or as it was recorded)
+ int16_t chan[PCM_HALF_FRAMES]; // one channel of it
+ IsaDmaT dma; // the staging buffer: a half of each channel (recording: the two halves)
+ IsaIrqT irq;
+ GusEnvT env;
+ CodecT codec; // a MAX's (recording)
+ uint32_t voices; // DVX.INI gusVoices (cardUp)
+ uint32_t recHalfBytes; // a recorded half
+ uint32_t ring[CHANNELS_MAX]; // each channel's ring in DRAM (its pad just below)
+ uint32_t rate;
+ uint32_t bits;
+ uint32_t channels;
+ uint32_t sampleBytes;
+ uint32_t halfBytes; // one channel's half
+ uint8_t voiceMode; // 16-bit or 8-bit
+ uint8_t dmaFlags; // 41h for a transfer
+ uint8_t format; // a MAX codec's I8 (recording)
+ uint8_t sampleCtrl; // 49h while the GF1 samples
+ uint8_t sampleRate; // 48h
+ volatile int32_t lastHalf; // the half refilled last
+ volatile int32_t pendingHalf; // the right channel's transfer waits for the left's
+ volatile int32_t sampleHalf; // the half the GF1 samples into
+ volatile uint32_t irqCount; // halves played (or recorded)
+ volatile uint32_t tcCount; // DMA transfers done
+ bool locked;
+ bool playing;
+ bool max; // an UltraSound MAX: its codec records
+ bool recording;
+ bool recCodec; // recording through the MAX's codec
+} GusPcmT;
+
+static GusPcmT sPcm = { .dma.selector = -1 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void cardUp(void);
+static uint32_t chanAddr(uint32_t channel, uint32_t frame);
+static void dmaStart(uint32_t channel, int32_t half);
+static void gusCapture(uint32_t half);
+static void gusClose(void);
+static void gusIsr(void);
+static bool gusLockPcm(bool lock);
+static bool gusOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
+static bool gusPlay(void);
+static bool gusRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got);
+static void halfEnded(void);
+static bool maxCodecOn(void);
+static void sampleNext(void);
+static void sampleStart(uint32_t half);
+static void voiceAddr(uint32_t voice, uint8_t reg, uint32_t addr, bool lowFirst);
+static void voicesStart(void);
+
+AUDIO_DRIVER("gus", AUDIO_PRIORITY_GUS,
+ .open = gusOpen,
+ .play = gusPlay,
+ .close = gusClose,
+ .recordStart = gusRecordStart,
+ .recordStop = gusClose
+);
+
+// The synthesizer on the other voices
+static const AudioSynthDriverT sGusSynth = {
+ .hdr = DVX_DRIVER_HDR(AudioSynthDriverT, "gus", AUDIO_PRIORITY_GUS),
+ .kind = AUDIO_SYNTH_WAVETABLE,
+ .open = gusSynthOpen,
+ .close = gusSynthClose,
+ .upload = gusSynthUpload,
+ .start = gusSynthStart,
+ .update = gusSynthUpdate,
+ .release = gusSynthRelease,
+ .stop = gusSynthStop,
+ .active = gusSynthActive
+};
+
+
+// The card reset with DVX.INI's voices and routed to ULTRASND's IRQ and
+// both DMA channels, unless it already runs (the synthesizer, or this
+// driver since its last release).
+static void cardUp(void) {
+ if (gusHw.running) {
+ return;
+ }
+
+ gusHw.dramBytes = gusSizeDram();
+ gusHw.park = gusHw.dramBytes - GUS_ALIGN;
+ gusOutput(false);
+ gusReset(sPcm.voices);
+ gusLatches(gusIrqCode(sPcm.env.irq1), gusDmaLatch(sPcm.env.dma1, sPcm.env.dma2));
+ gusHw.running = true;
+}
+
+
+// A channel's ring frame as a byte address in DRAM (frame -1: the pad).
+static uint32_t chanAddr(uint32_t channel, uint32_t frame) {
+ return sPcm.ring[channel] + frame * sPcm.sampleBytes;
+}
+
+
+// One channel's half from the staging buffer into its ring by DMA: the PC
+// channel, then the GF1's start address and control. Interrupt time.
+static void dmaStart(uint32_t channel, int32_t half) {
+ uint32_t addr = chanAddr(channel, (uint32_t)half * PCM_HALF_FRAMES);
+
+ if (sPcm.dma.channel >= ISA_DMA_FIRST_16) {
+ addr = gusAddr16(addr);
+ }
+
+ isaDmaProgram(&sPcm.dma, channel * sPcm.halfBytes, sPcm.halfBytes, ISA_DMA_ONCE_PLAY);
+ gusWrite16(GUS_G_DMA_ADDR, (uint16_t)(addr >> GUS_DMA_ADDR_SHIFT));
+ gusWrite8(GUS_G_DMA_CONTROL, sPcm.dmaFlags);
+}
+
+
+// Hand the half just recorded to audio.lib. Interrupt time.
+static void gusCapture(uint32_t half) {
+ isaDmaRead(&sPcm.dma, half * sPcm.recHalfBytes, (uint32_t *)sPcm.mixed, sPcm.recHalfBytes / WORD_BYTES);
+ audioRecPush(sPcm.mixed, sPcm.recHalfBytes);
+}
+
+
+// Stops playing or recording (recordStop is this too). Also runs from
+// audio.lib's shutdown hook, so it logs nothing and copes with any stage
+// of gusOpen, gusPlay, and gusRecordStart.
+static void gusClose(void) {
+ if (sPcm.playing) {
+ gusVoiceStop(PCM_LEFT);
+ gusVoiceStop(PCM_RIGHT);
+ gusWrite8(GUS_G_DMA_CONTROL, 0);
+ isaDmaStop(&sPcm.dma);
+ sPcm.playing = false;
+ }
+
+ if (sPcm.recording && sPcm.recCodec) {
+ codecStop(&sPcm.codec);
+ } else if (sPcm.recording) {
+ gusWrite8(GUS_G_SAMPLE_CTRL, 0);
+ gusRead8(GUS_G_SAMPLE_CTRL);
+ gusInputs(false, false);
+ }
+
+ if (sPcm.recording) {
+ isaDmaStop(&sPcm.dma);
+ sPcm.recording = false;
+ sPcm.recCodec = false;
+ }
+
+ gusHw.pcmOpen = false;
+ gusRelease();
+
+ isaIrqUnhook(&sPcm.irq);
+
+ if (sPcm.locked) {
+ gusLockPcm(false);
+ sPcm.locked = false;
+ }
+
+ isaDmaFree(&sPcm.dma);
+}
+
+
+// The GF1's interrupt: every source 2X6 shows taken (a terminal count by
+// reading 41h, or 49h while the GF1 samples, wave and volume interrupts
+// by draining the 8Fh queue), then a MAX codec's own interrupt (the same
+// line); then, for the left voice's wave interrupt, the half just played
+// mixed, for a sampled block the next started and this one handed on, for
+// the codec the half it finished handed on, with interrupts enabled; the
+// PIC acknowledged last.
+static void gusIsr(void) {
+ bool wave = false;
+ bool sampled = false;
+
+ if (isaIrqSpurious(&sPcm.irq)) {
+ return;
+ }
+
+ for (int32_t pass = 0; pass < ISR_PASSES; pass++) {
+ uint8_t status = inportb(gusHw.base + GUS_IRQ_STATUS);
+
+ if (!(status & (GUS_IRQ_WAVE | GUS_IRQ_VOLUME | GUS_IRQ_DMA))) {
+ break;
+ }
+
+ if ((status & GUS_IRQ_DMA) && sPcm.recording) {
+ sampled = (gusRead8(GUS_G_SAMPLE_CTRL) & GUS_SC_IRQ_PENDING) || sampled;
+ } else if (status & GUS_IRQ_DMA) {
+ gusRead8(GUS_G_DMA_CONTROL);
+ sPcm.tcCount++;
+
+ if (sPcm.pendingHalf != HALF_NONE) {
+ int32_t half = sPcm.pendingHalf;
+
+ sPcm.pendingHalf = HALF_NONE;
+ dmaStart(PCM_RIGHT, half);
+ }
+ }
+
+ for (uint8_t src = gusRead8(GUS_G_IRQ_SOURCE); (src & IRQ_SOURCE_NONE) != IRQ_SOURCE_NONE; src = gusRead8(GUS_G_IRQ_SOURCE)) {
+ wave = wave || (!(src & IRQ_SOURCE_WAVE) && (src & IRQ_SOURCE_VOICE) == PCM_LEFT);
+ }
+ }
+
+ bool captured = sPcm.recCodec && codecIrq(&sPcm.codec);
+
+ if (wave && sPcm.playing) {
+ sPcm.irqCount++;
+ halfEnded();
+ }
+
+ if (sampled && sPcm.recording && !sPcm.recCodec) {
+ sPcm.irqCount++;
+ sampleNext();
+ }
+
+ if (captured) {
+ uint32_t half = isaDmaPosition(&sPcm.dma) < sPcm.recHalfBytes ? 1 : 0;
+
+ sPcm.irqCount++;
+ __asm__ __volatile__ ("sti" : : : "memory");
+ gusCapture(half);
+ __asm__ __volatile__ ("cli" : : : "memory");
+ }
+
+ isaIrqEnd(&sPcm.irq);
+}
+
+
+// This file's interrupt-time code and state, and the shared GF1, codec,
+// and ISA code's.
+static bool gusLockPcm(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)chanAddr, 0 },
+ { (const void *)dmaStart, 0 },
+ { (const void *)gusCapture, 0 },
+ { (const void *)gusIsr, 0 },
+ { (const void *)halfEnded, 0 },
+ { (const void *)sampleNext, 0 },
+ { (const void *)sampleStart, 0 },
+ { (const void *)voiceAddr, 0 },
+ { &sPcm, sizeof(sPcm) }
+ };
+
+ bool ok = audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+
+ ok = isaLock(lock) && ok;
+ ok = codecLock(lock) && ok;
+ return gusLock(lock) && ok;
+}
+
+
+// ULTRASND, the card at its port, reset and routed; the ring at the top
+// of its memory, under the silent park block.
+static bool gusOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
+ memset(&sPcm, 0, sizeof(sPcm));
+ sPcm.dma.selector = -1;
+
+ if (!gusParseEnv(getenv("ULTRASND"), &sPcm.env)) {
+ dvxLog("Audio: gus: ULTRASND is not set (for example SET ULTRASND=220,1,1,11,5)");
+ return false;
+ }
+
+ if (!gusHw.running && !gusProbe((uint16_t)sPcm.env.base)) {
+ dvxLog("Audio: gus: no UltraSound at %lXh", (unsigned long)sPcm.env.base);
+ return false;
+ }
+
+ if (gusIrqCode(sPcm.env.irq1) == 0 || gusDmaCode(sPcm.env.dma1) == 0) {
+ dvxLog("Audio: gus: ULTRASND's IRQ %lu or DMA %lu cannot be used", (unsigned long)sPcm.env.irq1, (unsigned long)sPcm.env.dma1);
+ return false;
+ }
+
+ sPcm.bits = cfg->bits ? cfg->bits : BITS_16;
+ sPcm.channels = cfg->channels ? cfg->channels : CHANNELS_MAX;
+ sPcm.sampleBytes = sPcm.bits / BYTE_BITS;
+ sPcm.halfBytes = PCM_HALF_FRAMES * sPcm.sampleBytes;
+ sPcm.voiceMode = sPcm.bits == BITS_16 ? GUS_VC_16BIT : 0;
+ sPcm.lastHalf = HALF_NONE;
+ sPcm.pendingHalf = HALF_NONE;
+ sPcm.voices = (uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, GUS_INI_VOICES, GUS_VOICES_DEFAULT);
+ sPcm.max = gusIsMax(gusBoardVersion());
+
+ // The top of the memory: the park block, then each channel's ring with
+ // a block below it (its pad at the top of that) and one above; the
+ // synthesizer has what is below and the voices after these two
+ cardUp();
+
+ uint32_t span = (PCM_RING * sPcm.sampleBytes + 2 * GUS_ALIGN + GUS_ALIGN - 1) / GUS_ALIGN * GUS_ALIGN;
+
+ for (uint32_t c = 0; c < CHANNELS_MAX; c++) {
+ sPcm.ring[c] = gusHw.park - (c + 1) * span + GUS_ALIGN;
+ }
+
+ gusHw.synthTop = sPcm.ring[CHANNELS_MAX - 1] - GUS_ALIGN;
+ gusHw.synthFirst = PCM_VOICES;
+ gusHw.pcmOpen = true;
+
+ sPcm.rate = cfg->rate < gusHw.outRate ? cfg->rate : gusHw.outRate;
+ sPcm.dmaFlags = (uint8_t)(GUS_DMA_ENABLE | GUS_DMA_TC_IRQ | (sPcm.env.dma1 >= ISA_DMA_FIRST_16 ? GUS_DMA_WIDE_CHAN : 0) | (sPcm.bits == BITS_16 ? GUS_DMA_DATA16 : GUS_DMA_INVERT));
+
+ if (!isaDmaAlloc(&sPcm.dma, sPcm.env.dma1, sPcm.halfBytes * CHANNELS_MAX)) {
+ dvxLog("Audio: gus: no conventional memory for the DMA buffer");
+ return false;
+ }
+
+ snprintf(info->description, sizeof(info->description), "Gravis UltraSound%s at %lXh (%lu KB)", sPcm.max ? " MAX" : "", (unsigned long)sPcm.env.base, (unsigned long)(gusHw.dramBytes / 1024));
+ info->rate = sPcm.rate;
+ info->bits = sPcm.bits;
+ info->channels = sPcm.channels;
+ info->hasDac = true;
+ info->hasSynth = true;
+ info->synth = &sGusSynth;
+ info->recSources = (1U << AUDIO_REC_MIC) | (1U << AUDIO_REC_LINE);
+ dvxLog("Audio: gus: %lXh%s, IRQ %lu, DMA %lu, %lu KB, %lu voices (%lu Hz)", (unsigned long)sPcm.env.base, sPcm.max ? " (MAX)" : "", (unsigned long)sPcm.env.irq1, (unsigned long)sPcm.env.dma1, (unsigned long)(gusHw.dramBytes / 1024), (unsigned long)gusHw.voices, (unsigned long)gusHw.outRate);
+ return true;
+}
+
+
+static bool gusPlay(void) {
+ sPcm.locked = gusLockPcm(true);
+
+ if (!sPcm.locked) {
+ return false;
+ }
+
+ isaDmaFill(&sPcm.dma, sPcm.bits == BITS_16 ? 0 : SILENCE_8);
+
+ if (!isaIrqHook(&sPcm.irq, sPcm.env.irq1, gusIsr)) {
+ return false;
+ }
+
+ // The rings and their pads silent (8-bit samples signed in the card)
+ for (uint32_t c = 0; c < sPcm.channels; c++) {
+ for (uint32_t b = 0; b < (PCM_RING + 1) * sPcm.sampleBytes; b++) {
+ gusPoke(chanAddr(c, 0) - sPcm.sampleBytes + b, 0);
+ }
+ }
+
+ for (uint32_t b = 0; b < GUS_ALIGN; b++) {
+ gusPoke(gusHw.park + b, 0);
+ }
+
+
+ sPcm.playing = true;
+ voicesStart();
+ gusOutput(true);
+
+ // A card on another IRQ than ULTRASND says plays one ring and stops,
+ // and one on another DMA channel never finishes a transfer; give up
+ // on it so the next driver is tried.
+ uint64_t until = platformMicros() + FIRST_IRQ_WAIT_US;
+
+ while ((sPcm.irqCount == 0 || sPcm.tcCount == 0) && platformMicros() < until) {
+ platformYield();
+ }
+
+ if (sPcm.irqCount == 0) {
+ dvxLog("Audio: gus: no interrupt from the card on IRQ %lu (check ULTRASND)", (unsigned long)sPcm.env.irq1);
+ return false;
+ }
+
+ if (sPcm.tcCount == 0) {
+ dvxLog("Audio: gus: no DMA transfer into the card finished on DMA %lu (check ULTRASND)", (unsigned long)sPcm.env.dma1);
+ return false;
+ }
+
+
+ return true;
+}
+
+
+// Take the card for recording (the output is closed): a MAX's codec, else
+// the GF1's sampling, on the record channel, from source; got is what it
+// records. The first half must finish within FIRST_IRQ_WAIT_US (else the
+// IRQ or the channel is wrong, or nothing here records).
+static bool gusRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got) {
+ uint32_t dma = gusRecordDma(sPcm.env.dma1, sPcm.env.dma2);
+ uint32_t channels = want->channels == CHANNELS_MAX ? CHANNELS_MAX : 1;
+ uint32_t bits = BITS_8;
+ uint32_t rate;
+
+ cardUp();
+ gusHw.pcmOpen = true;
+ sPcm.recCodec = sPcm.max && maxCodecOn();
+
+ if (sPcm.recCodec) {
+ bits = want->bits == BITS_8 ? BITS_8 : BITS_16;
+ sPcm.format = (uint8_t)(codecFormat(bits, channels) | codecRateCode(want->rate, &rate));
+ } else {
+ sPcm.sampleRate = gusSampleRate(want->rate, &rate);
+ sPcm.sampleCtrl = (uint8_t)(GUS_SC_START | GUS_SC_IRQ | GUS_SC_INVERT | (channels > 1 ? GUS_SC_STEREO : 0) | (dma >= ISA_DMA_FIRST_16 ? GUS_SC_WIDE_CHAN : 0));
+ }
+
+ sPcm.recHalfBytes = REC_HALF_FRAMES * bits / BYTE_BITS * channels;
+
+ if (!isaDmaAlloc(&sPcm.dma, dma, sPcm.recHalfBytes * REC_HALVES)) {
+ dvxLog("Audio: gus: no conventional memory for the recording's DMA buffer");
+ gusClose();
+ return false;
+ }
+
+ sPcm.locked = gusLockPcm(true);
+
+ if (!sPcm.locked || !isaIrqHook(&sPcm.irq, sPcm.env.irq1, gusIsr)) {
+ gusClose();
+ return false;
+ }
+
+ sPcm.irqCount = 0;
+ sPcm.recording = true;
+
+ if (sPcm.recCodec) {
+ codecSetFormat(&sPcm.codec, sPcm.format);
+ codecInputSet(&sPcm.codec, sMaxInputs[source]);
+ isaDmaProgram(&sPcm.dma, 0, sPcm.dma.bytes, ISA_DMA_AUTO_RECORD);
+ codecStart(&sPcm.codec, REC_HALF_FRAMES, CODEC_CAPTURE);
+ } else {
+ gusInputs(source == AUDIO_REC_LINE, source == AUDIO_REC_MIC);
+ sampleStart(0);
+ }
+
+ uint64_t until = platformMicros() + FIRST_IRQ_WAIT_US;
+
+ while (sPcm.irqCount == 0 && platformMicros() < until) {
+ platformYield();
+ }
+
+ if (sPcm.irqCount == 0) {
+ dvxLog("Audio: gus: no interrupt from the card while recording on IRQ %lu, DMA %lu (check ULTRASND)", (unsigned long)sPcm.env.irq1, (unsigned long)dma);
+ gusClose();
+ return false;
+ }
+
+ got->rate = rate;
+ got->bits = bits;
+ got->channels = channels;
+ dvxLog("Audio: gus: recording through the %s, DMA %lu, %lu Hz", sPcm.recCodec ? "MAX's codec" : "GF1", (unsigned long)dma, (unsigned long)rate);
+ return true;
+}
+
+
+// The left voice crossed a half: its end and loop set for the half it
+// now plays, and the other one mixed (with interrupts on) and moved into
+// the card; once per half, however many interrupts come. Interrupt time.
+static void halfEnded(void) {
+ uint32_t mid = chanAddr(PCM_LEFT, PCM_HALF_FRAMES);
+ bool wide = sPcm.bits == BITS_16;
+ bool late = gusVoicePos(PCM_LEFT) >= (wide ? gusAddr16(mid) : mid);
+ int32_t free = late ? 0 : 1;
+
+ // In the second half: loop at the ring's end; in the first: roll over
+ // at the middle (the end moving back, its low word first)
+ if (late) {
+ voiceAddr(PCM_LEFT, GUS_V_END_HIGH, chanAddr(PCM_LEFT, PCM_RING - 1), false);
+ gusVoiceControl(PCM_LEFT, GUS_V_VOL_CONTROL, GUS_RAMP_STOPPED);
+ gusVoiceControl(PCM_LEFT, GUS_V_CONTROL, (uint8_t)(sPcm.voiceMode | GUS_VC_IRQ | GUS_VC_LOOP));
+ } else {
+ gusVoiceControl(PCM_LEFT, GUS_V_CONTROL, (uint8_t)(sPcm.voiceMode | GUS_VC_IRQ));
+ gusVoiceControl(PCM_LEFT, GUS_V_VOL_CONTROL, GUS_RAMP_STOPPED | GUS_VOL_ROLLOVER);
+ voiceAddr(PCM_LEFT, GUS_V_END_HIGH, mid, true);
+ }
+
+ if (free == sPcm.lastHalf) {
+ return;
+ }
+
+ sPcm.lastHalf = free;
+ __asm__ __volatile__ ("sti" : : : "memory");
+ audioMixRender(sPcm.mixed, PCM_HALF_FRAMES);
+ __asm__ __volatile__ ("cli" : : : "memory");
+
+ // Each channel apart into the staging buffer; the last half's last
+ // sample into the pad as well
+ for (uint32_t c = 0; c < sPcm.channels; c++) {
+ if (wide) {
+ for (uint32_t i = 0; i < PCM_HALF_FRAMES; i++) {
+ sPcm.chan[i] = sPcm.mixed[i * sPcm.channels + c];
+ }
+ } else {
+ const uint8_t *mixed8 = (const uint8_t *)sPcm.mixed;
+ uint8_t *chan8 = (uint8_t *)sPcm.chan;
+
+ for (uint32_t i = 0; i < PCM_HALF_FRAMES; i++) {
+ chan8[i] = mixed8[i * sPcm.channels + c];
+ }
+ }
+
+ isaDmaWrite(&sPcm.dma, c * sPcm.halfBytes, (const uint32_t *)sPcm.chan, sPcm.halfBytes / WORD_BYTES);
+
+ if (free == PCM_HALVES - 1) {
+ if (wide) {
+ gusPoke16(chanAddr(c, 0) - sPcm.sampleBytes, sPcm.chan[PCM_HALF_FRAMES - 1]);
+ } else {
+ gusPoke(chanAddr(c, 0) - sPcm.sampleBytes, (uint8_t)(((const uint8_t *)sPcm.chan)[PCM_HALF_FRAMES - 1] ^ SILENCE_8));
+ }
+ }
+ }
+
+ sPcm.pendingHalf = sPcm.channels > 1 ? free : HALF_NONE;
+ dmaStart(PCM_LEFT, free);
+}
+
+
+// A MAX's codec turned on (its control register) and found at base +
+// 10Ch; false (the GF1 records instead) when it does not answer.
+static bool maxCodecOn(void) {
+ gusMaxControlSet(gusMaxControl(sPcm.env.base, sPcm.env.dma1, sPcm.env.dma2));
+
+ if (codecFound(&sPcm.codec, (uint16_t)(sPcm.env.base + GUS_MAX_CODEC))) {
+ return true;
+ }
+
+ dvxLog("Audio: gus: the MAX's codec did not answer at %lXh; the GF1 records", (unsigned long)(sPcm.env.base + GUS_MAX_CODEC));
+ return false;
+}
+
+
+// The half just sampled handed on, sampling started again into the other
+// first (the GF1 does not run on by itself, so as little as possible goes
+// unrecorded in between). Interrupt time.
+static void sampleNext(void) {
+ uint32_t done = (uint32_t)sPcm.sampleHalf;
+
+ sampleStart((done + 1) % REC_HALVES);
+ __asm__ __volatile__ ("sti" : : : "memory");
+ gusCapture(done);
+ __asm__ __volatile__ ("cli" : : : "memory");
+}
+
+
+// The GF1 sampling into half (Linux gus_pcm.c's sequence): sampling off
+// and its interrupt cleared, the PC channel set for the half, the rate,
+// then started. Interrupt time as well.
+static void sampleStart(uint32_t half) {
+ gusWrite8(GUS_G_SAMPLE_CTRL, 0);
+ gusRead8(GUS_G_SAMPLE_CTRL);
+ isaDmaProgram(&sPcm.dma, half * sPcm.recHalfBytes, sPcm.recHalfBytes, ISA_DMA_ONCE_RECORD);
+ gusWrite8(GUS_G_SAMPLE_RATE, sPcm.sampleRate);
+ gusWrite8(GUS_G_SAMPLE_CTRL, sPcm.sampleCtrl);
+ sPcm.sampleHalf = (int32_t)half;
+}
+
+
+// A voice address (start, end, position) at a byte address, translated
+// for 16-bit voices. Interrupt time as well.
+static void voiceAddr(uint32_t voice, uint8_t reg, uint32_t addr, bool lowFirst) {
+ gusVoiceAddr(voice, reg, sPcm.bits == BITS_16 ? gusAddr16(addr) : addr, lowFirst);
+}
+
+
+// The left voice (and the right) set up and started together: the loop
+// from the pad, the left voice rolling over at the middle, the right
+// looping the whole ring.
+static void voicesStart(void) {
+ uint16_t fc = gusFc(sPcm.rate, gusHw.outRate);
+ bool stereo = sPcm.channels > 1;
+
+ for (uint32_t c = 0; c < sPcm.channels; c++) {
+ uint32_t v = c == 0 ? PCM_LEFT : PCM_RIGHT;
+
+ gusVoiceStop(v);
+ gusVoiceWrite16(v, GUS_V_FREQ, fc);
+ voiceAddr(v, GUS_V_START_HIGH, chanAddr(c, 0) - sPcm.sampleBytes, false);
+ voiceAddr(v, GUS_V_END_HIGH, c == 0 ? chanAddr(c, PCM_HALF_FRAMES) : chanAddr(c, PCM_RING - 1), false);
+ voiceAddr(v, GUS_V_POS_HIGH, chanAddr(c, 0), false);
+ gusVoiceWrite8(v, GUS_V_PAN, stereo ? (c == 0 ? GUS_PAN_LEFT : GUS_PAN_RIGHT) : GUS_PAN_CENTRE);
+ gusVoiceWrite16(v, GUS_V_VOLUME, (uint16_t)(PCM_VOLUME << GUS_VOL_SHIFT));
+ }
+
+ uint32_t flags = gusCli();
+
+ gusVoiceControl(PCM_LEFT, GUS_V_VOL_CONTROL, GUS_RAMP_STOPPED | GUS_VOL_ROLLOVER);
+ gusVoiceControl(PCM_LEFT, GUS_V_CONTROL, (uint8_t)(sPcm.voiceMode | GUS_VC_IRQ));
+
+ if (stereo) {
+ gusVoiceControl(PCM_RIGHT, GUS_V_CONTROL, (uint8_t)(sPcm.voiceMode | GUS_VC_LOOP));
+ }
+
+ gusRestore(flags);
+}
diff --git a/src/libs/kpunch/audio/drivers/guswt/gusWtDrv.c b/src/libs/kpunch/audio/drivers/guswt/gusWtDrv.c
new file mode 100644
index 00000000..54f75b01
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/guswt/gusWtDrv.c
@@ -0,0 +1,45 @@
+// 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.
+
+// gusWtDrv.c -- the Gravis UltraSound as a wavetable synthesizer alone
+// (guswt.drv)
+//
+// For a GUS beside another sound device (the common Sound Blaster 16 and
+// GUS machine: the Sound Blaster plays digital sound, the GUS music, with
+// all its voices and memory). When gus.drv plays digital sound on the
+// GUS, its own synthesizer comes first and this one is not opened. The
+// code is gus.drv's (gus/gusSynth.c).
+
+#include "audioDrv.h"
+#include "../../gus/gusSynth.h"
+
+AUDIO_SYNTH_DRIVER("guswt", AUDIO_PRIORITY_GUS,
+ .kind = AUDIO_SYNTH_WAVETABLE,
+ .open = gusSynthOpen,
+ .close = gusSynthClose,
+ .upload = gusSynthUpload,
+ .start = gusSynthStart,
+ .update = gusSynthUpdate,
+ .release = gusSynthRelease,
+ .stop = gusSynthStop,
+ .active = gusSynthActive
+);
diff --git a/src/libs/kpunch/audio/drivers/lpt/lptDrv.c b/src/libs/kpunch/audio/drivers/lpt/lptDrv.c
new file mode 100644
index 00000000..1e39faee
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/lpt/lptDrv.c
@@ -0,0 +1,344 @@
+// 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.
+
+// lptDrv.c -- digital sound on a DAC plugged into the parallel port
+// (lpt.drv)
+//
+// Covox Speech Thing an 8-bit DAC on the data lines: each sample is
+// written to the data port when it is due, so the
+// timer service interrupts at the rate (like the
+// speaker's digital sound) and writes one byte each
+// time. It has no way to be found: it is used only
+// when DVX.INI [audio] lptDac = covox.
+// Disney Sound Source a DAC behind a 16-sample FIFO that plays at a
+// fixed 7 kHz, powered by SELECT IN; a sample goes
+// in on SELECT IN's rising edge, ACK says the FIFO
+// is full. The timer service tops the FIFO up
+// LPT_DSS_TICK_HZ times a second until ACK says
+// full (few interrupts). Found by filling the FIFO
+// and watching it drain.
+// Stereo-on-1 two latched 8-bit DACs on the data lines, AUTOFEED
+// opening the left one's latch and STROBE the
+// right's: a frame is six port writes per interrupt
+// at the rate. Found by D7 wired back to BUSY.
+//
+// The port is DVX.INI [audio] lptPort (hex), else LPT1 as the BIOS
+// lists it. lptDac = auto (the default) looks for a Disney Sound Source,
+// then a Stereo-on-1, and gives up (so a printer on the port is left
+// alone: neither test strobes the port). The rate is the mixing rate
+// the configuration asks for, at most 11,048 Hz (LPT_DIVISOR_MIN; the
+// Disney's own 7 kHz there); samples are 8-bit unsigned. Feeding a
+// Covox or a Stereo-on-1 costs the CPU as the speaker's digital sound
+// does: an interrupt per frame.
+
+#include "audioDrv.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define INI_LPT_DAC "lptDac"
+#define INI_LPT_PORT "lptPort"
+#define LPT_DAC_AUTO "auto"
+#define LPT_DAC_COVOX "covox"
+#define LPT_DAC_DISNEY "disney"
+#define LPT_DAC_STEREO "stereo"
+#define PORT_BASE_HEX 16
+
+// The parallel port: data, status, control
+#define LPT_STATUS 1
+#define LPT_CONTROL 2
+#define STATUS_ACK 0x40 // Disney: the FIFO is full
+#define STATUS_BUSY 0x80 // Stereo-on-1: D7 inverted
+#define CTRL_STROBE 0x01 // inverted: set drives the pin low
+#define CTRL_AUTOFEED 0x02 // inverted
+#define CTRL_INIT 0x04 // not inverted: set holds a printer out of reset
+#define CTRL_SELECT_IN 0x08 // inverted
+#define BIOS_LPT1 0x0408 // the BIOS data area's LPT1 base
+#define SAMPLE_MIDDLE 0x80
+
+// Disney Sound Source: SELECT IN high (control bit clear) powers it and
+// clocks a sample in on its way up
+#define DSS_ON CTRL_INIT
+#define DSS_OFF (CTRL_INIT | CTRL_SELECT_IN)
+#define DSS_RATE 7000
+#define DSS_FIFO 16
+#define DSS_TICK_HZ 1000
+#define DSS_SETTLE_US 20000 // the FIFO drains in 2.3 ms; detection waits this
+#define DSS_DETECT_FILL 20 // samples sent to fill it
+
+// Stereo-on-1: a latch follows the data while its line is set
+#define STON_IDLE CTRL_INIT
+#define STON_LEFT (CTRL_INIT | CTRL_AUTOFEED)
+#define STON_RIGHT (CTRL_INIT | CTRL_STROBE)
+
+// Covox and Stereo-on-1: PIT clocks per frame, at least (an interrupt a
+// frame costs a 486 too much above about 11 kHz)
+#define LPT_DIVISOR_MIN 108 // 11,048 Hz
+#define LPT_BLOCK 8 // frames mixed at a time
+#define LPT_BITS 8
+#define LPT_TIMER_NONE (-1)
+
+typedef enum {
+ LptNoneE,
+ LptCovoxE,
+ LptDisneyE,
+ LptStereoE
+} LptDacE;
+
+typedef struct {
+ uint8_t buf[LPT_BLOCK * 2]; // frames mixed, waiting to be written
+ uint32_t pos; // next frame of buf
+ uint32_t channels;
+ uint32_t rate;
+ uint16_t base;
+ uint8_t control; // the control register as found
+ int32_t timerId;
+ LptDacE dac;
+ bool locked;
+} LptStateT;
+
+static LptStateT sLpt = { .timerId = LPT_TIMER_NONE };
+
+static const char *const sDacNames[] = { "", "Covox", "Disney Sound Source", "Stereo-on-1" };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void dssPut(uint16_t base, uint8_t sample);
+static void dssTick(void *unused);
+static void lptClose(void);
+static bool lptDetectDisney(uint16_t base);
+static bool lptDetectStereo(uint16_t base);
+static bool lptLock(bool lock);
+static bool lptOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
+static bool lptPlay(void);
+static void lptTick(void *unused);
+static void lptWait(uint32_t us);
+static uint8_t nextSample(void);
+
+AUDIO_DRIVER("lpt", AUDIO_PRIORITY_LPT,
+ .open = lptOpen,
+ .play = lptPlay,
+ .close = lptClose
+);
+
+
+// One sample into the Disney's FIFO: on the data lines, then SELECT IN
+// low and high again (the rising edge clocks it in). Interrupt time.
+static void dssPut(uint16_t base, uint8_t sample) {
+ outportb(base, sample);
+ outportb(base + LPT_CONTROL, DSS_OFF);
+ outportb(base + LPT_CONTROL, DSS_ON);
+}
+
+
+// Timer interrupt (Disney): the FIFO topped up until it says full.
+static void dssTick(void *unused) {
+ (void)unused;
+
+ for (int32_t i = 0; i < DSS_FIFO && !(inportb(sLpt.base + LPT_STATUS) & STATUS_ACK); i++) {
+ dssPut(sLpt.base, nextSample());
+ }
+}
+
+
+// Silent, the port's control lines as they were found (a Disney powered
+// off, as the BIOS leaves the port).
+static void lptClose(void) {
+ platformTimerRemove(sLpt.timerId);
+ sLpt.timerId = LPT_TIMER_NONE;
+ outportb(sLpt.base, SAMPLE_MIDDLE);
+ outportb(sLpt.base + LPT_CONTROL, sLpt.control);
+
+ if (sLpt.locked) {
+ lptLock(false);
+ sLpt.locked = false;
+ }
+}
+
+
+// A Disney powered on takes samples until its FIFO is full and plays them
+// out: not full at first, full after more samples than it holds, not
+// full again once they have played (a printer, or nothing, fails one of
+// the three). Left powered off.
+static bool lptDetectDisney(uint16_t base) {
+ bool found;
+
+ outportb(base + LPT_CONTROL, DSS_ON);
+ lptWait(DSS_SETTLE_US);
+ found = !(inportb(base + LPT_STATUS) & STATUS_ACK);
+
+ for (int32_t i = 0; found && i < DSS_DETECT_FILL; i++) {
+ dssPut(base, SAMPLE_MIDDLE);
+ }
+
+ found = found && (inportb(base + LPT_STATUS) & STATUS_ACK);
+ lptWait(DSS_SETTLE_US);
+ found = found && !(inportb(base + LPT_STATUS) & STATUS_ACK);
+ outportb(base + LPT_CONTROL, DSS_OFF);
+ return found;
+}
+
+
+// A Stereo-on-1 wires D7 back to BUSY (which reads inverted): BUSY follows
+// every pattern on the data lines. Nothing is strobed.
+static bool lptDetectStereo(uint16_t base) {
+ static const uint8_t patterns[] = { 0x00, 0x80, 0x7F, 0xFF, 0x00 };
+ bool found = true;
+
+ outportb(base + LPT_CONTROL, STON_IDLE);
+
+ for (uint32_t i = 0; found && i < sizeof(patterns); i++) {
+ outportb(base, patterns[i]);
+ found = ((inportb(base + LPT_STATUS) & STATUS_BUSY) != 0) == !(patterns[i] & STATUS_BUSY);
+ }
+
+ outportb(base, SAMPLE_MIDDLE);
+ return found;
+}
+
+
+static bool lptLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)dssPut, 0 },
+ { (const void *)dssTick, 0 },
+ { (const void *)lptTick, 0 },
+ { (const void *)nextSample, 0 },
+ { &sLpt, sizeof(sLpt) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+// The port (DVX.INI's, else LPT1), the DAC on it (DVX.INI's, else the one
+// found), the rate.
+static bool lptOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
+ const char *portText = prefsGetString(cfg->prefs, AUDIO_INI_SECTION, INI_LPT_PORT, "");
+ const char *want = prefsGetString(cfg->prefs, AUDIO_INI_SECTION, INI_LPT_DAC, LPT_DAC_AUTO);
+ uint32_t divisor;
+
+ sLpt.base = portText[0] ? (uint16_t)strtoul(portText, NULL, PORT_BASE_HEX) : _farpeekw(_dos_ds, BIOS_LPT1);
+ sLpt.dac = LptNoneE;
+
+ if (sLpt.base == 0) {
+ return false;
+ }
+
+ sLpt.control = inportb(sLpt.base + LPT_CONTROL);
+
+ if (strcasecmp(want, LPT_DAC_COVOX) == 0) {
+ sLpt.dac = LptCovoxE;
+ } else if (strcasecmp(want, LPT_DAC_DISNEY) == 0) {
+ sLpt.dac = LptDisneyE;
+ } else if (strcasecmp(want, LPT_DAC_STEREO) == 0) {
+ sLpt.dac = LptStereoE;
+ } else if (strcasecmp(want, LPT_DAC_AUTO) == 0) {
+ sLpt.dac = lptDetectDisney(sLpt.base) ? LptDisneyE : (lptDetectStereo(sLpt.base) ? LptStereoE : LptNoneE);
+ outportb(sLpt.base + LPT_CONTROL, sLpt.control);
+ }
+
+ if (sLpt.dac == LptNoneE) {
+ return false;
+ }
+
+ divisor = (PLATFORM_PIT_HZ + cfg->rate / 2) / cfg->rate;
+ divisor = divisor < LPT_DIVISOR_MIN ? LPT_DIVISOR_MIN : divisor;
+ sLpt.channels = sLpt.dac == LptStereoE ? 2 : 1;
+ sLpt.rate = sLpt.dac == LptDisneyE ? DSS_RATE : PLATFORM_PIT_HZ / divisor;
+ info->rate = sLpt.rate;
+ info->bits = LPT_BITS;
+ info->channels = sLpt.channels;
+ info->hasDac = true;
+ snprintf(info->description, sizeof(info->description), "%s at %Xh", sDacNames[sLpt.dac], (unsigned)sLpt.base);
+ return true;
+}
+
+
+// The Disney powered on and topped up from a slow timer; the others fed a
+// frame each interrupt at the rate.
+static bool lptPlay(void) {
+ sLpt.locked = lptLock(true);
+
+ if (!sLpt.locked) {
+ return false;
+ }
+
+ sLpt.pos = 0;
+
+ if (sLpt.dac == LptDisneyE) {
+ outportb(sLpt.base + LPT_CONTROL, DSS_ON);
+ sLpt.timerId = platformTimerAdd(DSS_TICK_HZ, dssTick, NULL, 0);
+ } else {
+ sLpt.timerId = platformTimerAddFast(sLpt.rate, lptTick, NULL, 0);
+ }
+
+ return sLpt.timerId >= 0;
+}
+
+
+// Timer interrupt (Covox, Stereo-on-1): the next frame on the DAC.
+static void lptTick(void *unused) {
+ uint16_t base = sLpt.base;
+
+ (void)unused;
+
+ if (sLpt.dac == LptCovoxE) {
+ outportb(base, nextSample());
+ return;
+ }
+
+ outportb(base + LPT_CONTROL, STON_LEFT);
+ outportb(base, nextSample());
+ outportb(base + LPT_CONTROL, STON_IDLE);
+ outportb(base + LPT_CONTROL, STON_RIGHT);
+ outportb(base, nextSample());
+ outportb(base + LPT_CONTROL, STON_IDLE);
+}
+
+
+// Program time: a wait on the timer service (detection only).
+static void lptWait(uint32_t us) {
+ uint64_t until = platformMicros() + us;
+
+ while (platformMicros() < until) {
+ }
+}
+
+
+// The next sample mixed (a block mixed when the last is used up).
+// Interrupt time.
+static uint8_t nextSample(void) {
+ if (sLpt.pos == 0) {
+ audioMixRender(sLpt.buf, LPT_BLOCK);
+ }
+
+ uint8_t s = sLpt.buf[sLpt.pos];
+
+ sLpt.pos = (sLpt.pos + 1) % (LPT_BLOCK * sLpt.channels);
+ return s;
+}
diff --git a/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c b/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
index ea416a23..14c65705 100644
--- a/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
+++ b/src/libs/kpunch/audio/drivers/mpu401/mpuDrv.c
@@ -25,24 +25,27 @@
// 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.
+// port go out on the MIDI cable as they are, and bytes arriving on its
+// MIDI IN (a keyboard) are read back. Looked for only when DVX.INI
+// [audio] midi or midiIn = 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).
+// may not answer that one). put first reads what the port has received
+// (it would hold the port up) into a small ring, then waits a little for
+// the port to be ready and otherwise returns false, so the caller sends
+// the byte later; get hands out the ring's bytes, then the port's. Both
+// run at interrupt time too and are pinned. Close sends All Notes Off on
+// every channel and resets the port (out of UART mode).
#include "audioDrv.h"
#include
#include
#include
+#include
#include
#define PORT_BASE_HEX 16
@@ -54,7 +57,8 @@
#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
+#define MPU_DRAIN_MAX 16 // input bytes put reads past (into the ring) at most
+#define MPU_IN_RING 64 // input put read, waiting for get (a power of two)
// Sent on close: All Notes Off (controller 123) on each channel
#define MIDI_CONTROL 0xB0
@@ -62,8 +66,11 @@
#define MIDI_ALL_NOTES_OFF 123
typedef struct {
- uint16_t data;
- uint16_t status;
+ uint8_t in[MPU_IN_RING]; // input put read past, for get
+ volatile uint32_t inHead; // bytes put stored
+ volatile uint32_t inTail; // bytes get took
+ uint16_t data;
+ uint16_t status;
} MpuT;
static MpuT sMpu;
@@ -74,6 +81,7 @@ static MpuT sMpu;
static void mpuClose(void);
static bool mpuCommand(uint8_t cmd);
+static bool mpuGet(uint8_t *byte);
static bool mpuLock(bool lock);
static bool mpuOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
static bool mpuPut(uint8_t byte);
@@ -83,7 +91,8 @@ AUDIO_SYNTH_DRIVER("mpu401", AUDIO_PRIORITY_CARD,
.kind = AUDIO_SYNTH_MIDI,
.open = mpuOpen,
.close = mpuClose,
- .put = mpuPut
+ .put = mpuPut,
+ .get = mpuGet
);
@@ -120,8 +129,26 @@ static bool mpuCommand(uint8_t cmd) {
}
+// What put read past first, then what the port has.
+static bool mpuGet(uint8_t *byte) {
+ if (sMpu.inTail != sMpu.inHead) {
+ *byte = sMpu.in[sMpu.inTail & (MPU_IN_RING - 1)];
+ sMpu.inTail++;
+ return true;
+ }
+
+ if (inportb(sMpu.status) & MPU_NO_DATA) {
+ return false;
+ }
+
+ *byte = inportb(sMpu.data);
+ return true;
+}
+
+
static bool mpuLock(bool lock) {
const AudioLockItemT items[] = {
+ { (const void *)mpuGet, 0 },
{ (const void *)mpuPut, 0 },
{ &sMpu, sizeof(sMpu) }
};
@@ -133,10 +160,15 @@ static bool mpuLock(bool 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) {
+ bool forMusic = strcasecmp(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO), AUDIO_MIDI_MPU401) == 0;
+ bool forInput = strcasecmp(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI_IN, AUDIO_MIDI_IN_NONE), AUDIO_MIDI_MPU401) == 0;
+
+ if (!forMusic && !forInput) {
return false;
}
+ memset(&sMpu, 0, sizeof(sMpu));
+
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);
@@ -156,11 +188,17 @@ static bool mpuOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) {
}
-// Drop what the port has sent, then send the byte if the port gets ready
-// soon enough.
+// Keep what the port has received for get (dropped when the ring is
+// full: nobody reads it), 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);
+ uint8_t in = inportb(sMpu.data);
+
+ if (sMpu.inHead - sMpu.inTail < MPU_IN_RING) {
+ sMpu.in[sMpu.inHead & (MPU_IN_RING - 1)] = in;
+ sMpu.inHead++;
+ }
}
for (int32_t i = 0; i < MPU_PUT_READS; i++) {
diff --git a/src/libs/kpunch/audio/drivers/opl/oplDrv.c b/src/libs/kpunch/audio/drivers/opl/oplDrv.c
index 945fd4c6..c56777f1 100644
--- a/src/libs/kpunch/audio/drivers/opl/oplDrv.c
+++ b/src/libs/kpunch/audio/drivers/opl/oplDrv.c
@@ -26,9 +26,10 @@
// 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).
+// flag must come up in the status register (and not at the slowest
+// count, which a Gravis UltraSound's AdLib timers ignore); 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
@@ -55,6 +56,7 @@
#define TIMER_IRQ_RESET 0x80 // clear the flags
#define TIMER1_START 0x21
#define TIMER1_FASTEST 0xFF
+#define TIMER1_SLOWEST 0x00 // 20 ms: never over in the wait
#define STATUS_FLAGS 0xE0
#define STATUS_IDLE 0x00
#define STATUS_TIMER1 0xC0
@@ -92,6 +94,8 @@ 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 uint8_t oplTimer1(uint16_t base, uint8_t count);
+static void oplTimersReset(uint16_t base);
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);
@@ -124,23 +128,18 @@ static void oplClose(void) {
// The AdLib test, with a register write that needs no timing yet (the
-// chip type is not known): generous waits.
+// chip type is not known): generous waits. Timer 1 must also keep the
+// count it is given: at its slowest it is still running when the
+// fastest has overflowed. A Gravis UltraSound answers at 388h with
+// timers that ignore it, and has no FM.
static bool oplDetect(uint16_t base, bool *opl3) {
- oplWriteBank(base, REG_TIMER_CTRL, TIMER_RESET);
- oplWriteBank(base, REG_TIMER_CTRL, TIMER_IRQ_RESET);
+ oplTimersReset(base);
uint8_t before = inportb(base);
+ uint8_t slow = oplTimer1(base, TIMER1_SLOWEST);
+ uint8_t fast = oplTimer1(base, TIMER1_FASTEST);
- 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) {
+ if ((before & STATUS_FLAGS) != STATUS_IDLE || (slow & STATUS_FLAGS) != STATUS_IDLE || (fast & STATUS_FLAGS) != STATUS_TIMER1) {
return false;
}
@@ -186,6 +185,27 @@ static bool oplOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) {
}
+// Timer 1 started at count and given the detection wait: the status
+// then, the timers stopped and their flags cleared after.
+static uint8_t oplTimer1(uint16_t base, uint8_t count) {
+ oplWriteBank(base, REG_TIMER1, count);
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER1_START);
+ oplWait(base, DETECT_WAIT_READS);
+
+ uint8_t status = inportb(base);
+
+ oplTimersReset(base);
+ return status;
+}
+
+
+// Both timers masked and their flags cleared.
+static void oplTimersReset(uint16_t base) {
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER_RESET);
+ oplWriteBank(base, REG_TIMER_CTRL, TIMER_IRQ_RESET);
+}
+
+
static void oplWait(uint16_t port, uint16_t reads) {
for (uint16_t i = 0; i < reads; i++) {
(void)inportb(port);
diff --git a/src/libs/kpunch/audio/drivers/sb/sbCalc.c b/src/libs/kpunch/audio/drivers/sb/sbCalc.c
index 7f9a1790..ba119467 100644
--- a/src/libs/kpunch/audio/drivers/sb/sbCalc.c
+++ b/src/libs/kpunch/audio/drivers/sb/sbCalc.c
@@ -43,6 +43,8 @@
#define RATE_HIGH_MAX 44100
#define RATE_STEREO_LOW 11025
#define RATE_STEREO_HIGH 22050
+#define REC_RATE_OLD_MAX 13000 // input before DSP 2.01, and its normal mode
+#define REC_RATE_201_MAX 15000 // DSP 2.01 input, high-speed above REC_RATE_OLD_MAX
#define BITS_8 8
#define BITS_16 16
#define CHANNELS_MONO 1
@@ -56,10 +58,10 @@
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);
+void sbRecordPlan(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) {
@@ -94,21 +96,6 @@ 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) {
- 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";
@@ -134,6 +121,7 @@ bool sbParseBlaster(const char *text, SbBlasterT *out) {
out->dma8 = SB_NONE;
out->dma16 = SB_NONE;
out->type = SB_NONE;
+ out->emu = SB_NONE;
const char *p = text;
@@ -166,6 +154,11 @@ bool sbParseBlaster(const char *text, SbBlasterT *out) {
out->type = parseNumber(&p, DEC_BASE);
break;
+ case 'E':
+ case 'e':
+ out->emu = parseNumber(&p, HEX_BASE);
+ break;
+
default:
while (*p && *p != ' ' && *p != '\t') {
p++;
@@ -216,3 +209,28 @@ void sbPlan(uint32_t dspVersion, uint32_t rate, uint32_t bits, uint32_t channels
plan->mode = SB_MODE_AUTO;
}
}
+
+
+void sbRecordPlan(uint32_t dspVersion, uint32_t rate, uint32_t bits, uint32_t channels, bool have16Dma, SbPlanT *plan) {
+ if (dspVersion >= SB_DSP_PRO) {
+ sbPlan(dspVersion, rate, bits, channels, have16Dma, plan);
+ return;
+ }
+
+ uint32_t top = dspVersion >= SB_DSP_HIGH_SPEED ? REC_RATE_201_MAX : REC_RATE_OLD_MAX;
+ uint32_t want = clampRate(rate, RATE_MIN, top);
+ uint32_t period = (TC_CLOCK + want / 2) / want;
+
+ plan->bits = BITS_8;
+ plan->channels = CHANNELS_MONO;
+ plan->timeConstant = TC_BASE - period;
+ plan->rate = TC_CLOCK / period;
+
+ if (dspVersion < SB_DSP_AUTO_INIT) {
+ plan->mode = SB_MODE_SINGLE;
+ } else if (plan->rate > REC_RATE_OLD_MAX) {
+ plan->mode = SB_MODE_HIGH;
+ } else {
+ plan->mode = SB_MODE_AUTO;
+ }
+}
diff --git a/src/libs/kpunch/audio/drivers/sb/sbCalc.h b/src/libs/kpunch/audio/drivers/sb/sbCalc.h
index e166a65e..3f70430e 100644
--- a/src/libs/kpunch/audio/drivers/sb/sbCalc.h
+++ b/src/libs/kpunch/audio/drivers/sb/sbCalc.h
@@ -23,9 +23,9 @@
// 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.
+// so the host tests run them: the BLASTER variable and the playback plan
+// for a DSP version (mode, achieved rate, time constant). Where the DMA
+// buffer goes is the shared ISA code's (audio/isa/isaCalc.h).
#ifndef SB_CALC_H
#define SB_CALC_H
@@ -42,10 +42,6 @@
// 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
@@ -54,14 +50,15 @@ typedef enum {
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.
+// The BLASTER environment variable ("A220 I5 D1 H5 E620 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
+ uint32_t emu; // E: an AWE32's EMU8000 base
} SbBlasterT;
// How a card plays what the mixer wants.
@@ -76,10 +73,6 @@ typedef struct {
// 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);
@@ -87,4 +80,11 @@ bool sbParseBlaster(const char *text, SbBlasterT *out);
// 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);
+// The recording plan: the same modes, recording (SB_MODE_SINGLE 24h,
+// SB_MODE_AUTO 2Ch, SB_MODE_HIGH 98h, SB_MODE_SB16 BEh / CEh), with the
+// cards' input limits: before the SB Pro, 8-bit mono up to 13,000 Hz
+// (15,000 on DSP 2.01, in high-speed mode above 13,000); the SB Pro and
+// the SB16 record what they play.
+void sbRecordPlan(uint32_t dspVersion, uint32_t rate, uint32_t bits, uint32_t channels, bool have16Dma, SbPlanT *plan);
+
#endif // SB_CALC_H
diff --git a/src/libs/kpunch/audio/drivers/sb/sbDrv.c b/src/libs/kpunch/audio/drivers/sb/sbDrv.c
index fe07c1e5..3d9b3c92 100644
--- a/src/libs/kpunch/audio/drivers/sb/sbDrv.c
+++ b/src/libs/kpunch/audio/drivers/sb/sbDrv.c
@@ -44,6 +44,14 @@
// programs are; the mixer is integer-only and pinned, like everything
// here the interrupt touches (sSb, the handler, audioMixRender).
//
+// Recording (recordStart / recordStop) takes the card the same way with
+// the input twins of the playback modes (sbRecordPlan): the DMA
+// controller writes the buffer, the interrupt hands the half just
+// filled to audioRecPush. The mixer picks the source (SB Pro 0Ch;
+// SB16 input switches 3Dh / 3Eh, the microphone at full); cards before
+// the Pro have no mixer and record the microphone. The registers it
+// changes are put back when it stops.
+//
// 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
@@ -51,14 +59,12 @@
#include "audioDrv.h"
#include "sbCalc.h"
+#include "../../isa/isaHw.h"
-#include
-#include
#include
#include
#include
#include
-#include
// DSP ports (offsets from the base)
#define DSP_MIXER_ADDR 0x04
@@ -73,13 +79,19 @@
// DSP commands
#define DSP_CMD_SINGLE8 0x14
+#define DSP_CMD_SINGLE8_IN 0x24
#define DSP_CMD_AUTO8 0x1C
+#define DSP_CMD_AUTO8_IN 0x2C
#define DSP_CMD_TIME_CONST 0x40
#define DSP_CMD_OUT_RATE 0x41
+#define DSP_CMD_IN_RATE 0x42
#define DSP_CMD_BLOCK_SIZE 0x48
#define DSP_CMD_HIGH_AUTO8 0x90
+#define DSP_CMD_HIGH_IN8 0x98
#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_SB16_16_IN 0xBE // 16-bit input, auto-init, FIFO on
+#define DSP_CMD_SB16_8_IN 0xCE // 8-bit input, auto-init, FIFO on
#define DSP_CMD_PAUSE8 0xD0
#define DSP_CMD_SPEAKER_ON 0xD1
#define DSP_CMD_SPEAKER_OFF 0xD3
@@ -92,11 +104,24 @@
// Mixer registers
#define MIX_REG_VOICE_PRO 0x04 // SB Pro voice volume (left and right nibbles)
+#define MIX_REG_INPUT_PRO 0x0C // SB Pro input: bits 2..1 the source, bit 5 the input filter off
+#define MIX_IN_PRO_MIC 0x00
+#define MIX_IN_PRO_CD 0x02
+#define MIX_IN_PRO_LINE 0x06
+#define MIX_IN_PRO_NO_FILT 0x20
#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_MIC16 0x3A // SB16 microphone volume, 5 bits in the top of the byte
+#define MIX_REG_IN16_L 0x3D // SB16 input switches, left and right
+#define MIX_REG_IN16_R 0x3E
+#define MIX_IN16_MIC 0x01
+#define MIX_IN16_CD_L 0x04
+#define MIX_IN16_CD_R 0x02
+#define MIX_IN16_LINE_L 0x10
+#define MIX_IN16_LINE_R 0x08
#define MIX_REG_IRQ 0x80
#define MIX_REG_DMA 0x81
#define MIX_REG_IRQ_STATUS 0x82
@@ -108,47 +133,14 @@
// 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
+// 22,050 Hz)
#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 IRQ_COUNT 16
#define SILENCE_8 0x80
#define SILENCE_16 0x00
@@ -162,11 +154,6 @@
// 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 };
@@ -178,113 +165,67 @@ static const uint8_t sSb16Irqs[SB16_IRQ_COUNT] = { 2, 5, 7, 10 };
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;
+ IsaDmaT dma; // the buffer and the channel in use
+ IsaIrqT irq;
SbPlanT plan;
uint32_t base;
- uint32_t irq;
- uint32_t dma; // the channel in use
+ uint32_t irqNumber;
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
+ uint8_t savedInput; // SB Pro mixer 0Ch, or SB16 3Dh
+ uint8_t savedInputR; // SB16 3Eh
+ uint8_t savedMic; // SB16 3Ah
+ uint32_t dma8; // the card's DMA channels (open)
+ uint32_t dma16;
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;
+ bool recording;
} SbStateT;
-static SbStateT sSb = { .dosSelector = -1 };
+static SbStateT sSb = { .dma.selector = -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 sbCapture(uint32_t half);
static void sbClose(void);
static bool sbFindCard(SbBlasterT *bl);
+static void sbHalt(void);
+static void sbInputRestore(void);
+static void sbInputSet(int32_t source, uint32_t channels);
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 bool sbRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got);
static void sbRefill(uint32_t half);
-static void sbStartDsp(void);
+static void sbRelease(void);
+static void sbStartDsp(bool input);
+static bool sbWaitFirstIrq(void);
AUDIO_DRIVER("sb", AUDIO_PRIORITY_CARD,
- .open = sbOpen,
- .play = sbPlay,
- .close = sbClose
+ .open = sbOpen,
+ .play = sbPlay,
+ .close = sbClose,
+ .recordStart = sbRecordStart,
+ .recordStop = 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) {
@@ -329,63 +270,6 @@ static bool dspWrite(uint32_t base, uint8_t value) {
}
-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);
@@ -398,28 +282,21 @@ static void mixerWrite(uint8_t reg, uint8_t value) {
}
-// Also runs from audio.lib's shutdown hook, so it logs nothing and copes
-// with any stage of sbOpen / sbPlay.
+// Hand the half just recorded to audio.lib. Interrupt time.
+static void sbCapture(uint32_t half) {
+ isaDmaRead(&sSb.dma, half * sSb.halfBytes, sSb.scratch, sSb.halfBytes / WORD_BYTES);
+ audioRecPush(sSb.scratch, sSb.halfBytes);
+}
+
+
+// Stops playing or recording (recordStop is this too). Also runs from
+// audio.lib's shutdown hook, so it logs nothing and copes with any stage
+// of sbOpen / sbPlay / sbRecordStart.
static void sbClose(void) {
- bool sb16 = sSb.dspVersion >= SB_DSP_SB16;
-
if (sSb.playing) {
- if (sb16) {
- bool wide = sSb.plan.bits == BITS_16;
+ sbHalt();
- 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) {
+ if (sSb.dspVersion >= SB_DSP_SB16) {
mixerWrite(MIX_REG_VOICE16_L, sSb.savedVoiceL);
mixerWrite(MIX_REG_VOICE16_R, sSb.savedVoiceR);
} else if (sSb.dspVersion >= SB_DSP_PRO) {
@@ -430,17 +307,13 @@ static void sbClose(void) {
sSb.playing = false;
}
- irqUnhook();
-
- if (sSb.locked) {
- sbLock(false);
- sSb.locked = false;
+ if (sSb.recording) {
+ sbHalt();
+ sbInputRestore();
+ sSb.recording = false;
}
- if (sSb.dosSelector >= 0) {
- __dpmi_free_dos_memory(sSb.dosSelector);
- sSb.dosSelector = -1;
- }
+ sbRelease();
}
@@ -461,6 +334,65 @@ static bool sbFindCard(SbBlasterT *bl) {
}
+// Stop the transfer running, out or in, at the DSP (a reset stops every
+// mode, high-speed and single-cycle included) and the DMA controller.
+static void sbHalt(void) {
+ if (sSb.dspVersion >= SB_DSP_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);
+ }
+
+ dspReset(sSb.base);
+ dspWrite(sSb.base, DSP_CMD_SPEAKER_OFF);
+ isaDmaStop(&sSb.dma);
+}
+
+
+// The mixer registers sbInputSet changed, back as they were.
+static void sbInputRestore(void) {
+ if (sSb.dspVersion >= SB_DSP_SB16) {
+ mixerWrite(MIX_REG_IN16_L, sSb.savedInput);
+ mixerWrite(MIX_REG_IN16_R, sSb.savedInputR);
+ mixerWrite(MIX_REG_MIC16, sSb.savedMic);
+ } else if (sSb.dspVersion >= SB_DSP_PRO) {
+ mixerWrite(MIX_REG_INPUT_PRO, sSb.savedInput);
+ mixerWrite(MIX_REG_OUTPUT, sSb.savedOutput);
+ }
+}
+
+
+// The mixer records from source (AUDIO_REC_*): an SB16's input switches
+// and its microphone at full; an SB Pro's input select (the input filter
+// off) and its stereo bit. Cards before the Pro have no mixer: the
+// microphone.
+static void sbInputSet(int32_t source, uint32_t channels) {
+ static const uint8_t in16L[AUDIO_REC_SOURCES] = { MIX_IN16_MIC, MIX_IN16_LINE_L, MIX_IN16_CD_L };
+ static const uint8_t in16R[AUDIO_REC_SOURCES] = { MIX_IN16_MIC, MIX_IN16_LINE_R, MIX_IN16_CD_R };
+ static const uint8_t inPro[AUDIO_REC_SOURCES] = { MIX_IN_PRO_MIC, MIX_IN_PRO_LINE, MIX_IN_PRO_CD };
+
+ if (sSb.dspVersion >= SB_DSP_SB16) {
+ sSb.savedInput = mixerRead(MIX_REG_IN16_L);
+ sSb.savedInputR = mixerRead(MIX_REG_IN16_R);
+ sSb.savedMic = mixerRead(MIX_REG_MIC16);
+ mixerWrite(MIX_REG_IN16_L, in16L[source]);
+ mixerWrite(MIX_REG_IN16_R, in16R[source]);
+ mixerWrite(MIX_REG_MIC16, MIX_VOICE16_MAX);
+ } else if (sSb.dspVersion >= SB_DSP_PRO) {
+ uint8_t output = mixerRead(MIX_REG_OUTPUT);
+
+ sSb.savedInput = mixerRead(MIX_REG_INPUT_PRO);
+ sSb.savedOutput = output;
+ mixerWrite(MIX_REG_INPUT_PRO, (uint8_t)(inPro[source] | MIX_IN_PRO_NO_FILT));
+ mixerWrite(MIX_REG_OUTPUT, (uint8_t)(channels > 1 ? (output | MIX_OUT_STEREO) : (output & ~MIX_OUT_STEREO)));
+ }
+}
+
+
// 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
@@ -470,12 +402,8 @@ 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 (isaIrqSpurious(&sSb.irq)) {
+ return;
}
if (sSb.dspVersion >= SB_DSP_SB16) {
@@ -498,41 +426,49 @@ static void sbIsr(void) {
if (mine && sSb.priming) {
sSb.primed = true;
- } else if (mine && sSb.playing) {
+ } else if (mine && (sSb.playing || sSb.recording)) {
sSb.irqCount++;
if (sSb.plan.mode == SB_MODE_SINGLE) {
uint32_t len = sSb.halfBytes - 1;
- dspWrite(base, DSP_CMD_SINGLE8);
+ dspWrite(base, sSb.recording ? DSP_CMD_SINGLE8_IN : DSP_CMD_SINGLE8);
dspWrite(base, (uint8_t)(len & BYTE_MASK));
dspWrite(base, (uint8_t)((len >> BYTE_BITS) & BYTE_MASK));
}
+ // The half the DMA controller is not in now is the one done.
+ uint32_t half = isaDmaPosition(&sSb.dma) < sSb.halfBytes ? 1 : 0;
+
__asm__ __volatile__ ("sti" : : : "memory");
- sbRefill(1 - dmaHalfPlaying());
+
+ if (sSb.recording) {
+ sbCapture(half);
+ } else {
+ sbRefill(half);
+ }
+
__asm__ __volatile__ ("cli" : : : "memory");
}
- if (sSb.irq >= IRQ_SLAVE_FIRST) {
- outportb(PIC2_CMD, PIC_EOI);
- }
-
- outportb(PIC1_CMD, PIC_EOI);
+ isaIrqEnd(&sSb.irq);
}
+// Pins this file's interrupt-time code and state, and the shared ISA
+// code's.
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) }
+ { (const void *)dspWrite, 0 },
+ { (const void *)sbCapture, 0 },
+ { (const void *)sbIsr, 0 },
+ { (const void *)sbRefill, 0 },
+ { &sSb, sizeof(sSb) }
};
- return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+ bool ok = audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+
+ return isaLock(lock) && ok;
}
@@ -540,7 +476,7 @@ static bool sbOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
SbBlasterT bl;
memset(&sSb, 0, sizeof(sSb));
- sSb.dosSelector = -1;
+ sSb.dma.selector = -1;
if (!sbFindCard(&bl)) {
dvxLog("Audio: sb: no Sound Blaster found (BLASTER %s)", getenv("BLASTER") ? "set" : "not set");
@@ -570,7 +506,7 @@ static bool sbOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
}
}
- for (uint32_t ch = 0; ch < DMA_CHANNELS; ch++) {
+ for (uint32_t ch = 0; ch < ISA_DMA_CHANNELS; ch++) {
if (dmaBits & (1 << ch)) {
if (ch >= SB16_DMA_16_FIRST) {
bl.dma16 = ch;
@@ -581,44 +517,29 @@ static bool sbOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
}
}
- sSb.irq = bl.irq == IRQ_CASCADE ? IRQ_AT_REDIRECT : bl.irq;
+ sSb.irqNumber = bl.irq;
+ sSb.dma8 = bl.dma8;
+ sSb.dma16 = bl.dma16;
- bool have16 = sSb.dspVersion >= SB_DSP_SB16 && bl.dma16 != SB_NONE && bl.dma16 >= SB16_DMA_16_FIRST && bl.dma16 < DMA_CHANNELS;
+ bool have16 = sSb.dspVersion >= SB_DSP_SB16 && bl.dma16 != SB_NONE && bl.dma16 >= SB16_DMA_16_FIRST && bl.dma16 < ISA_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;
+ uint32_t dma = sSb.plan.bits == BITS_16 ? bl.dma16 : bl.dma8;
+ bool dmaOk = sSb.plan.bits == BITS_16 ? (dma >= SB16_DMA_16_FIRST && dma < ISA_DMA_CHANNELS) : dma < ISA_DMA_FIRST_16;
- if (sSb.irq >= IRQ_COUNT || !dmaOk) {
+ if (sSb.irqNumber >= 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) {
+ if (!isaDmaAlloc(&sSb.dma, dma, SB_BLOCK_FRAMES * frameBytes * SB_HALVES)) {
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;
@@ -626,8 +547,9 @@ static bool sbOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
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);
+ info->hasDac = true;
+ info->recSources = sSb.dspVersion >= SB_DSP_PRO ? (1U << AUDIO_REC_MIC) | (1U << AUDIO_REC_LINE) | (1U << AUDIO_REC_CD) : (1U << AUDIO_REC_MIC);
+ 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.irqNumber, (unsigned long)sSb.dma.channel, (int)sSb.plan.mode);
return true;
}
@@ -641,15 +563,9 @@ static bool sbPlay(void) {
return false;
}
- _farsetsel((unsigned short)sSb.dosSelector);
+ isaDmaFill(&sSb.dma, silence);
- for (uint32_t i = 0; i < sSb.halfBytes * SB_HALVES; i++) {
- _farnspokeb(sSb.bufOffset + i, silence);
- }
-
- irqHook();
-
- if (!sSb.hooked) {
+ if (!isaIrqHook(&sSb.irq, sSb.irqNumber, sbIsr)) {
return false;
}
@@ -675,32 +591,16 @@ static bool sbPlay(void) {
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;
+ isaDmaProgram(&sSb.dma, 0, sSb.dma.bytes, ISA_DMA_AUTO_PLAY);
+ sbStartDsp(false);
+ return sbWaitFirstIrq();
}
-// SB Pro stereo: play one silent byte and wait for its interrupt, so the
-// real transfer starts on the left channel.
+// SB Pro stereo: play one silent byte (the buffer is silent yet) 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);
+ isaDmaProgram(&sSb.dma, 0, 1, ISA_DMA_ONCE_PLAY);
sSb.primed = false;
sSb.priming = true;
dspWrite(sSb.base, DSP_CMD_TIME_CONST);
@@ -716,27 +616,84 @@ static void sbPrimeStereo(void) {
}
sSb.priming = false;
- dmaStop();
+ isaDmaStop(&sSb.dma);
+}
+
+
+// Take the card for recording (the output is closed): the plan for the
+// format asked, a DMA buffer on the channel its bits need, the mixer set
+// to source, and the transfer started; got is what the card records.
+static bool sbRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got) {
+ bool have16 = sSb.dspVersion >= SB_DSP_SB16 && sSb.dma16 >= SB16_DMA_16_FIRST && sSb.dma16 < ISA_DMA_CHANNELS;
+
+ sbRecordPlan(sSb.dspVersion, want->rate, want->bits, want->channels, have16, &sSb.plan);
+
+ uint32_t dma = sSb.plan.bits == BITS_16 ? sSb.dma16 : sSb.dma8;
+ uint32_t frameBytes = sSb.plan.bits / BYTE_BITS * sSb.plan.channels;
+
+ if (sSb.plan.bits != BITS_16 && dma >= ISA_DMA_FIRST_16) {
+ dvxLog("Audio: sb: no 8-bit DMA channel to record on (set BLASTER D)");
+ return false;
+ }
+
+ if (!isaDmaAlloc(&sSb.dma, dma, SB_BLOCK_FRAMES * frameBytes * SB_HALVES)) {
+ dvxLog("Audio: sb: no conventional memory for the recording's DMA buffer");
+ return false;
+ }
+
+ sSb.halfFrames = SB_BLOCK_FRAMES;
+ sSb.halfBytes = SB_BLOCK_FRAMES * frameBytes;
+ sSb.locked = sbLock(true);
+
+ if (!sSb.locked || !isaIrqHook(&sSb.irq, sSb.irqNumber, sbIsr)) {
+ sbRelease();
+ return false;
+ }
+
+ dspWrite(sSb.base, DSP_CMD_SPEAKER_OFF);
+ sbInputSet(source, sSb.plan.channels);
+ sSb.irqCount = 0;
+ sSb.recording = true;
+ isaDmaProgram(&sSb.dma, 0, sSb.dma.bytes, ISA_DMA_AUTO_RECORD);
+ sbStartDsp(true);
+
+ if (!sbWaitFirstIrq()) {
+ sbClose();
+ return false;
+ }
+
+ got->rate = sSb.plan.rate;
+ got->bits = sSb.plan.bits;
+ got->channels = sSb.plan.channels;
+ dvxLog("Audio: sb: recording, mode %d, DMA %lu", (int)sSb.plan.mode, (unsigned long)sSb.dma.channel);
+ return true;
}
// 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]);
- }
+ isaDmaWrite(&sSb.dma, half * sSb.halfBytes, sSb.scratch, sSb.halfBytes / WORD_BYTES);
}
-// Start the transfer the plan calls for; the DSP interrupts after each
-// half.
-static void sbStartDsp(void) {
+// Unhook the interrupt, unpin, and free the DMA buffer (after sbHalt).
+static void sbRelease(void) {
+ isaIrqUnhook(&sSb.irq);
+
+ if (sSb.locked) {
+ sbLock(false);
+ sSb.locked = false;
+ }
+
+ isaDmaFree(&sSb.dma);
+}
+
+
+// Start the transfer the plan calls for, playing or recording (input);
+// the DSP interrupts after each half.
+static void sbStartDsp(bool input) {
uint32_t base = sSb.base;
if (sSb.plan.mode == SB_MODE_SB16) {
@@ -744,10 +701,16 @@ static void sbStartDsp(void) {
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, input ? DSP_CMD_IN_RATE : 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);
+
+ if (input) {
+ dspWrite(base, wide ? DSP_CMD_SB16_16_IN : DSP_CMD_SB16_8_IN);
+ } else {
+ 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));
@@ -760,7 +723,7 @@ static void sbStartDsp(void) {
dspWrite(base, (uint8_t)sSb.plan.timeConstant);
if (sSb.plan.mode == SB_MODE_SINGLE) {
- dspWrite(base, DSP_CMD_SINGLE8);
+ dspWrite(base, input ? DSP_CMD_SINGLE8_IN : DSP_CMD_SINGLE8);
dspWrite(base, (uint8_t)(len & BYTE_MASK));
dspWrite(base, (uint8_t)((len >> BYTE_BITS) & BYTE_MASK));
return;
@@ -769,5 +732,28 @@ static void sbStartDsp(void) {
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);
+
+ if (input) {
+ dspWrite(base, sSb.plan.mode == SB_MODE_HIGH ? DSP_CMD_HIGH_IN8 : DSP_CMD_AUTO8_IN);
+ } else {
+ dspWrite(base, sSb.plan.mode == SB_MODE_HIGH ? DSP_CMD_HIGH_AUTO8 : DSP_CMD_AUTO8);
+ }
+}
+
+
+// A card set to another IRQ than BLASTER says moves one buffer and stops;
+// give up on it (the next driver is tried, or the recording refused).
+static bool sbWaitFirstIrq(void) {
+ 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.irqNumber);
+ return false;
+ }
+
+ return true;
}
diff --git a/src/libs/kpunch/audio/drivers/wss/wssCalc.c b/src/libs/kpunch/audio/drivers/wss/wssCalc.c
new file mode 100644
index 00000000..40f9e5e6
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/wss/wssCalc.c
@@ -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.
+
+// wssCalc.c -- Windows Sound System driver arithmetic (see wssCalc.h)
+//
+// The board's configuration register (Microsoft's layout, as OPTi's data
+// sheet prints it) holds the IRQ in bits 5:3 and the playback DMA
+// channel in bits 2:0.
+
+#include "wssCalc.h"
+
+#define CONFIG_IRQ_SHIFT 3
+#define NONE 0 // a code that routes nothing
+
+// The configuration codes for IRQ 7, 9, 10, 11 and DMA 0, 1, 3
+static const uint8_t sIrqs[] = { 7, 9, 10, 11 };
+static const uint8_t sIrqCodes[] = { 1, 2, 3, 4 };
+static const uint8_t sDmas[] = { 0, 1, 3 };
+static const uint8_t sDmaCodes[] = { 1, 2, 3 };
+
+#define IRQ_CHOICES ((int32_t)(sizeof(sIrqs) / sizeof(sIrqs[0])))
+#define DMA_CHOICES ((int32_t)(sizeof(sDmas) / sizeof(sDmas[0])))
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool wssConfig(uint32_t irq, uint32_t dma, uint8_t *value);
+
+
+bool wssConfig(uint32_t irq, uint32_t dma, uint8_t *value) {
+ uint8_t irqCode = NONE;
+ uint8_t dmaCode = NONE;
+
+ for (int32_t i = 0; i < IRQ_CHOICES; i++) {
+ irqCode = sIrqs[i] == irq ? sIrqCodes[i] : irqCode;
+ }
+
+ for (int32_t i = 0; i < DMA_CHOICES; i++) {
+ dmaCode = sDmas[i] == dma ? sDmaCodes[i] : dmaCode;
+ }
+
+ *value = (uint8_t)(irqCode << CONFIG_IRQ_SHIFT | dmaCode);
+ return irqCode != NONE && dmaCode != NONE;
+}
diff --git a/src/libs/kpunch/audio/drivers/wss/wssCalc.h b/src/libs/kpunch/audio/drivers/wss/wssCalc.h
new file mode 100644
index 00000000..8bad81e9
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/wss/wssCalc.h
@@ -0,0 +1,41 @@
+// 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.
+
+// wssCalc.h -- Windows Sound System driver arithmetic (no hardware
+// access)
+//
+// The part of wss.drv that decides rather than touches hardware, kept
+// apart so the host tests run it: the board's configuration register
+// value for an IRQ and a DMA channel. The codec's own arithmetic (its
+// rates and format byte) is codec/codecCalc.c's.
+#ifndef WSS_CALC_H
+#define WSS_CALC_H
+
+#include
+#include
+
+// The configuration register value (written at the board's base) that
+// routes the codec to irq (7, 9, 10 or 11) and DMA channel dma (0, 1 or
+// 3); false for one the board cannot use.
+bool wssConfig(uint32_t irq, uint32_t dma, uint8_t *value);
+
+#endif // WSS_CALC_H
diff --git a/src/libs/kpunch/audio/drivers/wss/wssDrv.c b/src/libs/kpunch/audio/drivers/wss/wssDrv.c
new file mode 100644
index 00000000..15196739
--- /dev/null
+++ b/src/libs/kpunch/audio/drivers/wss/wssDrv.c
@@ -0,0 +1,385 @@
+// 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.
+
+// wssDrv.c -- the Windows Sound System sound driver (wss.drv)
+//
+// For boards built on the AD1848 codec and its kin (Microsoft's own,
+// the Crystal CS4231 / CS4248 ones, Aztech, OPTi and others in their WSS
+// mode, which play 16-bit stereo at up to 48 kHz). The board sits at
+// 530h, 604h, E80h or F40h (DVX.INI [audio] wssPort, hex, to name one):
+// its configuration register at base..base+3, the codec at base+4..+7.
+// A codec is there when its index registers keep what is written to
+// them. The configuration register routes the codec to an IRQ and an
+// 8-bit DMA channel (DVX.INI wssIrq: 7, 9, 10 or 11, default 7; wssDma:
+// 0, 1 or 3, default 1); a codec without one (a Plug and Play board set
+// up by its own utility) must be on the ones DVX.INI names. Either way
+// a board that does not interrupt on that IRQ is given up, so the next
+// driver is tried.
+//
+// The codec itself is codec/codecHw.c's (shared with the UltraSound
+// MAX): the rate is its nearest to the one asked for, 8- or 16-bit
+// (16-bit data still moves over an 8-bit DMA channel, a byte at a time),
+// mono or stereo, the format set in a mode change with its calibration,
+// the DAC unmuted at full (DVX's own volumes do the rest) and put back
+// when it stops. Output is double-buffered as sb.drv's is: one auto-init
+// DMA transfer over a buffer of two halves, the codec's sample counter
+// interrupting at the end of each, the interrupt mixing the half just
+// played (audio.lib's mixer, inside the interrupt with interrupts enabled
+// again but the card's not yet acknowledged at the PIC).
+//
+// Recording (recordStart / recordStop) runs the codec's capture the same
+// way over the same DMA channel, its interrupt handing each half
+// recorded to audio.lib. The input is the ADC's source: the line input,
+// AUX1 (the CD on WSS boards), or the microphone with its 20 dB boost;
+// the inputs it found are put back when it stops.
+
+#include "audioDrv.h"
+#include "wssCalc.h"
+#include "../../codec/codecHw.h"
+#include "../../isa/isaHw.h"
+
+#include
+#include
+#include
+#include
+
+#define INI_WSS_PORT "wssPort"
+#define INI_WSS_IRQ "wssIrq"
+#define INI_WSS_DMA "wssDma"
+#define WSS_IRQ_DEFAULT 7
+#define WSS_DMA_DEFAULT 1
+#define PORT_BASE_HEX 16
+
+// The board: its version register (read) and configuration register
+// (write) at base+0..3, the codec from base+4
+#define BOARD_VERSION 3
+#define BOARD_ID_MASK 0x3F
+#define CODEC_OFFSET 4
+
+// Buffer: two halves of WSS_BLOCK_FRAMES frames
+#define WSS_BLOCK_FRAMES 512
+#define WSS_FRAME_BYTES_MAX 4
+#define WSS_HALVES 2
+#define WORD_BYTES 4
+#define BYTE_BITS 8
+#define BITS_8 8
+#define BITS_16 16
+#define CHANNELS_MAX 2
+#define SILENCE_8 0x80
+#define SILENCE_16 0x00
+#define FIRST_IRQ_WAIT_US 500000 // many blocks: no interrupt by then means a wrong IRQ
+
+// Board bases probed without wssPort
+static const uint16_t sBases[] = { 0x530, 0x604, 0xE80, 0xF40 };
+
+// Version register values of WSS boards (the low six bits)
+static const uint8_t sBoardIds[] = { 0x04, 0x00, 0x0F };
+
+// The ADC's input for each recording source
+static const uint8_t sInputs[] = {
+ [AUDIO_REC_MIC] = CODEC_INPUT_MIC | CODEC_INPUT_MIC_BOOST,
+ [AUDIO_REC_LINE] = CODEC_INPUT_LINE,
+ [AUDIO_REC_CD] = CODEC_INPUT_AUX1
+};
+
+typedef struct {
+ uint32_t scratch[WSS_BLOCK_FRAMES * WSS_FRAME_BYTES_MAX / WORD_BYTES]; // one half, mixed here first
+ CodecT codec;
+ IsaDmaT dma;
+ IsaIrqT irq;
+ uint32_t irqNumber;
+ uint32_t halfBytes;
+ uint16_t base;
+ uint8_t format; // I8
+ volatile uint32_t irqCount;
+ bool locked;
+ bool playing;
+ bool recording;
+} WssStateT;
+
+static WssStateT sWss = { .dma.selector = -1 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool hasConfig(uint16_t base);
+static void wssCapture(uint32_t half);
+static void wssClose(void);
+static void wssIsr(void);
+static bool wssLock(bool lock);
+static bool wssOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
+static bool wssPlay(void);
+static bool wssRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got);
+static void wssRefill(uint32_t half);
+static bool wssStart(uint8_t what);
+
+AUDIO_DRIVER("wss", AUDIO_PRIORITY_CODEC,
+ .open = wssOpen,
+ .play = wssPlay,
+ .close = wssClose,
+ .recordStart = wssRecordStart,
+ .recordStop = wssClose
+);
+
+
+// The board has Microsoft's configuration register (its version register
+// reads one of the WSS ids).
+static bool hasConfig(uint16_t base) {
+ uint8_t id = inportb(base + BOARD_VERSION) & BOARD_ID_MASK;
+
+ for (uint32_t i = 0; i < sizeof(sBoardIds); i++) {
+ if (id == sBoardIds[i]) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+// Hand the half just recorded to audio.lib. Interrupt time.
+static void wssCapture(uint32_t half) {
+ isaDmaRead(&sWss.dma, half * sWss.halfBytes, sWss.scratch, sWss.halfBytes / WORD_BYTES);
+ audioRecPush(sWss.scratch, sWss.halfBytes);
+}
+
+
+// Stops playing or recording (recordStop is this too), the codec putting
+// back the levels or the inputs it found. Also runs from audio.lib's
+// shutdown hook, so it logs nothing and copes with any stage of wssOpen,
+// wssPlay, and wssRecordStart.
+static void wssClose(void) {
+ if (sWss.playing || sWss.recording) {
+ codecStop(&sWss.codec);
+ isaDmaStop(&sWss.dma);
+ sWss.playing = false;
+ sWss.recording = false;
+ }
+
+ isaIrqUnhook(&sWss.irq);
+
+ if (sWss.locked) {
+ wssLock(false);
+ sWss.locked = false;
+ }
+
+ isaDmaFree(&sWss.dma);
+}
+
+
+// The codec's interrupt: its sample counter ran out at the end of a half.
+// Acknowledged at the codec, the half just played mixed (or the half just
+// recorded handed on) with interrupts enabled, and only then acknowledged
+// at the PIC. A spurious IRQ 7 gets no acknowledgement at all.
+static void wssIsr(void) {
+ if (isaIrqSpurious(&sWss.irq)) {
+ return;
+ }
+
+ if (codecIrq(&sWss.codec) && (sWss.playing || sWss.recording)) {
+ uint32_t half = isaDmaPosition(&sWss.dma) < sWss.halfBytes ? 1 : 0;
+
+ sWss.irqCount++;
+ __asm__ __volatile__ ("sti" : : : "memory");
+
+ if (sWss.recording) {
+ wssCapture(half);
+ } else {
+ wssRefill(half);
+ }
+
+ __asm__ __volatile__ ("cli" : : : "memory");
+ }
+
+ isaIrqEnd(&sWss.irq);
+}
+
+
+// This file's interrupt-time code and state, and the shared codec and ISA
+// code's.
+static bool wssLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)wssCapture, 0 },
+ { (const void *)wssIsr, 0 },
+ { (const void *)wssRefill, 0 },
+ { &sWss, sizeof(sWss) }
+ };
+
+ bool ok = audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+
+ ok = codecLock(lock) && ok;
+ return isaLock(lock) && ok;
+}
+
+
+// The board (DVX.INI's, else the usual bases), routed to the IRQ and DMA
+// channel; the format and the DMA buffer.
+static bool wssOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
+ const char *portText = prefsGetString(cfg->prefs, AUDIO_INI_SECTION, INI_WSS_PORT, "");
+ uint32_t dma = (uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, INI_WSS_DMA, WSS_DMA_DEFAULT);
+ uint32_t bits = cfg->bits ? cfg->bits : BITS_16;
+ uint32_t channels = cfg->channels ? cfg->channels : CHANNELS_MAX;
+ uint32_t rate;
+ uint8_t route;
+ bool found = false;
+
+ memset(&sWss, 0, sizeof(sWss));
+ sWss.dma.selector = -1;
+ sWss.irqNumber = (uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, INI_WSS_IRQ, WSS_IRQ_DEFAULT);
+
+ uint16_t named = portText[0] ? (uint16_t)strtoul(portText, NULL, PORT_BASE_HEX) : 0;
+ uint32_t tries = named ? 1 : (uint32_t)(sizeof(sBases) / sizeof(sBases[0]));
+
+ for (uint32_t i = 0; !found && i < tries; i++) {
+ sWss.base = named ? named : sBases[i];
+ found = codecFound(&sWss.codec, (uint16_t)(sWss.base + CODEC_OFFSET));
+ }
+
+ if (!found) {
+ return false;
+ }
+
+ if (!wssConfig(sWss.irqNumber, dma, &route)) {
+ dvxLog("Audio: wss: IRQ %lu / DMA %lu cannot be used (wssIrq 7, 9, 10 or 11; wssDma 0, 1 or 3)", (unsigned long)sWss.irqNumber, (unsigned long)dma);
+ return false;
+ }
+
+ if (hasConfig(sWss.base)) {
+ outportb(sWss.base, route);
+ }
+
+ sWss.format = (uint8_t)(codecFormat(bits, channels) | codecRateCode(cfg->rate, &rate));
+
+ uint32_t frameBytes = bits / BYTE_BITS * channels;
+
+ if (!isaDmaAlloc(&sWss.dma, dma, WSS_BLOCK_FRAMES * frameBytes * WSS_HALVES)) {
+ dvxLog("Audio: wss: no conventional memory for the DMA buffer");
+ return false;
+ }
+
+ sWss.halfBytes = WSS_BLOCK_FRAMES * frameBytes;
+ snprintf(info->description, sizeof(info->description), "Windows Sound System (%s codec) at %Xh", codecIsCrystal(&sWss.codec) ? "Crystal" : "AD1848", (unsigned)sWss.base);
+ info->rate = rate;
+ info->bits = bits;
+ info->channels = channels;
+ info->hasDac = true;
+ info->recSources = (1U << AUDIO_REC_MIC) | (1U << AUDIO_REC_LINE) | (1U << AUDIO_REC_CD);
+ dvxLog("Audio: wss: codec at %Xh, IRQ %lu, DMA %lu, %lu Hz", (unsigned)sWss.codec.port, (unsigned long)sWss.irqNumber, (unsigned long)dma, (unsigned long)rate);
+ return true;
+}
+
+
+static bool wssPlay(void) {
+ sWss.locked = wssLock(true);
+
+ if (!sWss.locked) {
+ return false;
+ }
+
+ isaDmaFill(&sWss.dma, (sWss.format & CODEC_FMT_16BIT) ? SILENCE_16 : SILENCE_8);
+
+ if (!isaIrqHook(&sWss.irq, sWss.irqNumber, wssIsr)) {
+ return false;
+ }
+
+ codecSetFormat(&sWss.codec, sWss.format);
+ codecDacOn(&sWss.codec);
+ sWss.playing = true;
+ wssRefill(0);
+ wssRefill(1);
+ isaDmaProgram(&sWss.dma, 0, sWss.dma.bytes, ISA_DMA_AUTO_PLAY);
+ return wssStart(CODEC_PLAY);
+}
+
+
+// Take the codec for recording (the output is closed): the format asked
+// for (the codec's nearest rate; it records anything it plays), a DMA
+// buffer on the board's channel, the input set to source, and capture
+// started; got is what it records.
+static bool wssRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got) {
+ uint32_t bits = want->bits == BITS_8 ? BITS_8 : BITS_16;
+ uint32_t channels = want->channels == CHANNELS_MAX ? CHANNELS_MAX : 1;
+ uint32_t frameBytes = bits / BYTE_BITS * channels;
+ uint32_t rate;
+
+ sWss.format = (uint8_t)(codecFormat(bits, channels) | codecRateCode(want->rate, &rate));
+
+ if (!isaDmaAlloc(&sWss.dma, sWss.dma.channel, WSS_BLOCK_FRAMES * frameBytes * WSS_HALVES)) {
+ dvxLog("Audio: wss: no conventional memory for the recording's DMA buffer");
+ return false;
+ }
+
+ sWss.halfBytes = WSS_BLOCK_FRAMES * frameBytes;
+ sWss.locked = wssLock(true);
+
+ if (!sWss.locked || !isaIrqHook(&sWss.irq, sWss.irqNumber, wssIsr)) {
+ wssClose();
+ return false;
+ }
+
+ codecSetFormat(&sWss.codec, sWss.format);
+ codecInputSet(&sWss.codec, sInputs[source]);
+ sWss.recording = true;
+ isaDmaProgram(&sWss.dma, 0, sWss.dma.bytes, ISA_DMA_AUTO_RECORD);
+
+ if (!wssStart(CODEC_CAPTURE)) {
+ wssClose();
+ return false;
+ }
+
+ got->rate = rate;
+ got->bits = bits;
+ got->channels = channels;
+ dvxLog("Audio: wss: recording, %lu Hz", (unsigned long)rate);
+ return true;
+}
+
+
+// Mix one half into the scratch buffer and copy it to the DMA buffer.
+// Interrupt time.
+static void wssRefill(uint32_t half) {
+ audioMixRender(sWss.scratch, WSS_BLOCK_FRAMES);
+ isaDmaWrite(&sWss.dma, half * sWss.halfBytes, sWss.scratch, sWss.halfBytes / WORD_BYTES);
+}
+
+
+// The codec started (what: playing or capture) with an interrupt every
+// half. A board on another IRQ moves one buffer and stops; it is given
+// up on (the next driver is tried, or the recording refused).
+static bool wssStart(uint8_t what) {
+ sWss.irqCount = 0;
+ codecStart(&sWss.codec, WSS_BLOCK_FRAMES, what);
+
+ uint64_t until = platformMicros() + FIRST_IRQ_WAIT_US;
+
+ while (sWss.irqCount == 0 && platformMicros() < until) {
+ platformYield();
+ }
+
+ if (sWss.irqCount == 0) {
+ dvxLog("Audio: wss: no interrupt from the codec on IRQ %lu (set wssIrq)", (unsigned long)sWss.irqNumber);
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/libs/kpunch/audio/gus/gusCalc.c b/src/libs/kpunch/audio/gus/gusCalc.c
new file mode 100644
index 00000000..75f31813
--- /dev/null
+++ b/src/libs/kpunch/audio/gus/gusCalc.c
@@ -0,0 +1,275 @@
+// 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.
+
+// gusCalc.c -- Gravis UltraSound arithmetic (see gusCalc.h)
+
+#include "gusCalc.h"
+#include "../math/drvMath.h"
+
+#include
+
+#define ENV_FIELDS 5
+#define PORT_HEX 16
+#define DECIMAL 10
+#define IRQ_CASCADE 2
+#define IRQ_AT_CASCADE 9
+#define FC_FRACTION_BITS 9 // FC: integer in bits 15-10, fraction in 9-1
+#define ADDR_HIGH_SHIFT 7
+#define ADDR_HIGH_MASK 0x1FFF
+#define ADDR_LOW_MASK 0x7F
+#define ADDR_LOW_SHIFT 9
+#define ADDR16_WORD_MASK 0x1FFFF
+#define ADDR16_BANK_MASK 0xC0000
+#define VOL_MANT_BITS 8
+#define VOL_MANT_MASK 0xFF
+#define LINEAR_TOP_BIT 15
+
+#define ATTEN_STEP_NUM 4252 // 12-bit volume steps per 1000 centibels (256 / 6.0206 dB x 100)
+#define ATTEN_STEP_DEN 1000
+#define PAN_SPAN 1000 // -500..500
+#define PAN_HALF 500
+#define PAN_STEPS 15
+#define FC_UNITY_SHIFT 26 // FC 1.0 (0400h, 10 bits) in 16.16
+#define FC_RESULT_SHIFT 32 // 16.16 FC x 16.16 2^x
+#define FC_MAX 0xFFFE
+#define DMA_LATCH2_SHIFT 3
+#define DMA_FIRST_16 4 // channels 5..7 move words
+#define SAMPLE_DIVISOR_BIAS 2 // 48h holds the divisor less two
+#define MAX_VERSION_FIRST 10 // a MAX's board versions
+#define MAX_VERSION_LAST 11
+#define MAX_PORT_SHIFT 4 // the codec's port: base + 10Ch, from the base's bits 7-4
+#define MAX_PORT_MASK 0x0F
+#define MAX_DMA1_WIDE 0x10
+#define MAX_DMA2_WIDE 0x20
+#define MAX_CODEC_ON 0x40
+#define FC_BIT0 0xFFFE // bit 0 unused
+
+// IRQ and DMA latch codes (SDK 2.14, 2.15): position + 1 is the code
+static const uint8_t sIrqs[] = { IRQ_CASCADE, 5, 3, 7, 11, 12, 15 };
+static const uint8_t sDmas[] = { 1, 3, 5, 6, 7 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+uint32_t gusAddr16(uint32_t addr);
+uint16_t gusAddrHigh(uint32_t addr);
+uint16_t gusAddrLow(uint32_t addr);
+uint16_t gusAttenVolume(int32_t centibels);
+uint8_t gusDmaCode(uint32_t dma);
+uint8_t gusDmaLatch(uint32_t dma1, uint32_t dma2);
+uint16_t gusFc(uint32_t hz, uint32_t outRate);
+uint32_t gusFcBase(uint32_t unityRate, uint32_t outRate);
+uint16_t gusFcCents(int32_t cents, uint32_t fcBase);
+uint8_t gusIrqCode(uint32_t irq);
+bool gusIsMax(uint8_t boardVersion);
+uint8_t gusMaxControl(uint32_t base, uint32_t dma1, uint32_t dma2);
+uint32_t gusOutputRate(uint32_t voices);
+uint8_t gusPan(int32_t pan);
+bool gusParseEnv(const char *text, GusEnvT *out);
+uint32_t gusRecordDma(uint32_t dma1, uint32_t dma2);
+uint8_t gusSampleRate(uint32_t want, uint32_t *rate);
+uint16_t gusVolume(uint32_t linear);
+
+
+uint32_t gusAddr16(uint32_t addr) {
+ return ((addr >> 1) & ADDR16_WORD_MASK) | (addr & ADDR16_BANK_MASK);
+}
+
+
+uint16_t gusAddrHigh(uint32_t addr) {
+ return (uint16_t)((addr >> ADDR_HIGH_SHIFT) & ADDR_HIGH_MASK);
+}
+
+
+uint16_t gusAddrLow(uint32_t addr) {
+ return (uint16_t)((addr & ADDR_LOW_MASK) << ADDR_LOW_SHIFT);
+}
+
+
+uint16_t gusAttenVolume(int32_t centibels) {
+ int32_t v = GUS_VOLUME_MAX - (centibels > 0 ? centibels : 0) * ATTEN_STEP_NUM / ATTEN_STEP_DEN;
+
+ return (uint16_t)(v > 0 ? v : 0);
+}
+
+
+uint8_t gusDmaCode(uint32_t dma) {
+ for (uint32_t i = 0; i < sizeof(sDmas); i++) {
+ if (sDmas[i] == dma) {
+ return (uint8_t)(i + 1);
+ }
+ }
+
+ return 0;
+}
+
+
+uint8_t gusDmaLatch(uint32_t dma1, uint32_t dma2) {
+ uint8_t code1 = gusDmaCode(dma1);
+ uint8_t code2 = gusDmaCode(dma2);
+
+ if (code2 == 0 || dma2 == dma1) {
+ return (uint8_t)(code1 | GUS_DMA_LATCH_COMBINE);
+ }
+
+ return (uint8_t)(code1 | code2 << DMA_LATCH2_SHIFT);
+}
+
+
+// SDK VOCFREQ.C: the 9-bit fraction rounded, then shifted into place.
+uint16_t gusFc(uint32_t hz, uint32_t outRate) {
+ return (uint16_t)((((hz << FC_FRACTION_BITS) + (outRate >> 1)) / outRate) << 1);
+}
+
+
+// 1.0 at unityRate on this output rate, 16.16 (program time: a 64-bit
+// division).
+uint32_t gusFcBase(uint32_t unityRate, uint32_t outRate) {
+ return (uint32_t)(((uint64_t)unityRate << FC_UNITY_SHIFT) / outRate);
+}
+
+
+uint16_t gusFcCents(int32_t cents, uint32_t fcBase) {
+ uint64_t fc = ((uint64_t)fcBase * drvPow2(cents)) >> FC_RESULT_SHIFT;
+
+ return (uint16_t)((fc > FC_MAX ? FC_MAX : fc) & FC_BIT0);
+}
+
+
+uint8_t gusIrqCode(uint32_t irq) {
+ uint32_t line = irq == IRQ_AT_CASCADE ? IRQ_CASCADE : irq;
+
+ for (uint32_t i = 0; i < sizeof(sIrqs); i++) {
+ if (sIrqs[i] == line) {
+ return (uint8_t)(i + 1);
+ }
+ }
+
+ return 0;
+}
+
+
+bool gusIsMax(uint8_t boardVersion) {
+ return boardVersion >= MAX_VERSION_FIRST && boardVersion <= MAX_VERSION_LAST;
+}
+
+
+uint8_t gusMaxControl(uint32_t base, uint32_t dma1, uint32_t dma2) {
+ uint8_t value = (uint8_t)(((base >> MAX_PORT_SHIFT) & MAX_PORT_MASK) | MAX_CODEC_ON);
+
+ if (dma1 != GUS_NONE && dma1 >= DMA_FIRST_16) {
+ value |= MAX_DMA1_WIDE;
+ }
+
+ if (dma2 != GUS_NONE && dma2 >= DMA_FIRST_16) {
+ value |= MAX_DMA2_WIDE;
+ }
+
+ return value;
+}
+
+
+uint32_t gusOutputRate(uint32_t voices) {
+ uint32_t v = voices < GUS_VOICES_MIN ? GUS_VOICES_MIN : (voices > GUS_VOICES_MAX ? GUS_VOICES_MAX : voices);
+
+ return GUS_RATE_BASE / v;
+}
+
+
+uint8_t gusPan(int32_t pan) {
+ int32_t p = pan < -PAN_HALF ? -PAN_HALF : (pan > PAN_HALF ? PAN_HALF : pan);
+
+ return (uint8_t)(((p + PAN_HALF) * PAN_STEPS + PAN_HALF) / PAN_SPAN);
+}
+
+
+// "220,1,1,11,5": the port in hex, the rest decimal; spaces after the
+// commas allowed.
+bool gusParseEnv(const char *text, GusEnvT *out) {
+ uint32_t *fields[ENV_FIELDS] = { &out->base, &out->dma1, &out->dma2, &out->irq1, &out->irq2 };
+ const char *p = text;
+
+ for (int32_t i = 0; i < ENV_FIELDS; i++) {
+ *fields[i] = GUS_NONE;
+ }
+
+ for (int32_t i = 0; p && *p && i < ENV_FIELDS; i++) {
+ char *end;
+
+ while (*p == ' ') {
+ p++;
+ }
+
+ unsigned long v = strtoul(p, &end, i == 0 ? PORT_HEX : DECIMAL);
+
+ if (end == p) {
+ break;
+ }
+
+ *fields[i] = (uint32_t)v;
+ p = end;
+
+ while (*p == ' ') {
+ p++;
+ }
+
+ p = *p == ',' ? p + 1 : NULL;
+ }
+
+ return out->base != GUS_NONE;
+}
+
+
+// The exponent is the top set bit's place, the mantissa the eight bits
+// below it (SDK 4.4).
+uint32_t gusRecordDma(uint32_t dma1, uint32_t dma2) {
+ return gusDmaCode(dma2) ? dma2 : dma1;
+}
+
+
+// The nearest divisor of the clamped rate; the rate it gives, rounded.
+uint8_t gusSampleRate(uint32_t want, uint32_t *rate) {
+ uint32_t r = want < GUS_SAMPLE_RATE_MIN ? GUS_SAMPLE_RATE_MIN : (want > GUS_SAMPLE_RATE_MAX ? GUS_SAMPLE_RATE_MAX : want);
+ uint32_t divisor = (GUS_RATE_BASE + r / 2) / r;
+
+ *rate = (GUS_RATE_BASE + divisor / 2) / divisor;
+ return (uint8_t)(divisor - SAMPLE_DIVISOR_BIAS);
+}
+
+
+uint16_t gusVolume(uint32_t linear) {
+ if (linear == 0) {
+ return 0;
+ }
+
+ uint32_t v = linear > UINT16_MAX ? UINT16_MAX : linear;
+ uint32_t e = LINEAR_TOP_BIT;
+
+ while (!(v & (1u << e))) {
+ e--;
+ }
+
+ uint32_t m = e >= VOL_MANT_BITS ? v >> (e - VOL_MANT_BITS) : v << (VOL_MANT_BITS - e);
+
+ return (uint16_t)(e << VOL_MANT_BITS | (m & VOL_MANT_MASK));
+}
diff --git a/src/libs/kpunch/audio/gus/gusCalc.h b/src/libs/kpunch/audio/gus/gusCalc.h
new file mode 100644
index 00000000..25c82956
--- /dev/null
+++ b/src/libs/kpunch/audio/gus/gusCalc.h
@@ -0,0 +1,129 @@
+// 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.
+
+// gusCalc.h -- Gravis UltraSound arithmetic shared by its drivers (no
+// hardware access)
+//
+// The parts of gus.drv and guswt.drv that decide rather than touch
+// hardware, kept apart so the host tests run them: the ULTRASND variable,
+// the IRQ and DMA latch codes, the GF1's output rate for a voice count,
+// frequency control values, address register halves, the 16-bit address
+// translation, the GF1's logarithmic volume, its sampling rate, and the
+// UltraSound MAX's board version and control register. Facts from the
+// UltraSound SDK 2.22 (sections given) and Linux's GUS drivers
+// (gus_pcm.c, gusmax.c).
+#ifndef GUS_CALC_H
+#define GUS_CALC_H
+
+#include
+#include
+
+#define GUS_VOICES_MIN 14 // the GF1 plays 14..32 voices (SDK 2.6.2.15)
+#define GUS_VOICES_MAX 32
+#define GUS_RATE_BASE 617400 // output rate x active voices (1 / 1.619695497 us, SDK 1.4)
+#define GUS_BANK_BYTES 0x40000 // a 16-bit sample stays inside one 256 KB bank
+#define GUS_VOLUME_MAX 0x0FFF // the 12-bit logarithmic volume at full scale
+#define GUS_NONE 0xFFFFFFFFU
+
+// SET ULTRASND=port,dma1,dma2,irq1,irq2 (SDK UTILITY/CONFIG.C)
+typedef struct {
+ uint32_t base; // hex
+ uint32_t dma1; // DRAM DMA (playback)
+ uint32_t dma2; // record DMA
+ uint32_t irq1; // the GF1's
+ uint32_t irq2; // the MIDI port's
+} GusEnvT;
+
+// The ULTRASND value (NULL allowed; spaces allowed after the commas);
+// false when it names no port. Fields not given are GUS_NONE.
+bool gusParseEnv(const char *text, GusEnvT *out);
+
+// Latch codes (SDK 2.14, 2.15): 0 for a DMA channel or an IRQ the card
+// cannot use (IRQ 9 is IRQ 2's line).
+uint8_t gusDmaCode(uint32_t dma);
+uint8_t gusIrqCode(uint32_t irq);
+
+// The DMA latch for ULTRASND's two channels (SDK 2.15): the first's code
+// in bits 2:0 and the second's in 5:3, or GUS_DMA_LATCH_COMBINE to record
+// on the first too when the second is the same, not given, or one the
+// card cannot use.
+#define GUS_DMA_LATCH_COMBINE 0x40
+uint8_t gusDmaLatch(uint32_t dma1, uint32_t dma2);
+
+// The channel recording uses, whether the GF1 samples or a MAX's codec
+// captures: the second when the card can use it, else the first.
+uint32_t gusRecordDma(uint32_t dma1, uint32_t dma2);
+
+// The GF1's sampling (8-bit, mono or stereo): the rate register (48h) for
+// want, clamped to GUS_SAMPLE_RATE_MIN..GUS_SAMPLE_RATE_MAX, which samples
+// at GUS_RATE_BASE / (value + 2) Hz (the GF1's 9,878,400 Hz clock / 16,
+// the clock its voices run on); *rate is that rate.
+#define GUS_SAMPLE_RATE_MIN 5510
+#define GUS_SAMPLE_RATE_MAX 44100
+uint8_t gusSampleRate(uint32_t want, uint32_t *rate);
+
+// The UltraSound MAX: its board version (base + 506h) reads 10 or 11
+// (Linux gus_main.c: 5..9 an ICS mixer board, 30h the ACE, 50h the
+// Extreme, FFh an older board); and its control register value (written
+// there) turning its CS4231 codec on at base + 10Ch with ULTRASND's
+// channels.
+bool gusIsMax(uint8_t boardVersion);
+uint8_t gusMaxControl(uint32_t base, uint32_t dma1, uint32_t dma2);
+
+// The output rate with voices active (clamped to 14..32).
+uint32_t gusOutputRate(uint32_t voices);
+
+// Frequency control for a voice playing a sample recorded at hz on a GF1
+// running at outRate (1.0 = 0400h; SDK VOCFREQ.C rounding).
+uint16_t gusFc(uint32_t hz, uint32_t outRate);
+
+// A DRAM byte address as the address registers take it: the high word
+// (bits 19-7) and the low word (bits 6-0, fraction 0) (SDK GF1HWARE.H).
+uint16_t gusAddrHigh(uint32_t addr);
+uint16_t gusAddrLow(uint32_t addr);
+
+// A 16-bit sample's byte address (even) as a 16-bit voice or a 16-bit
+// DMA channel addresses it: the word offset inside its 256 KB bank (SDK
+// ULTRADRV.PAS Convert_To_16Bit).
+uint32_t gusAddr16(uint32_t addr);
+
+// The GF1 volume for a linear level (0..65535: 65535 full scale), 12 bits
+// of exponent and mantissa; 0 for silence (SDK 4.4: out = S x 2^(V/256 -
+// 16)).
+uint16_t gusVolume(uint32_t linear);
+
+// The GF1 volume for an attenuation in centibels (0 full scale; a step of
+// the 12-bit volume is 20 log10 2 / 256 dB), 0 at the most. Interrupt
+// time.
+uint16_t gusAttenVolume(int32_t centibels);
+
+// A pan of -500 (left) .. 500 (right) as the GF1's 0..15. Interrupt time.
+uint8_t gusPan(int32_t pan);
+
+// The frequency control for a pitch in cents (0: unityRate frames a
+// second) on a GF1 whose frequency control for unityRate is fcBase
+// (16.16, from gusFcBase at the output rate); interrupt time (drvPow2's
+// 2^x).
+uint32_t gusFcBase(uint32_t unityRate, uint32_t outRate);
+uint16_t gusFcCents(int32_t cents, uint32_t fcBase);
+
+#endif // GUS_CALC_H
diff --git a/src/libs/kpunch/audio/gus/gusHw.c b/src/libs/kpunch/audio/gus/gusHw.c
new file mode 100644
index 00000000..057c0fd1
--- /dev/null
+++ b/src/libs/kpunch/audio/gus/gusHw.c
@@ -0,0 +1,558 @@
+// 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.
+
+// gusHw.c -- the Gravis UltraSound's GF1 chip (see gusHw.h)
+
+#include "gusHw.h"
+#include "../audioDrv.h"
+#include "../math/drvMath.h"
+
+#include
+
+#define DELAY_READS 7 // DRAM port reads: three voice services
+#define RESET_DELAYS 10
+#define PROBE_DELAYS 2
+#define RESET_HOLD 0x00 // 4Ch: the GF1 held in reset
+#define RESET_RUN 0x01
+#define RESET_RUNNING 0x07 // running, DAC on, interrupts on
+#define MIDI_RESET 0x03
+#define VOICES_FIXED 0xC0 // 0Eh: bits 7-6 set
+#define VOICE_FC_ONE 0x0400
+#define VOICE_RAMP_RATE 0x01
+#define VOICE_RAMP_START 0x10
+#define VOICE_RAMP_END 0xE0
+#define VOICE_STOPPED (GUS_VC_STOPPED | GUS_VC_STOP)
+#define MIX_IMAGE 0x0B // line in and out off, latches powered
+#define MIX_LINE_IN_OFF 0x01
+#define MIX_LINE_OUT_OFF 0x02
+#define MIX_MIC_ON 0x04
+#define MIX_LATCHES_ON 0x09 // latches powered, line in off
+#define MIX_IRQ_LATCH 0x40 // the next 2XB write: the IRQ latch
+#define TIMER_COUNT_FASTEST 0xFF // 46h / 47h: one step to FFh
+#define LATCH_DMA_FIRST 0x80 // the DMA latch's first write
+#define LATCH_COMBINE 0x40 // the MIDI interrupts on the GF1's line
+#define REG_CONTROL_CLEAR 0x05 // 2XF: 2XB's power-up IRQ clear register
+#define DRAM_HIGH_MASK 0x0F
+#define DRAM_HIGH_SHIFT 16
+#define WORD_MASK 0xFFFF
+#define PAGE_BYTES 0x10000
+#define POKE_RUN 256 // bytes poked with interrupts off at a time
+#define POS_HIGH_SHIFT 7
+#define POS_LOW_SHIFT 9
+#define POS_MASK 0xFFFFF
+#define TEST_A 0xAA
+#define TEST_B 0x55
+#define BANKS_MAX 4
+#define BYTE_BITS 8
+#define BYTE_MASK 0xFF
+#define FLAGS_IF 0x200
+
+GusHwT gusHw;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+uint8_t gusBoardVersion(void);
+uint32_t gusCli(void);
+void gusDelay(void);
+void gusInputs(bool line, bool mic);
+void gusLatches(uint8_t irqCode, uint8_t dmaLatch);
+bool gusLock(bool lock);
+void gusMaxControlSet(uint8_t value);
+void gusOutput(bool on);
+uint8_t gusPeek(uint32_t addr);
+void gusPoke(uint32_t addr, uint8_t value);
+void gusPoke16(uint32_t addr, int16_t value);
+void gusPokeBlock(uint32_t addr, const uint8_t *src, uint32_t count);
+bool gusProbe(uint16_t base);
+uint8_t gusRead8(uint8_t reg);
+void gusRelease(void);
+void gusReset(uint32_t voices);
+void gusRestore(uint32_t flags);
+uint32_t gusSizeDram(void);
+void gusVoiceAddr(uint32_t voice, uint8_t regHigh, uint32_t addr, bool lowFirst);
+void gusVoiceControl(uint32_t voice, uint8_t reg, uint8_t value);
+uint32_t gusVoicePos(uint32_t voice);
+uint16_t gusVoiceRead16(uint32_t voice, uint8_t reg);
+uint8_t gusVoiceRead8(uint32_t voice, uint8_t reg);
+void gusVoiceStop(uint32_t voice);
+void gusVoiceWrite16(uint32_t voice, uint8_t reg, uint16_t value);
+void gusVoiceWrite8(uint32_t voice, uint8_t reg, uint8_t value);
+void gusWrite16(uint8_t reg, uint16_t value);
+void gusWrite8(uint8_t reg, uint8_t value);
+
+
+uint8_t gusBoardVersion(void) {
+ return inportb(gusHw.base + GUS_BOARD_VERSION);
+}
+
+
+uint32_t gusCli(void) {
+ uint32_t flags;
+
+ __asm__ __volatile__ ("pushfl\n\tpopl %0\n\tcli" : "=r"(flags) : : "memory");
+ return flags;
+}
+
+
+void gusDelay(void) {
+ for (int32_t i = 0; i < DELAY_READS; i++) {
+ inportb(gusHw.base + GUS_DRAM);
+ }
+}
+
+
+// SDK INIT.C UltraSetInterface: 2XB takes the latch the mix control write
+// just before it selected, and only as the very next write.
+void gusInputs(bool line, bool mic) {
+ uint32_t flags = gusCli();
+
+ gusHw.mix = (uint8_t)((gusHw.mix & ~(MIX_LINE_IN_OFF | MIX_MIC_ON)) | (line ? 0 : MIX_LINE_IN_OFF) | (mic ? MIX_MIC_ON : 0));
+ outportb(gusHw.base + GUS_MIX, gusHw.mix);
+ gusRestore(flags);
+}
+
+
+void gusLatches(uint8_t irqCode, uint8_t dmaLatch) {
+ uint16_t base = gusHw.base;
+ uint32_t flags = gusCli();
+ uint8_t irq = (uint8_t)(irqCode | LATCH_COMBINE);
+
+ gusHw.mix = MIX_IMAGE;
+ outportb(base + GUS_REG_CONTROL, REG_CONTROL_CLEAR);
+ outportb(base + GUS_MIX, gusHw.mix);
+ outportb(base + GUS_LATCH, 0);
+ outportb(base + GUS_REG_CONTROL, 0);
+ outportb(base + GUS_MIX, gusHw.mix);
+ outportb(base + GUS_LATCH, (uint8_t)(dmaLatch | LATCH_DMA_FIRST));
+ outportb(base + GUS_MIX, (uint8_t)(gusHw.mix | MIX_IRQ_LATCH));
+ outportb(base + GUS_LATCH, irq);
+ outportb(base + GUS_MIX, gusHw.mix);
+ outportb(base + GUS_LATCH, dmaLatch);
+ outportb(base + GUS_MIX, (uint8_t)(gusHw.mix | MIX_IRQ_LATCH));
+ outportb(base + GUS_LATCH, irq);
+ outportb(base + GUS_VOICE, 0);
+ gusHw.mix |= MIX_LATCHES_ON;
+ outportb(base + GUS_MIX, gusHw.mix);
+ outportb(base + GUS_VOICE, 0);
+ gusRestore(flags);
+}
+
+
+// This file's code and gusHw, the gusCalc.c code interrupt time uses, and
+// drvMath's (a span each: the files need not be next to each other).
+bool gusLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)gusCli, 0 },
+ { (const void *)gusDelay, 0 },
+ { (const void *)gusPoke, 0 },
+ { (const void *)gusPoke16, 0 },
+ { (const void *)gusRead8, 0 },
+ { (const void *)gusRestore, 0 },
+ { (const void *)gusVoiceAddr, 0 },
+ { (const void *)gusVoiceControl, 0 },
+ { (const void *)gusVoicePos, 0 },
+ { (const void *)gusVoiceRead16, 0 },
+ { (const void *)gusVoiceRead8, 0 },
+ { (const void *)gusVoiceStop, 0 },
+ { (const void *)gusVoiceWrite16, 0 },
+ { (const void *)gusVoiceWrite8, 0 },
+ { (const void *)gusWrite16, 0 },
+ { (const void *)gusWrite8, 0 },
+ { &gusHw, sizeof(gusHw) }
+ };
+ const AudioLockItemT calc[] = {
+ { (const void *)gusAddr16, 0 },
+ { (const void *)gusAddrHigh, 0 },
+ { (const void *)gusAddrLow, 0 },
+ { (const void *)gusAttenVolume, 0 },
+ { (const void *)gusFcCents, 0 },
+ { (const void *)gusPan, 0 }
+ };
+
+ bool ok = audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+
+ ok = audioLockSpan(calc, (int32_t)(sizeof(calc) / sizeof(calc[0])), lock) && ok;
+ return drvMathLock(lock) && ok;
+}
+
+
+void gusMaxControlSet(uint8_t value) {
+ outportb(gusHw.base + GUS_BOARD_VERSION, value);
+}
+
+
+void gusOutput(bool on) {
+ uint32_t flags = gusCli();
+
+ gusHw.mix = (uint8_t)(on ? gusHw.mix & ~MIX_LINE_OUT_OFF : gusHw.mix | MIX_LINE_OUT_OFF);
+ outportb(gusHw.base + GUS_MIX, gusHw.mix);
+ gusRestore(flags);
+}
+
+
+uint8_t gusPeek(uint32_t addr) {
+ uint16_t base = gusHw.base;
+ uint32_t flags = gusCli();
+
+ outportb(base + GUS_SELECT, GUS_G_DRAM_LOW);
+ outportw(base + GUS_DATA_LOW, (uint16_t)(addr & WORD_MASK));
+ outportb(base + GUS_SELECT, GUS_G_DRAM_HIGH);
+ outportb(base + GUS_DATA_HIGH, (uint8_t)((addr >> DRAM_HIGH_SHIFT) & DRAM_HIGH_MASK));
+
+ uint8_t value = inportb(base + GUS_DRAM);
+
+ gusRestore(flags);
+ return value;
+}
+
+
+void gusPoke(uint32_t addr, uint8_t value) {
+ uint16_t base = gusHw.base;
+ uint32_t flags = gusCli();
+
+ outportb(base + GUS_SELECT, GUS_G_DRAM_LOW);
+ outportw(base + GUS_DATA_LOW, (uint16_t)(addr & WORD_MASK));
+ outportb(base + GUS_SELECT, GUS_G_DRAM_HIGH);
+ outportb(base + GUS_DATA_HIGH, (uint8_t)((addr >> DRAM_HIGH_SHIFT) & DRAM_HIGH_MASK));
+ outportb(base + GUS_DRAM, value);
+ gusRestore(flags);
+}
+
+
+void gusPoke16(uint32_t addr, int16_t value) {
+ gusPoke(addr, (uint8_t)((uint16_t)value & BYTE_MASK));
+ gusPoke(addr + 1, (uint8_t)((uint16_t)value >> BYTE_BITS));
+}
+
+
+// Each run inside one 64 KB page and at most POKE_RUN bytes: the high
+// bits once, then 43h left selected and only its word and the byte
+// written per byte.
+void gusPokeBlock(uint32_t addr, const uint8_t *src, uint32_t count) {
+ uint16_t base = gusHw.base;
+
+ while (count > 0) {
+ uint32_t room = PAGE_BYTES - (addr & WORD_MASK);
+ uint32_t run = count < POKE_RUN ? count : POKE_RUN;
+ uint32_t flags;
+
+ run = run < room ? run : room;
+ flags = gusCli();
+ outportb(base + GUS_SELECT, GUS_G_DRAM_HIGH);
+ outportb(base + GUS_DATA_HIGH, (uint8_t)((addr >> DRAM_HIGH_SHIFT) & DRAM_HIGH_MASK));
+ outportb(base + GUS_SELECT, GUS_G_DRAM_LOW);
+
+ for (uint32_t i = 0; i < run; i++) {
+ outportw(base + GUS_DATA_LOW, (uint16_t)((addr + i) & WORD_MASK));
+ outportb(base + GUS_DRAM, src[i]);
+ }
+
+ gusRestore(flags);
+ addr += run;
+ src += run;
+ count -= run;
+ }
+}
+
+
+// SDK INIT.C UltraProbe: out of reset, then a ping of DRAM 0 and 1 (put
+// back as they were).
+bool gusProbe(uint16_t base) {
+ gusHw.base = base;
+ gusWrite8(GUS_G_RESET, RESET_HOLD);
+
+ for (int32_t i = 0; i < PROBE_DELAYS; i++) {
+ gusDelay();
+ }
+
+ gusWrite8(GUS_G_RESET, RESET_RUN);
+
+ for (int32_t i = 0; i < PROBE_DELAYS; i++) {
+ gusDelay();
+ }
+
+ uint8_t a = gusPeek(0);
+ uint8_t b = gusPeek(1);
+
+ gusPoke(0, TEST_A);
+ gusPoke(1, TEST_B);
+
+ bool found = gusPeek(0) == TEST_A && gusPeek(1) == TEST_B;
+
+ gusPoke(0, a);
+ gusPoke(1, b);
+ return found;
+}
+
+
+uint8_t gusRead8(uint8_t reg) {
+ uint32_t flags = gusCli();
+
+ outportb(gusHw.base + GUS_SELECT, reg);
+
+ uint8_t value = inportb(gusHw.base + GUS_DATA_HIGH);
+
+ gusRestore(flags);
+ return value;
+}
+
+
+void gusRelease(void) {
+ if (gusHw.pcmOpen || gusHw.synthOpen) {
+ return;
+ }
+
+ gusOutput(false);
+ gusHw.running = false;
+}
+
+
+// SDK RESET.C UltraReset.
+void gusReset(uint32_t voices) {
+ uint16_t base = gusHw.base;
+ uint32_t flags = gusCli();
+
+ gusWrite8(GUS_G_RESET, RESET_HOLD);
+
+ for (int32_t i = 0; i < RESET_DELAYS; i++) {
+ gusDelay();
+ }
+
+ gusWrite8(GUS_G_RESET, RESET_RUN);
+
+ for (int32_t i = 0; i < RESET_DELAYS; i++) {
+ gusDelay();
+ }
+
+ outportb(base + GUS_MIDI_CONTROL, MIDI_RESET);
+
+ for (int32_t i = 0; i < RESET_DELAYS; i++) {
+ gusDelay();
+ }
+
+ outportb(base + GUS_MIDI_CONTROL, 0);
+ gusWrite8(GUS_G_DMA_CONTROL, 0);
+ gusWrite8(GUS_G_TIMER_CONTROL, 0);
+
+ // The timers count from here to FFh, and they answer an AdLib's
+ // timer commands at 388h-389h, which the card always decodes. At
+ // their fastest whatever count an AdLib program writes, they never
+ // pass for an OPL's: opl.drv wants timer 1 still running at its
+ // slowest count.
+ gusWrite8(GUS_G_TIMER1_COUNT, TIMER_COUNT_FASTEST);
+ gusWrite8(GUS_G_TIMER2_COUNT, TIMER_COUNT_FASTEST);
+ gusWrite8(GUS_G_SAMPLE_CTRL, 0);
+ gusHw.voices = voices < GUS_VOICES_MIN ? GUS_VOICES_MIN : (voices > GUS_VOICES_MAX ? GUS_VOICES_MAX : voices);
+ gusHw.outRate = gusOutputRate(gusHw.voices);
+ gusWrite8(GUS_G_VOICES, (uint8_t)((gusHw.voices - 1) | VOICES_FIXED));
+
+ // Stale interrupts cleared, before the voices and after
+ inportb(base + GUS_IRQ_STATUS);
+ gusRead8(GUS_G_DMA_CONTROL);
+ gusRead8(GUS_G_SAMPLE_CTRL);
+ gusRead8(GUS_G_IRQ_SOURCE);
+
+ for (uint32_t v = 0; v < GUS_VOICES_MAX; v++) {
+ gusVoiceStop(v);
+ gusVoiceWrite16(v, GUS_V_FREQ, VOICE_FC_ONE);
+ gusVoiceWrite8(v, GUS_V_RAMP_RATE, VOICE_RAMP_RATE);
+ gusVoiceWrite8(v, GUS_V_RAMP_START, VOICE_RAMP_START);
+ gusVoiceWrite8(v, GUS_V_RAMP_END, VOICE_RAMP_END);
+ gusVoiceWrite8(v, GUS_V_PAN, GUS_PAN_CENTRE);
+ }
+
+ inportb(base + GUS_IRQ_STATUS);
+ gusRead8(GUS_G_DMA_CONTROL);
+ gusRead8(GUS_G_SAMPLE_CTRL);
+ gusRead8(GUS_G_IRQ_SOURCE);
+ gusWrite8(GUS_G_RESET, RESET_RUNNING);
+ gusRestore(flags);
+}
+
+
+void gusRestore(uint32_t flags) {
+ if (flags & FLAGS_IF) {
+ __asm__ __volatile__ ("sti" : : : "memory");
+ }
+}
+
+
+// ALSA's way: each further 256 KB bank holds two bytes without them
+// showing at 0 (a missing bank wraps there); every byte touched put back.
+uint32_t gusSizeDram(void) {
+ uint8_t keep0 = gusPeek(0);
+ uint8_t keep1 = gusPeek(1);
+ uint32_t banks = 1;
+ bool more = true;
+
+ gusPoke(0, TEST_A);
+ gusPoke(1, TEST_B);
+
+ while (more && banks < BANKS_MAX) {
+ uint32_t at = banks * GUS_BANK_BYTES;
+ uint8_t s0 = gusPeek(at);
+ uint8_t s1 = gusPeek(at + 1);
+
+ gusPoke(at, TEST_B);
+ gusPoke(at + 1, TEST_A);
+ more = gusPeek(at) == TEST_B && gusPeek(at + 1) == TEST_A && gusPeek(0) == TEST_A;
+ gusPoke(at, s0);
+ gusPoke(at + 1, s1);
+ banks += more ? 1 : 0;
+ }
+
+ gusPoke(0, keep0);
+ gusPoke(1, keep1);
+ return banks * GUS_BANK_BYTES;
+}
+
+
+void gusVoiceAddr(uint32_t voice, uint8_t regHigh, uint32_t addr, bool lowFirst) {
+ uint16_t base = gusHw.base;
+ uint32_t flags = gusCli();
+
+ outportb(base + GUS_VOICE, (uint8_t)voice);
+
+ if (lowFirst) {
+ outportb(base + GUS_SELECT, (uint8_t)(regHigh + 1));
+ outportw(base + GUS_DATA_LOW, gusAddrLow(addr));
+ }
+
+ outportb(base + GUS_SELECT, regHigh);
+ outportw(base + GUS_DATA_LOW, gusAddrHigh(addr));
+
+ if (!lowFirst) {
+ outportb(base + GUS_SELECT, (uint8_t)(regHigh + 1));
+ outportw(base + GUS_DATA_LOW, gusAddrLow(addr));
+ }
+
+ gusRestore(flags);
+}
+
+
+void gusVoiceControl(uint32_t voice, uint8_t reg, uint8_t value) {
+ uint16_t base = gusHw.base;
+ uint32_t flags = gusCli();
+
+ outportb(base + GUS_VOICE, (uint8_t)voice);
+ outportb(base + GUS_SELECT, reg);
+ outportb(base + GUS_DATA_HIGH, value);
+ gusDelay();
+ outportb(base + GUS_DATA_HIGH, value);
+ gusRestore(flags);
+}
+
+
+uint32_t gusVoicePos(uint32_t voice) {
+ uint16_t base = gusHw.base;
+ uint32_t flags = gusCli();
+
+ outportb(base + GUS_VOICE, (uint8_t)voice);
+ outportb(base + GUS_SELECT, GUS_V_POS_HIGH | GUS_READ);
+
+ uint32_t high = inportw(base + GUS_DATA_LOW);
+
+ outportb(base + GUS_SELECT, (GUS_V_POS_HIGH + 1) | GUS_READ);
+
+ uint32_t low = inportw(base + GUS_DATA_LOW);
+
+ gusRestore(flags);
+ return ((high << POS_HIGH_SHIFT) | (low >> POS_LOW_SHIFT)) & POS_MASK;
+}
+
+
+uint16_t gusVoiceRead16(uint32_t voice, uint8_t reg) {
+ uint32_t flags = gusCli();
+
+ outportb(gusHw.base + GUS_VOICE, (uint8_t)voice);
+ outportb(gusHw.base + GUS_SELECT, (uint8_t)(reg | GUS_READ));
+
+ uint16_t value = inportw(gusHw.base + GUS_DATA_LOW);
+
+ gusRestore(flags);
+ return value;
+}
+
+
+uint8_t gusVoiceRead8(uint32_t voice, uint8_t reg) {
+ uint32_t flags = gusCli();
+
+ outportb(gusHw.base + GUS_VOICE, (uint8_t)voice);
+ outportb(gusHw.base + GUS_SELECT, (uint8_t)(reg | GUS_READ));
+
+ uint8_t value = inportb(gusHw.base + GUS_DATA_HIGH);
+
+ gusRestore(flags);
+ return value;
+}
+
+
+// Stopped (both self-modifying registers written twice), silent, and
+// every address at the silent park block.
+void gusVoiceStop(uint32_t voice) {
+ gusVoiceControl(voice, GUS_V_CONTROL, VOICE_STOPPED);
+ gusVoiceControl(voice, GUS_V_VOL_CONTROL, GUS_RAMP_STOPPED);
+ gusVoiceWrite16(voice, GUS_V_VOLUME, 0);
+ gusVoiceAddr(voice, GUS_V_START_HIGH, gusHw.park, false);
+ gusVoiceAddr(voice, GUS_V_END_HIGH, gusHw.park, false);
+ gusVoiceAddr(voice, GUS_V_POS_HIGH, gusHw.park, false);
+}
+
+
+void gusVoiceWrite16(uint32_t voice, uint8_t reg, uint16_t value) {
+ uint32_t flags = gusCli();
+
+ outportb(gusHw.base + GUS_VOICE, (uint8_t)voice);
+ outportb(gusHw.base + GUS_SELECT, reg);
+ outportw(gusHw.base + GUS_DATA_LOW, value);
+ gusRestore(flags);
+}
+
+
+void gusVoiceWrite8(uint32_t voice, uint8_t reg, uint8_t value) {
+ uint32_t flags = gusCli();
+
+ outportb(gusHw.base + GUS_VOICE, (uint8_t)voice);
+ outportb(gusHw.base + GUS_SELECT, reg);
+ outportb(gusHw.base + GUS_DATA_HIGH, value);
+ gusRestore(flags);
+}
+
+
+void gusWrite16(uint8_t reg, uint16_t value) {
+ uint32_t flags = gusCli();
+
+ outportb(gusHw.base + GUS_SELECT, reg);
+ outportw(gusHw.base + GUS_DATA_LOW, value);
+ gusRestore(flags);
+}
+
+
+void gusWrite8(uint8_t reg, uint8_t value) {
+ uint32_t flags = gusCli();
+
+ outportb(gusHw.base + GUS_SELECT, reg);
+ outportb(gusHw.base + GUS_DATA_HIGH, value);
+ gusRestore(flags);
+}
diff --git a/src/libs/kpunch/audio/gus/gusHw.h b/src/libs/kpunch/audio/gus/gusHw.h
new file mode 100644
index 00000000..b1c4723a
--- /dev/null
+++ b/src/libs/kpunch/audio/gus/gusHw.h
@@ -0,0 +1,215 @@
+// 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.
+
+// gusHw.h -- the Gravis UltraSound's GF1 chip, shared by its drivers
+//
+// gus.drv (digital sound on two voices, and the wavetable synthesizer on
+// the rest) and guswt.drv (the synthesizer alone, beside another sound
+// device) each link their own copy. Every sequence of port accesses (a
+// voice or register select, then its data) runs with interrupts off, so
+// the sound interrupt and the music timer can use the chip as well as the
+// program: no sequence is ever cut in two. Facts from the UltraSound SDK
+// 2.22 (sections given) and the reference summary.
+#ifndef GUS_HW_H
+#define GUS_HW_H
+
+#include "gusCalc.h"
+
+#include
+#include
+
+// Ports, from the base (2X0)
+#define GUS_MIX 0x000 // mix control (write only)
+#define GUS_IRQ_STATUS 0x006
+#define GUS_LATCH 0x00B // IRQ or DMA latch (after a mix control write)
+#define GUS_REG_CONTROL 0x00F // 2XB register select (rev 3.4+)
+#define GUS_MIDI_CONTROL 0x100
+#define GUS_VOICE 0x102 // voice select
+#define GUS_SELECT 0x103 // register select
+#define GUS_DATA_LOW 0x104 // 16-bit registers (a word access)
+#define GUS_DATA_HIGH 0x105 // 8-bit registers
+#define GUS_DRAM 0x107 // DRAM at the I/O address
+#define GUS_MAX_CODEC 0x10C // a MAX's CS4231 (once its control register turns it on)
+#define GUS_BOARD_VERSION 0x506 // board version (read); a MAX's control register (write)
+
+// 2X6 IRQ status bits
+#define GUS_IRQ_WAVE 0x20
+#define GUS_IRQ_VOLUME 0x40
+#define GUS_IRQ_DMA 0x80
+
+// Global registers
+#define GUS_G_DMA_CONTROL 0x41
+#define GUS_G_DMA_ADDR 0x42
+#define GUS_G_DRAM_LOW 0x43
+#define GUS_G_DRAM_HIGH 0x44
+#define GUS_G_TIMER_CONTROL 0x45
+#define GUS_G_TIMER1_COUNT 0x46
+#define GUS_G_TIMER2_COUNT 0x47
+#define GUS_G_SAMPLE_RATE 0x48
+#define GUS_G_SAMPLE_CTRL 0x49
+#define GUS_G_RESET 0x4C
+#define GUS_G_VOICES 0x0E
+#define GUS_G_IRQ_SOURCE 0x8F
+
+// Voice registers (reads: + GUS_READ)
+#define GUS_V_CONTROL 0x00
+#define GUS_V_FREQ 0x01
+#define GUS_V_START_HIGH 0x02 // loop start; low is + 1
+#define GUS_V_END_HIGH 0x04 // end; low is + 1
+#define GUS_V_RAMP_RATE 0x06
+#define GUS_V_RAMP_START 0x07
+#define GUS_V_RAMP_END 0x08
+#define GUS_V_VOLUME 0x09
+#define GUS_V_POS_HIGH 0x0A // current address; low is + 1
+#define GUS_V_PAN 0x0C
+#define GUS_V_VOL_CONTROL 0x0D
+#define GUS_READ 0x80
+
+// Voice control (00h) and volume control (0Dh) bits
+#define GUS_VC_STOPPED 0x01
+#define GUS_VC_STOP 0x02
+#define GUS_VC_16BIT 0x04
+#define GUS_VC_LOOP 0x08
+#define GUS_VC_IRQ 0x20
+#define GUS_RAMP_STOPPED 0x03 // volume control: the ramp stopped (never left running)
+#define GUS_VOL_ROLLOVER 0x04
+
+// DRAM DMA control (41h)
+#define GUS_DMA_ENABLE 0x01
+#define GUS_DMA_WIDE_CHAN 0x04 // a 16-bit PC channel
+#define GUS_DMA_TC_IRQ 0x20
+#define GUS_DMA_DATA16 0x40
+#define GUS_DMA_INVERT 0x80 // flip each sample's top bit (unsigned data)
+#define GUS_DMA_ADDR_SHIFT 4
+
+// Sampling control (49h; Linux gus_pcm.c): no auto-init, so each block is
+// started again from its terminal count
+#define GUS_SC_START 0x01
+#define GUS_SC_STEREO 0x02
+#define GUS_SC_WIDE_CHAN 0x04 // a 16-bit PC channel
+#define GUS_SC_IRQ 0x20 // an interrupt at the terminal count
+#define GUS_SC_IRQ_PENDING 0x40 // (read) that interrupt, cleared by the read
+#define GUS_SC_INVERT 0x80 // flip each sample's top bit (unsigned data)
+
+#define GUS_VOL_SHIFT 4 // 09h holds the 12-bit volume in bits 15-4
+#define GUS_PAN_LEFT 0
+#define GUS_PAN_CENTRE 7
+#define GUS_PAN_RIGHT 15
+#define GUS_ALIGN 32 // DRAM blocks, DMA start addresses
+
+// The card as the module's drivers share it: digital sound on the first
+// voices and a ring at the top of the memory, the synthesizer on the
+// voices from synthFirst and the memory below synthTop
+typedef struct {
+ uint32_t dramBytes; // its memory (256 KB steps)
+ uint32_t voices; // active voices
+ uint32_t outRate; // the output rate they give
+ uint32_t park; // a silent DRAM block idle voices point at
+ uint32_t synthTop; // the synthesizer's memory: 0 .. synthTop - 1
+ uint32_t synthFirst; // the synthesizer's first voice
+ uint16_t base;
+ uint8_t mix; // the mix control register as last written
+ bool running; // reset and on (by whichever opened first)
+ bool pcmOpen; // digital sound uses it
+ bool synthOpen; // the synthesizer uses it
+} GusHwT;
+
+extern GusHwT gusHw;
+
+// Interrupts off (the flags before) and back.
+uint32_t gusCli(void);
+void gusRestore(uint32_t flags);
+
+// A GF1 is at base: it holds a byte at DRAM 0 and 1 after a reset.
+bool gusProbe(uint16_t base);
+
+// The DRAM size in 256 KB steps, looked for without leaving a trace.
+uint32_t gusSizeDram(void);
+
+// The SDK's reset with voices active: every voice stopped, silent and
+// pointed at gusHw.park, timers and DMA off (the timers' counts at their
+// fastest: see gusReset), stale interrupts cleared, then the DAC and the
+// interrupts on.
+void gusReset(uint32_t voices);
+
+// The IRQ and DMA latches (gusIrqCode, gusDmaLatch) in the SDK's
+// sequence; the latches powered.
+void gusLatches(uint8_t irqCode, uint8_t dmaLatch);
+
+// The board version register (a MAX: gusIsMax), and a MAX's control
+// register (gusMaxControl).
+uint8_t gusBoardVersion(void);
+void gusMaxControlSet(uint8_t value);
+
+// What the GF1 samples: the line input, the microphone, or neither.
+void gusInputs(bool line, bool mic);
+
+// Line out on or off.
+void gusOutput(bool on);
+
+// One of the module's users is done with the card (its flag cleared by
+// the caller): the last turns the output off.
+void gusRelease(void);
+
+// One DRAM byte at addr, read and written.
+uint8_t gusPeek(uint32_t addr);
+void gusPoke(uint32_t addr, uint8_t value);
+
+// A 16-bit sample poked at addr (low byte first).
+void gusPoke16(uint32_t addr, int16_t value);
+
+// count bytes poked from addr on, a few hundred at a time with interrupts
+// off (the address's low word written per byte, its high bits per 64 KB).
+void gusPokeBlock(uint32_t addr, const uint8_t *src, uint32_t count);
+
+// Global or voice register access (a voice selected first; interrupts off
+// across the selects and the data).
+uint8_t gusRead8(uint8_t reg);
+uint16_t gusVoiceRead16(uint32_t voice, uint8_t reg);
+uint8_t gusVoiceRead8(uint32_t voice, uint8_t reg);
+void gusVoiceWrite16(uint32_t voice, uint8_t reg, uint16_t value);
+void gusVoiceWrite8(uint32_t voice, uint8_t reg, uint8_t value);
+void gusWrite16(uint8_t reg, uint16_t value);
+void gusWrite8(uint8_t reg, uint8_t value);
+
+// A voice's address pair (start, end or position: the high register) at
+// a byte address (16-bit voices: translated by the caller). The high
+// word first, or the low first when lowFirst (moving the end backwards).
+void gusVoiceAddr(uint32_t voice, uint8_t regHigh, uint32_t addr, bool lowFirst);
+
+// The voice's current address (byte or 16-bit word address, as written).
+uint32_t gusVoicePos(uint32_t voice);
+
+// A self-modifying register (voice control, volume control) written
+// twice with the GF1's wait between (SDK 2.6.2).
+void gusVoiceControl(uint32_t voice, uint8_t reg, uint8_t value);
+
+// The voice stopped and silent.
+void gusVoiceStop(uint32_t voice);
+
+// The GF1's wait: three voice services (seven DRAM port reads).
+void gusDelay(void);
+
+// Pin or release this file's code and gusHw (interrupt time uses them).
+bool gusLock(bool lock);
+
+#endif // GUS_HW_H
diff --git a/src/libs/kpunch/audio/gus/gusSynth.c b/src/libs/kpunch/audio/gus/gusSynth.c
new file mode 100644
index 00000000..2079b3e4
--- /dev/null
+++ b/src/libs/kpunch/audio/gus/gusSynth.c
@@ -0,0 +1,277 @@
+// 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.
+
+// gusSynth.c -- the Gravis UltraSound as a wavetable synthesizer (see
+// gusSynth.h)
+//
+// A note: the voice stopped, its frequency control from the pitch, its
+// loop (start at the loop's start, end at its end: the GF1 jumps back on
+// reaching the end, so the loop's last frame is the one before) or its
+// end (the last frame), its position at the sample's start, its pan and
+// volume, then started. A sample that loops until the key is up plays
+// out from there: the loop off and the end moved to the sample's. A
+// voice that has played to its end reads stopped.
+//
+// Alone (guswt.drv), the card is found here: ULTRASND's port, else the
+// usual ones probed; reset, its memory sized, a silent block at its top
+// for idle voices. No IRQ or DMA is needed: samples go in through the
+// DRAM port.
+
+#include "gusSynth.h"
+#include "gusHw.h"
+
+#include
+#include
+#include
+
+#define BITS_16 16
+#define BYTE_BITS 8
+#define BYTE_MASK 0xFF
+#define UPLOAD_CHUNK 512 // bytes made ready at a time
+#define KB 1024
+
+// Ports probed without ULTRASND
+static const uint16_t sBases[] = { 0x220, 0x240, 0x260, 0x210, 0x230, 0x250 };
+
+// What a voice plays, for its release
+typedef struct {
+ uint32_t end; // the sample's last frame (byte address)
+ uint8_t mode; // voice control: 16-bit, loop
+ uint8_t loop; // AUDIO_WT_LOOP_*
+} SynthVoiceT;
+
+static SynthVoiceT sVoices[GUS_VOICES_MAX];
+static uint32_t sFcBase; // gusFcBase of the output rate
+static bool sLocked = false;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool gusSynthActive(uint32_t voice);
+void gusSynthClose(void);
+static bool gusSynthFind(const AudioDrvConfigT *cfg);
+static bool gusSynthLock(bool lock);
+bool gusSynthOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
+void gusSynthRelease(uint32_t voice);
+void gusSynthStart(uint32_t voice, const AudioWtVoiceT *v);
+void gusSynthStop(uint32_t voice);
+void gusSynthUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what);
+bool gusSynthUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
+static void voiceAddr(uint32_t voice, uint8_t reg, uint32_t addr, bool wide, bool lowFirst);
+
+
+// Played to its end (stopped) or not. Interrupt time.
+bool gusSynthActive(uint32_t voice) {
+ return !(gusVoiceRead8(gusHw.synthFirst + voice, GUS_V_CONTROL) & GUS_VC_STOPPED);
+}
+
+
+void gusSynthClose(void) {
+ for (uint32_t v = gusHw.synthFirst; v < gusHw.voices; v++) {
+ gusVoiceStop(v);
+ }
+
+ if (sLocked) {
+ gusSynthLock(false);
+ sLocked = false;
+ }
+
+ gusHw.synthOpen = false;
+ gusRelease();
+}
+
+
+// The card alone: ULTRASND's port, else the usual ones; reset with the
+// voices DVX.INI asks for, the park block at the top of its memory.
+static bool gusSynthFind(const AudioDrvConfigT *cfg) {
+ GusEnvT env;
+ bool named = gusParseEnv(getenv("ULTRASND"), &env);
+ uint32_t tries = named ? 1 : (uint32_t)(sizeof(sBases) / sizeof(sBases[0]));
+ bool found = false;
+
+ for (uint32_t i = 0; !found && i < tries; i++) {
+ found = gusProbe(named ? (uint16_t)env.base : sBases[i]);
+ }
+
+ if (!found) {
+ return false;
+ }
+
+ gusHw.dramBytes = gusSizeDram();
+ gusHw.park = gusHw.dramBytes - GUS_ALIGN;
+
+ for (uint32_t b = 0; b < GUS_ALIGN; b++) {
+ gusPoke(gusHw.park + b, 0);
+ }
+
+ gusOutput(false);
+ gusReset((uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, GUS_INI_VOICES, GUS_VOICES_DEFAULT));
+ gusOutput(true);
+ gusHw.synthTop = gusHw.park;
+ gusHw.synthFirst = 0;
+ gusHw.running = true;
+ return true;
+}
+
+
+// The interrupt-time functions here, the voices' table, and the GF1 code.
+static bool gusSynthLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)gusSynthActive, 0 },
+ { (const void *)gusSynthRelease, 0 },
+ { (const void *)gusSynthStart, 0 },
+ { (const void *)gusSynthStop, 0 },
+ { (const void *)gusSynthUpdate, 0 },
+ { (const void *)voiceAddr, 0 },
+ { sVoices, sizeof(sVoices) },
+ { &sFcBase, sizeof(sFcBase) }
+ };
+
+ bool ok = audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+
+ return gusLock(lock) && ok;
+}
+
+
+// The voices and memory the card (as the module set it up, or found
+// here) leaves the synthesizer.
+bool gusSynthOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) {
+ if (!gusHw.running && !gusSynthFind(cfg)) {
+ return false;
+ }
+
+ sLocked = gusSynthLock(true);
+
+ if (!sLocked) {
+ gusRelease();
+ return false;
+ }
+
+ // level stays 0: a GUS alone plays digital sound on its own voices
+ sFcBase = gusFcBase(AUDIO_WT_PITCH_RATE, gusHw.outRate);
+ gusHw.synthOpen = true;
+ info->voices = gusHw.voices - gusHw.synthFirst;
+ info->memory = gusHw.synthTop;
+ info->flags = AUDIO_WT_8BIT | AUDIO_WT_BANK_256K;
+ snprintf(info->description, sizeof(info->description), "Gravis UltraSound wavetable at %Xh (%lu voices, %lu KB)", (unsigned)gusHw.base, (unsigned long)info->voices, (unsigned long)(info->memory / KB));
+ return true;
+}
+
+
+// A sample that loops while the key is down plays out: the end moved to
+// the sample's (forwards: its high word first), then the loop off.
+// Interrupt time.
+void gusSynthRelease(uint32_t voice) {
+ const SynthVoiceT *s = &sVoices[voice];
+ uint32_t gv = gusHw.synthFirst + voice;
+
+ if (s->loop != AUDIO_WT_LOOP_RELEASE) {
+ return;
+ }
+
+ voiceAddr(gv, GUS_V_END_HIGH, s->end, (s->mode & GUS_VC_16BIT) != 0, false);
+ gusVoiceControl(gv, GUS_V_CONTROL, (uint8_t)(s->mode & ~GUS_VC_LOOP));
+}
+
+
+// Interrupt time.
+void gusSynthStart(uint32_t voice, const AudioWtVoiceT *v) {
+ SynthVoiceT *s = &sVoices[voice];
+ uint32_t gv = gusHw.synthFirst + voice;
+ uint32_t bytes = v->bits / BYTE_BITS;
+ bool wide = v->bits == BITS_16;
+ bool loops = v->loop != AUDIO_WT_LOOP_NONE && v->loopEnd > v->loopStart;
+ uint32_t last = v->end > 0 ? v->end - 1 : 0;
+
+ s->end = v->addr + last * bytes;
+ s->loop = loops ? v->loop : AUDIO_WT_LOOP_NONE;
+ s->mode = (uint8_t)((wide ? GUS_VC_16BIT : 0) | (loops ? GUS_VC_LOOP : 0));
+ gusVoiceStop(gv);
+ gusVoiceWrite16(gv, GUS_V_FREQ, gusFcCents(v->pitch, sFcBase));
+ voiceAddr(gv, GUS_V_START_HIGH, v->addr + (loops ? v->loopStart : 0) * bytes, wide, false);
+ voiceAddr(gv, GUS_V_END_HIGH, loops ? v->addr + v->loopEnd * bytes : s->end, wide, false);
+ voiceAddr(gv, GUS_V_POS_HIGH, v->addr + v->offset * bytes, wide, false);
+ gusVoiceWrite8(gv, GUS_V_PAN, gusPan(v->pan));
+ gusVoiceWrite16(gv, GUS_V_VOLUME, (uint16_t)(gusAttenVolume(v->attenuation) << GUS_VOL_SHIFT));
+ gusVoiceControl(gv, GUS_V_CONTROL, s->mode);
+}
+
+
+// Interrupt time.
+void gusSynthStop(uint32_t voice) {
+ gusVoiceStop(gusHw.synthFirst + voice);
+}
+
+
+// Pitch, volume and pan (what the GF1 has). Interrupt time.
+void gusSynthUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ uint32_t gv = gusHw.synthFirst + voice;
+
+ if (what & AUDIO_WT_SET_PITCH) {
+ gusVoiceWrite16(gv, GUS_V_FREQ, gusFcCents(v->pitch, sFcBase));
+ }
+
+ if (what & AUDIO_WT_SET_VOLUME) {
+ gusVoiceWrite16(gv, GUS_V_VOLUME, (uint16_t)(gusAttenVolume(v->attenuation) << GUS_VOL_SHIFT));
+ }
+
+ if (what & AUDIO_WT_SET_PAN) {
+ gusVoiceWrite8(gv, GUS_V_PAN, gusPan(v->pan));
+ }
+}
+
+
+// Frames as the GF1 stores them (signed, 16-bit little-endian or the top
+// byte), through the DRAM port a chunk at a time. Program time.
+bool gusSynthUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits) {
+ uint8_t chunk[UPLOAD_CHUNK];
+ bool wide = bits == BITS_16;
+ uint32_t bytes = count * (wide ? 2 : 1);
+
+ if (addr > gusHw.synthTop || bytes > gusHw.synthTop - addr) {
+ return false;
+ }
+
+ for (uint32_t done = 0; done < bytes;) {
+ uint32_t n = bytes - done < UPLOAD_CHUNK ? bytes - done : UPLOAD_CHUNK;
+
+ for (uint32_t i = 0; i < n; i++) {
+ uint32_t at = done + i;
+ uint16_t s = (uint16_t)frames[wide ? at / 2 : at];
+
+ chunk[i] = (uint8_t)(wide && !(at & 1) ? s & BYTE_MASK : s >> BYTE_BITS);
+ }
+
+ gusPokeBlock(addr + done, chunk, n);
+ done += n;
+ }
+
+ return true;
+}
+
+
+// A voice address at a byte address (translated for 16-bit samples).
+// Interrupt time.
+static void voiceAddr(uint32_t voice, uint8_t reg, uint32_t addr, bool wide, bool lowFirst) {
+ gusVoiceAddr(voice, reg, wide ? gusAddr16(addr) : addr, lowFirst);
+}
diff --git a/src/libs/kpunch/audio/gus/gusSynth.h b/src/libs/kpunch/audio/gus/gusSynth.h
new file mode 100644
index 00000000..dac10608
--- /dev/null
+++ b/src/libs/kpunch/audio/gus/gusSynth.h
@@ -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.
+
+// gusSynth.h -- the Gravis UltraSound as a wavetable synthesizer
+//
+// The functions of an AUDIO_SYNTH_WAVETABLE driver (audioDrv.h), shared
+// by gus.drv (the synthesizer beside its own digital sound: the voices
+// and memory its ring leaves) and guswt.drv (the synthesizer alone, the
+// card beside another sound device: every voice and all the memory).
+// The GF1 plays 8- and 16-bit samples with loops and pans 16 ways; the
+// envelopes, the LFOs, filter and effects are left to music.lib (updates
+// of pitch and volume), so the flags are AUDIO_WT_8BIT and
+// AUDIO_WT_BANK_256K only.
+#ifndef GUS_SYNTH_H
+#define GUS_SYNTH_H
+
+#include "../audioDrv.h"
+
+bool gusSynthActive(uint32_t voice);
+void gusSynthClose(void);
+bool gusSynthOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
+void gusSynthRelease(uint32_t voice);
+void gusSynthStart(uint32_t voice, const AudioWtVoiceT *v);
+void gusSynthStop(uint32_t voice);
+void gusSynthUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what);
+bool gusSynthUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
+
+// The DVX.INI key for the active voices, and its default (both drivers)
+#define GUS_INI_VOICES "gusVoices"
+#define GUS_VOICES_DEFAULT 28
+
+#endif // GUS_SYNTH_H
diff --git a/src/libs/kpunch/audio/isa/isaCalc.c b/src/libs/kpunch/audio/isa/isaCalc.c
new file mode 100644
index 00000000..4c0e805a
--- /dev/null
+++ b/src/libs/kpunch/audio/isa/isaCalc.c
@@ -0,0 +1,54 @@
+// 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.
+
+// isaCalc.c -- ISA sound card arithmetic shared by the drivers; see
+// isaCalc.h
+
+#include "isaCalc.h"
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+uint32_t isaDmaBoundary(uint32_t channel);
+uint32_t isaDmaPlace(uint32_t phys, uint32_t allocBytes, uint32_t bufBytes, uint32_t boundary);
+
+
+uint32_t isaDmaBoundary(uint32_t channel) {
+ return channel >= ISA_DMA_FIRST_16 ? ISA_DMA_BOUNDARY_16 : ISA_DMA_BOUNDARY_8;
+}
+
+
+// At phys when the buffer fits there, else at the next boundary.
+uint32_t isaDmaPlace(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 ISA_NONE;
+ }
+
+ return start - phys;
+}
diff --git a/src/libs/kpunch/audio/isa/isaCalc.h b/src/libs/kpunch/audio/isa/isaCalc.h
new file mode 100644
index 00000000..ac6adf82
--- /dev/null
+++ b/src/libs/kpunch/audio/isa/isaCalc.h
@@ -0,0 +1,52 @@
+// 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.
+
+// isaCalc.h -- ISA sound card arithmetic shared by the drivers (no
+// hardware access, so the host tests run it)
+//
+// Where a DMA buffer goes in a block of conventional memory: an ISA DMA
+// transfer cannot cross a 64 KB boundary (byte channels 0-3) or a 128 KB
+// one (word channels 5-7).
+#ifndef ISA_CALC_H
+#define ISA_CALC_H
+
+#include
+
+// No such value (an offset that does not fit)
+#define ISA_NONE 0xFFFFFFFFU
+
+// Boundaries a DMA transfer must not cross
+#define ISA_DMA_BOUNDARY_8 0x10000U
+#define ISA_DMA_BOUNDARY_16 0x20000U
+
+// The first word-wide DMA channel and the channel count
+#define ISA_DMA_FIRST_16 4
+#define ISA_DMA_CHANNELS 8
+
+// The boundary of a DMA channel.
+uint32_t isaDmaBoundary(uint32_t channel);
+
+// Offset in a block at physical address phys, allocBytes long, of a
+// bufBytes buffer that does not cross boundary; ISA_NONE when none fits.
+uint32_t isaDmaPlace(uint32_t phys, uint32_t allocBytes, uint32_t bufBytes, uint32_t boundary);
+
+#endif // ISA_CALC_H
diff --git a/src/libs/kpunch/audio/isa/isaHw.c b/src/libs/kpunch/audio/isa/isaHw.c
new file mode 100644
index 00000000..b3456209
--- /dev/null
+++ b/src/libs/kpunch/audio/isa/isaHw.c
@@ -0,0 +1,288 @@
+// 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.
+
+// isaHw.c -- ISA sound card hardware shared by the drivers; see isaHw.h
+
+#include "isaHw.h"
+#include "audioDrv.h"
+
+#include
+#include
+#include
+
+// 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_PAGE_SHIFT 16
+#define DMA_PAGE_MASK16 0xFE
+
+// 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_SPURIOUS_MASTER 7
+#define VEC_MASTER_BASE 0x08
+#define VEC_SLAVE_BASE 0x70
+
+#define PARAGRAPH_BYTES 16
+#define PARAGRAPH_SHIFT 4
+#define WORD_BYTES 4
+#define BYTE_MASK 0xFF
+#define BYTE_BITS 8
+#define ALLOC_FACTOR 2 // twice over, so a boundary-free stretch always exists
+
+// Channel registers, by DMA channel number
+static const uint8_t sDmaAddrPort[ISA_DMA_CHANNELS] = { 0x00, 0x02, 0x04, 0x06, 0xC0, 0xC4, 0xC8, 0xCC };
+static const uint8_t sDmaCountPort[ISA_DMA_CHANNELS] = { 0x01, 0x03, 0x05, 0x07, 0xC2, 0xC6, 0xCA, 0xCE };
+static const uint8_t sDmaPagePort[ISA_DMA_CHANNELS] = { 0x87, 0x83, 0x81, 0x82, 0x8F, 0x8B, 0x89, 0x8A };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool isaDmaAlloc(IsaDmaT *dma, uint32_t channel, uint32_t bytes);
+void isaDmaFill(const IsaDmaT *dma, uint8_t value);
+void isaDmaFree(IsaDmaT *dma);
+uint32_t isaDmaPosition(const IsaDmaT *dma);
+void isaDmaProgram(const IsaDmaT *dma, uint32_t offset, uint32_t bytes, uint8_t mode);
+void isaDmaRead(const IsaDmaT *dma, uint32_t offset, uint32_t *words, uint32_t count);
+void isaDmaStop(const IsaDmaT *dma);
+void isaDmaWrite(const IsaDmaT *dma, uint32_t offset, const uint32_t *words, uint32_t count);
+void isaIrqEnd(const IsaIrqT *irq);
+bool isaIrqHook(IsaIrqT *irq, uint32_t number, void (*isr)(void));
+bool isaIrqSpurious(const IsaIrqT *irq);
+void isaIrqUnhook(IsaIrqT *irq);
+bool isaLock(bool lock);
+static uint32_t picMaskPort(uint32_t irq);
+static int picVector(uint32_t irq);
+
+
+bool isaDmaAlloc(IsaDmaT *dma, uint32_t channel, uint32_t bytes) {
+ uint32_t allocBytes = bytes * ALLOC_FACTOR;
+ int selector = 0;
+ int segment = __dpmi_allocate_dos_memory((int)((allocBytes + PARAGRAPH_BYTES - 1) / PARAGRAPH_BYTES), &selector);
+
+ dma->selector = -1;
+
+ if (segment < 0) {
+ return false;
+ }
+
+ uint32_t blockPhys = (uint32_t)segment << PARAGRAPH_SHIFT;
+ uint32_t offset = isaDmaPlace(blockPhys, allocBytes, bytes, isaDmaBoundary(channel));
+
+ if (offset == ISA_NONE) {
+ __dpmi_free_dos_memory(selector);
+ return false;
+ }
+
+ dma->channel = channel;
+ dma->bytes = bytes;
+ dma->offset = offset;
+ dma->phys = blockPhys + offset;
+ dma->selector = selector;
+ return true;
+}
+
+
+void isaDmaFill(const IsaDmaT *dma, uint8_t value) {
+ _farsetsel((unsigned short)dma->selector);
+
+ for (uint32_t i = 0; i < dma->bytes; i++) {
+ _farnspokeb(dma->offset + i, value);
+ }
+}
+
+
+void isaDmaFree(IsaDmaT *dma) {
+ if (dma->selector >= 0) {
+ __dpmi_free_dos_memory(dma->selector);
+ dma->selector = -1;
+ }
+}
+
+
+// The count register holds the units left minus one (bytes on 0-3,
+// words on 5-7).
+uint32_t isaDmaPosition(const IsaDmaT *dma) {
+ uint32_t ch = dma->channel;
+ bool wide = ch >= ISA_DMA_FIRST_16;
+ uint32_t units = wide ? dma->bytes / 2 : dma->bytes;
+ uint32_t count;
+
+ outportb(wide ? DMA16_FLIPFLOP : DMA8_FLIPFLOP, 0);
+ count = inportb(sDmaCountPort[ch]);
+ count |= (uint32_t)inportb(sDmaCountPort[ch]) << BYTE_BITS;
+
+ uint32_t done = units - 1 - count;
+
+ return wide ? done * 2 : done;
+}
+
+
+void isaDmaProgram(const IsaDmaT *dma, uint32_t offset, uint32_t bytes, uint8_t mode) {
+ uint32_t ch = dma->channel;
+ bool wide = ch >= ISA_DMA_FIRST_16;
+ uint32_t sel = ch & DMA_CHANNEL_BITS;
+ uint32_t phys = dma->phys + 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);
+}
+
+
+void isaDmaRead(const IsaDmaT *dma, uint32_t offset, uint32_t *words, uint32_t count) {
+ _farsetsel((unsigned short)dma->selector);
+
+ for (uint32_t i = 0; i < count; i++) {
+ words[i] = _farnspeekl(dma->offset + offset + i * WORD_BYTES);
+ }
+}
+
+
+void isaDmaStop(const IsaDmaT *dma) {
+ bool wide = dma->channel >= ISA_DMA_FIRST_16;
+
+ outportb(wide ? DMA16_MASK : DMA8_MASK, (uint8_t)(DMA_MASK_SET | (dma->channel & DMA_CHANNEL_BITS)));
+}
+
+
+void isaDmaWrite(const IsaDmaT *dma, uint32_t offset, const uint32_t *words, uint32_t count) {
+ _farsetsel((unsigned short)dma->selector);
+
+ for (uint32_t i = 0; i < count; i++) {
+ _farnspokel(dma->offset + offset + i * WORD_BYTES, words[i]);
+ }
+}
+
+
+void isaIrqEnd(const IsaIrqT *irq) {
+ if (irq->irq >= IRQ_SLAVE_FIRST) {
+ outportb(PIC2_CMD, PIC_EOI);
+ }
+
+ outportb(PIC1_CMD, PIC_EOI);
+}
+
+
+bool isaIrqHook(IsaIrqT *irq, uint32_t number, void (*isr)(void)) {
+ irq->irq = number == ISA_IRQ_CASCADE ? ISA_IRQ_AT_REDIRECT : number;
+ irq->hooked = false;
+
+ int vector = picVector(irq->irq);
+ uint32_t port = picMaskPort(irq->irq);
+ uint8_t bit = (uint8_t)(1 << (irq->irq & (IRQ_SLAVE_FIRST - 1)));
+
+ _go32_dpmi_get_protected_mode_interrupt_vector(vector, &irq->oldVector);
+ irq->newVector.pm_offset = (unsigned long)isr;
+ irq->newVector.pm_selector = _go32_my_cs();
+
+ if (_go32_dpmi_allocate_iret_wrapper(&irq->newVector) != 0) {
+ return false;
+ }
+
+ uint32_t state = platformIrqSave();
+
+ _go32_dpmi_set_protected_mode_interrupt_vector(vector, &irq->newVector);
+ irq->savedPicBit = inportb(port) & bit;
+ outportb(port, inportb(port) & ~bit);
+
+ if (irq->irq >= IRQ_SLAVE_FIRST) {
+ outportb(PIC1_DATA, inportb(PIC1_DATA) & ~(1 << ISA_IRQ_CASCADE));
+ }
+
+ irq->hooked = true;
+ platformIrqRestore(state);
+ return true;
+}
+
+
+bool isaIrqSpurious(const IsaIrqT *irq) {
+ if (irq->irq != IRQ_SPURIOUS_MASTER) {
+ return false;
+ }
+
+ outportb(PIC1_CMD, PIC_READ_ISR);
+ return !(inportb(PIC1_CMD) & (1 << IRQ_SPURIOUS_MASTER));
+}
+
+
+void isaIrqUnhook(IsaIrqT *irq) {
+ if (!irq->hooked) {
+ return;
+ }
+
+ uint32_t port = picMaskPort(irq->irq);
+ uint32_t state = platformIrqSave();
+
+ outportb(port, inportb(port) | irq->savedPicBit);
+ _go32_dpmi_set_protected_mode_interrupt_vector(picVector(irq->irq), &irq->oldVector);
+ platformIrqRestore(state);
+ _go32_dpmi_free_iret_wrapper(&irq->newVector);
+ irq->hooked = false;
+}
+
+
+bool isaLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)isaDmaPosition, 0 },
+ { (const void *)isaDmaRead, 0 },
+ { (const void *)isaDmaWrite, 0 },
+ { (const void *)isaIrqEnd, 0 },
+ { (const void *)isaIrqSpurious, 0 },
+ { sDmaCountPort, sizeof(sDmaCountPort) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+// The PIC's mask register for an IRQ.
+static uint32_t picMaskPort(uint32_t irq) {
+ return irq < IRQ_SLAVE_FIRST ? PIC1_DATA : PIC2_DATA;
+}
+
+
+// The interrupt vector an IRQ arrives on.
+static int picVector(uint32_t irq) {
+ return irq < IRQ_SLAVE_FIRST ? VEC_MASTER_BASE + (int)irq : VEC_SLAVE_BASE + (int)(irq - IRQ_SLAVE_FIRST);
+}
diff --git a/src/libs/kpunch/audio/isa/isaHw.h b/src/libs/kpunch/audio/isa/isaHw.h
new file mode 100644
index 00000000..bd41602d
--- /dev/null
+++ b/src/libs/kpunch/audio/isa/isaHw.h
@@ -0,0 +1,114 @@
+// 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.
+
+// isaHw.h -- ISA sound card hardware shared by the drivers: a DMA buffer
+// in conventional memory, the 8237 DMA controllers, and a card's
+// interrupt at the 8259s
+//
+// Each driver links its own copy (audio/isa, beside the drivers), keeps
+// its own IsaDmaT and IsaIrqT, and pins the copy with isaLock before its
+// interrupt can run. The functions marked "interrupt time" keep to the
+// rules in audioDrv.h.
+#ifndef ISA_HW_H
+#define ISA_HW_H
+
+#include "isaCalc.h"
+
+#include
+#include
+#include
+
+// 8237 transfer modes (single transfer): playing moves memory to the
+// device, recording the device to memory
+#define ISA_DMA_AUTO_PLAY 0x58 // auto-init: the buffer over and over
+#define ISA_DMA_ONCE_PLAY 0x48 // once
+#define ISA_DMA_AUTO_RECORD 0x54 // auto-init, into the buffer
+#define ISA_DMA_ONCE_RECORD 0x44 // once, into the buffer
+
+// The cascade IRQ, which an AT delivers as IRQ 9
+#define ISA_IRQ_CASCADE 2
+#define ISA_IRQ_AT_REDIRECT 9
+#define ISA_IRQ_COUNT 16
+
+// A DMA buffer in conventional memory and the channel that plays it
+typedef struct {
+ uint32_t channel;
+ uint32_t bytes; // the buffer's length
+ uint32_t offset; // of the buffer in the DOS block
+ uint32_t phys; // its physical address
+ int32_t selector; // the DOS block (-1: none)
+} IsaDmaT;
+
+// A card's interrupt, hooked
+typedef struct {
+ _go32_dpmi_seginfo oldVector;
+ _go32_dpmi_seginfo newVector;
+ uint32_t irq;
+ uint8_t savedPicBit; // the IRQ was masked before
+ bool hooked;
+} IsaIrqT;
+
+// A buffer of bytes for channel, placed so no transfer crosses a DMA
+// boundary. False (nothing allocated) when there is no room.
+bool isaDmaAlloc(IsaDmaT *dma, uint32_t channel, uint32_t bytes);
+
+// Every byte of the buffer set to value.
+void isaDmaFill(const IsaDmaT *dma, uint8_t value);
+
+void isaDmaFree(IsaDmaT *dma);
+
+// Bytes of the buffer the controller has moved in the current pass of an
+// auto-init transfer over the whole buffer. Interrupt time.
+uint32_t isaDmaPosition(const IsaDmaT *dma);
+
+// Copy count 32-bit words out of the buffer from offset (what the card
+// recorded). Interrupt time.
+void isaDmaRead(const IsaDmaT *dma, uint32_t offset, uint32_t *words, uint32_t count);
+
+// Program the channel for bytes of the buffer from offset, in mode, and
+// unmask it.
+void isaDmaProgram(const IsaDmaT *dma, uint32_t offset, uint32_t bytes, uint8_t mode);
+
+// Mask the channel.
+void isaDmaStop(const IsaDmaT *dma);
+
+// Copy count 32-bit words into the buffer from offset. Interrupt time.
+void isaDmaWrite(const IsaDmaT *dma, uint32_t offset, const uint32_t *words, uint32_t count);
+
+// End of interrupt at the PICs. Interrupt time.
+void isaIrqEnd(const IsaIrqT *irq);
+
+// Point irq (2 meaning 9) at isr and unmask it. False when the wrapper
+// could not be made.
+bool isaIrqHook(IsaIrqT *irq, uint32_t number, void (*isr)(void));
+
+// True for a spurious IRQ 7 (the PIC has it not in service): no
+// acknowledgement at all. Interrupt time.
+bool isaIrqSpurious(const IsaIrqT *irq);
+
+// The vector and the mask back as they were.
+void isaIrqUnhook(IsaIrqT *irq);
+
+// Pin or release this file's interrupt-time code and tables.
+bool isaLock(bool lock);
+
+#endif // ISA_HW_H
diff --git a/src/libs/kpunch/audio/math/drvMath.c b/src/libs/kpunch/audio/math/drvMath.c
new file mode 100644
index 00000000..3159dab8
--- /dev/null
+++ b/src/libs/kpunch/audio/math/drvMath.c
@@ -0,0 +1,61 @@
+// The MIT License (MIT)
+//
+// Copyright (C) 2026 Scott Duensing
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+
+// drvMath.c -- arithmetic the synthesizer drivers share (see drvMath.h)
+
+#include "drvMath.h"
+#include "../audioDrv.h"
+
+#define CENTS_SEMI 100
+#define SEMITONES 13
+
+// 2^(k / 12) in 16.16, k = 0..12
+static const uint32_t sSemitones[SEMITONES] = {
+ 65536, 69433, 73562, 77936, 82570, 87480, 92682, 98193, 104032, 110218, 116772, 123715, 131072
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool drvMathLock(bool lock);
+uint32_t drvPow2(int32_t cents);
+
+
+bool drvMathLock(bool lock) {
+ const AudioLockItemT items[] = {
+ { (const void *)drvPow2, 0 },
+ { sSemitones, sizeof(sSemitones) }
+ };
+
+ return audioLockSpan(items, (int32_t)(sizeof(items) / sizeof(items[0])), lock);
+}
+
+
+uint32_t drvPow2(int32_t cents) {
+ int32_t octave = cents >= 0 ? cents / DRV_CENTS_OCTAVE : -((DRV_CENTS_OCTAVE - 1 - cents) / DRV_CENTS_OCTAVE);
+ int32_t rest = cents - octave * DRV_CENTS_OCTAVE;
+ int32_t semi = rest / CENTS_SEMI;
+ uint32_t mul = sSemitones[semi] + (sSemitones[semi + 1] - sSemitones[semi]) * (uint32_t)(rest % CENTS_SEMI) / CENTS_SEMI;
+
+ return octave >= 0 ? mul << octave : mul >> -octave;
+}
diff --git a/src/libs/kpunch/audio/math/drvMath.h b/src/libs/kpunch/audio/math/drvMath.h
new file mode 100644
index 00000000..1717e1c4
--- /dev/null
+++ b/src/libs/kpunch/audio/math/drvMath.h
@@ -0,0 +1,45 @@
+// 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.
+
+// drvMath.h -- arithmetic the synthesizer drivers share
+//
+// Integer only, so it runs at interrupt time (the drivers pin it with
+// drvMathLock): no division by 64-bit values, no floating point.
+#ifndef DRV_MATH_H
+#define DRV_MATH_H
+
+#include
+#include
+
+#define DRV_POW2_SHIFT 16
+#define DRV_POW2_ONE (1 << DRV_POW2_SHIFT) // 1.0 in drvPow2's 16.16
+#define DRV_CENTS_OCTAVE 1200
+
+// 2^(cents / 1200) in 16.16: the octave as a shift, the semitone from a
+// table, the cents between two semitones by a straight line (within 0.1
+// percent). cents from -16 to +15 octaves.
+uint32_t drvPow2(int32_t cents);
+
+// Pin or release drvPow2 and its table.
+bool drvMathLock(bool lock);
+
+#endif // DRV_MATH_H
diff --git a/src/libs/kpunch/dvxnet/netDos.c b/src/libs/kpunch/dvxnet/netDos.c
index 4fa0b5f2..4e98cfe6 100644
--- a/src/libs/kpunch/dvxnet/netDos.c
+++ b/src/libs/kpunch/dvxnet/netDos.c
@@ -33,9 +33,11 @@
// one reach this fcntl).
//
// Shell: the constructor registers an idle handler (the stack runs while
-// DVX is idle) and an app cleanup hook (sockets an ending app left open
-// are closed); the destructor stops the stack so the packet driver never
-// calls into an unloaded module.
+// DVX is idle), an app cleanup hook (sockets an ending app left open are
+// closed), and a suspend hook (the stack stops while DVX steps aside for
+// a DOS program, which may use the packet driver itself; it starts again
+// at its next use); the destructor stops the stack so the packet driver
+// never calls into an unloaded module.
#include "netInt.h"
#include "dvxPlat.h"
@@ -64,6 +66,7 @@ static int fsextHandler(__FSEXT_Fnumber fn, int *rv, va_list args);
static void idlePoll(void *ctx);
static void netDosInit(void) __attribute__((constructor));
static void netDosShutdown(void) __attribute__((destructor));
+static void netSuspend(bool suspending);
// fcntl for every module loaded after this one (see the file comment).
@@ -157,11 +160,13 @@ static void netDosInit(void) {
sLibcFcntl = (libc == fcntl) ? NULL : libc;
shellRegisterIdle(idlePoll, NULL);
shellRegisterAppCleanup(netCloseAppSockets);
+ platformSuspendHookAdd(netSuspend);
}
static void netDosShutdown(void) {
netStop();
+ platformSuspendHookRemove(netSuspend);
shellUnregisterAppCleanup(netCloseAppSockets);
shellUnregisterIdle(idlePoll, NULL);
}
@@ -204,3 +209,13 @@ int32_t netFdSocket(int32_t fd) {
return sFdSock[fd] - 1;
}
+
+
+// The packet driver is let go while a DOS program runs (it may be a
+// network program itself); nothing to do after: the stack starts again
+// when a program next uses it.
+static void netSuspend(bool suspending) {
+ if (suspending && netStackRunning()) {
+ netStop();
+ }
+}
diff --git a/src/libs/kpunch/dvxshell/Makefile b/src/libs/kpunch/dvxshell/Makefile
index 967aa215..a9856409 100644
--- a/src/libs/kpunch/dvxshell/Makefile
+++ b/src/libs/kpunch/dvxshell/Makefile
@@ -36,7 +36,7 @@ THEMEDIR = $(SHAREDDIR)/themes
WPAPERDIR = $(SHAREDDIR)/wpaper
KEYSDIR = $(SYSTEMDIR)/KEYS
-SRCS = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellRun.c pmModel.c pmModelBas.c
+SRCS = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellRun.c shellSaver.c pmModel.c pmModelBas.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGETDIR = $(LIBSDIR)/kpunch/dvxshell
TARGET = $(TARGETDIR)/dvxshell.lib
@@ -50,7 +50,7 @@ WPAPERS = $(WPAPERDIR)/blueglow.jpg $(WPAPERDIR)/swoop.jpg $(WPAPERDIR)/triangle
# dvxpkg keygen): what DVX trusts as its own (pkgTrust.h)
KEYS = $(patsubst ../../../../config/keys/%,$(KEYSDIR)/%,$(wildcard ../../../../config/keys/*.KEY))
-all: $(TARGET) $(TARGETDIR)/dvxshell.dep $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI $(THEMES) $(WPAPERS) $(KEYS)
+all: $(TARGET) $(TARGETDIR)/dvxshell.dep $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI $(SYSTEMDIR)/DOSPROG.BMP $(THEMES) $(WPAPERS) $(KEYS)
$(TARGETDIR)/dvxshell.dep: dvxshell.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@
@@ -67,6 +67,10 @@ $(SYSTEMDIR)/DVX.INI: ../../../../config/dvx.ini | $(SYSTEMDIR)
$(SYSTEMDIR)/ASSOC.INI: ../../../../config/ASSOC.INI | $(SYSTEMDIR)
sed 's/$$/\r/' $< > $@
+# The icon of a DOS program that has none of its own (shellProg.c)
+$(SYSTEMDIR)/DOSPROG.BMP: ../../../../config/DOSPROG.BMP | $(SYSTEMDIR)
+ cp $< $@
+
$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
@@ -101,4 +105,4 @@ $(KEYSDIR)/%.KEY: ../../../../config/keys/%.KEY | $(KEYSDIR)
clean:
rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/dvxshell.dep $(TARGETDIR)
- rm -rf $(WPAPERDIR) $(THEMEDIR) $(KEYSDIR) $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI
+ rm -rf $(WPAPERDIR) $(THEMEDIR) $(KEYSDIR) $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI $(SYSTEMDIR)/DOSPROG.BMP
diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs
index 48bd06d2..6ba8eddd 100644
--- a/src/libs/kpunch/dvxshell/dvxshell.dhs
+++ b/src/libs/kpunch/dvxshell/dvxshell.dhs
@@ -105,6 +105,7 @@ LoadedE is transient (only during shellLoadApp before the entry point is called)
.link shell.info System Information
.link shell.programs Installed Programs
.link shell.run Running Programs and Documents
+.link shell.saver The Screen Saver
.link shell.assoc File Associations
.link shell.basic BASIC Entry Points
.link shell.sysbasic BASIC System Settings
@@ -226,6 +227,7 @@ Per-app slot in the shell's app table. Slot 0 is reserved for the shell itself;
dxeCtx DxeAppContextT * Heap-allocated context (address stable across realloc).
taskCodeDepth int32_t * Per task id: SHELL_APP_CALL calls into this app still open on that task's stack.
killPending bool A force kill was deferred; shellReapApps finishes it.
+ faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill).
.endtable
.topic shell.lifecycle
@@ -304,7 +306,7 @@ Gracefully shut down a single app. Calls the app's shutdownFn (if present), dest
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
.endcode
-Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery. The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.
+Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.
Cleanup order: callbacks and windows first (removes them from the compositor), then the task (frees its stack; any dispatch frames still open on it are dropped from the context's dispatch depth), then the widget classes and system-wide hotkeys the app registered (its shutdownFn never got the chance to unregister them), then the DXE handle (unmaps the code). Closing the DXE before destroying windows would cause callbacks into unmapped code.
@@ -518,6 +520,8 @@ When the serial library is loaded, the shell feeds keyboard and mouse event timi
.index shellUnregisterIdle
.index shellRegisterAppCleanup
.index shellUnregisterAppCleanup
+.index shellRegisterAppKill
+.index shellUnregisterAppKill
.index ShellAppCleanupFnT
.index shellDesktopUpdate
.index shellDesktopChangeCount
@@ -582,6 +586,15 @@ void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);
Register a function the shell calls with an app's id whenever that app ends -- normally, force-killed, or after a crash -- once its windows are gone, next to the shell's own release of the app's widget classes and hotkeys. A library uses it to release what the app left open with it: the networking library closes the app's sockets this way. For library code that stays loaded; registering the same function again has no effect, and unregistering one that is not registered does nothing.
+.h2 shellRegisterAppKill
+
+.code
+void shellRegisterAppKill(ShellAppCleanupFnT fn);
+void shellUnregisterAppKill(ShellAppCleanupFnT fn);
+.endcode
+
+Register a function the shell calls with an app's id when the app is ended by force (End Task, a screen saver that would not close, DVX exiting) while none of the app's code is running and its windows are still there -- before the windows go and before the cleanup hooks. A library ends what it runs for the app the way the app's own end would: the BASIC runtime (basrt) frees a standalone program's VM, forms, screen, and module this way, closing its serial ports and its forms' windows, so End Task on a BASIC program leaks nothing. Not called for an app that faulted (its state cannot be trusted), nor for one whose kill had to wait until its code returned (End Task from inside the app's own dialog: its windows went at once). Registration rules as for cleanup hooks.
+
.h2 shellDesktopUpdate
.code
@@ -729,6 +742,13 @@ shellProgramListFree frees a list from shellProgramScan (NULL is fine). shellIsP
.index shellSplitCommand
.index shellResolveArgs
.index shellMinimizeApp
+.index shellRunDos
+.index shellRunDosRequest
+.index shellIsDosProgram
+.index SHELL_RUN_DOS
+.index SHELL_DOS_FILTER
+.index SHELL_DOS_ICON
+.index DOS programs
.index Run dialog
.index Run Minimized
@@ -744,7 +764,19 @@ One way for every program to start another (Program Manager's items and Run...,
int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args, const char *folder);
.endcode
-Start program with args. A .app is loaded with shellLoadAppWithArgs, its arguments first passed through shellResolveArgs(folder, ...). Anything else is a document: it is looked for in folder first (when relative), must exist, and is opened with shellAssocCommand's program and arguments. Problems are shown to the user in an error box (a missing document, a document type with no association; load errors come from shellLoadApp as usual). Returns the new application id, or -1.
+Start program with args. A .app is loaded with shellLoadAppWithArgs, its arguments first passed through shellResolveArgs(folder, ...). A DOS program (shellIsDosProgram: .EXE, .COM, or .BAT, any case) is set to run with shellRunDos (folder is where it runs; looked for in folder first when relative). Anything else is a document: it is looked for in folder first (when relative), must exist, and is opened with shellAssocCommand's program and arguments; when that program is a DOS program, the document goes to it by its full short name (it runs in its own folder) through shellRunDos. Problems are shown to the user in an error box (a missing document, a document type with no association; load errors come from shellLoadApp as usual). Returns the new application id, SHELL_RUN_DOS (0) for a DOS program set to run, or -1.
+
+.h2 shellRunDos / shellRunDosRequest
+
+.code
+void shellRunDos(const char *program, const char *args, const char *folder, bool pause);
+void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId);
+bool shellIsDosProgram(const char *path);
+.endcode
+
+Run a DOS program with DVX stepped aside (libdvx's dvxRunDos, which takes the same program, args, folder, and pause). shellRunDos only records the request (a newer one replaces one still waiting): closing the other programs pumps the event loop, which is not safe from an event handler. The main loop calls shellRunDosRequest after shellRunExitRequest; keepAppId is the desktop, which stays. A program named with a folder must exist ("Cannot find X.", titled Run DOS Program); a bare name is looked for on the PATH by DOS. When programs other than the desktop are running, a question names them (by their program names, shellProgramInfo) and says each asks about unsaved work as it closes; No, or a program that keeps a window (shellCloseAllApps), cancels the run. A program that cannot be started is reported after DVX is back ("DVX could not run X." with the reason). The Task Manager is the shell's own window and is not asked. Program Manager's DOS items call shellRunDos with their "Wait for a key" setting (PmItemT.pause, PROGMAN.INI itemN.pause); shellRunProgram uses pause false.
+
+SHELL_DOS_FILTER ("DOS programs (*.exe;*.com;*.bat)") is the file dialogs' filter for them (Run..., Program Manager's Properties, Control Panel's File Types). A Program Manager item for a DOS program with no icon file of its own shows SHELL_DOS_ICON (SYSTEM\DOSPROG.BMP, from config/DOSPROG.BMP).
.h2 shellRunCommand
@@ -787,6 +819,59 @@ void shellMinimizeIdle(AppContextT *ctx);
Run Minimized. shellMinimizeApp minimizes every visible window of appId (an id from shellRunProgram) at once and again on each of the next SHELL_MINIMIZE_FRAMES (60) idle frames, because a program with a main loop opens its windows from its own task after it has started. Asking again for the same program starts its frames over; nothing happens for an id that is not running, and a program that ends is forgotten (an app-cleanup hook). shellMinimizeIdle is the per-frame step, called by the shell's idle dispatch. Program Manager's Run Minimized items and BASIC's ShellMinimizeApp use it.
+.topic shell.saver
+.title The Screen Saver
+.toc 1 The Screen Saver
+.index shellSaverSet
+.index shellSaverStart
+.index shellSaverStep
+.index shellSaverRunning
+.index shellSaverName
+.index shellSaverWait
+.index shellSaverFromPrefs
+.index shellSaverToPrefs
+.index SHELL_SAVER_ARGS
+.index Screen saver
+
+.h1 The Screen Saver
+
+Header: dvxshell/shellSaver.h
+
+A screen saver is a program (.app, C or BASIC) in a SAVERS folder (DvxKindSaversE: SHARED\SAVERS, which DVX and add-ons fill, and the user's SAVERS); Program Manager does not list it. After the wait with no key or mouse input (dvxIdleMs) the shell starts the chosen one with SHELL_SAVER_ARGS ("/S"). It takes the whole screen (dvxExclusiveBegin; BASIC SCREEN and FULLSCREEN ON) and draws until its window is closed; it needs no input handling of its own, since the input that wakes the screen goes to it (full-screen mode), never to the program underneath. Input SHELL_SAVER_GRACE_MS (1,000 ms) or more after it started asks each of its windows to close (dvxRequestWindowClose, as Alt+F4 does); one still running SHELL_SAVER_END_MS (3,000 ms) later is ended (shellForceKillApp). The log says "Shell: screen saver X started" and "... ended".
+
+The saver does not start while a dialog box is open (ctx->modalWindow) or a program has the full screen (ctx->exclusive.owner: a game played with a joystick gives no key or mouse input). After a saver ends (or fails to start: missing, or not a program, logged and never shown in a message box) the next wait counts from then.
+
+DVX.INI [desktop] saver is the saver's name in SAVERS ("" or missing: none) and saverWait the minutes (SHELL_SAVER_WAIT_MIN to SHELL_SAVER_WAIT_MAX, 1 to 60; SHELL_SAVER_WAIT_DEFAULT, 10, when missing). The shell reads them at start; Control Panel's Desktop page sets them live and stores them with OK.
+
+.code
+void shellSaverSet(const char *name, int32_t minutes);
+const char *shellSaverName(void);
+int32_t shellSaverWait(void);
+bool shellSaverStart(void);
+bool shellSaverRunning(void);
+void shellSaverStep(AppContextT *ctx);
+void shellSaverFromPrefs(PrefsHandleT *h);
+void shellSaverToPrefs(PrefsHandleT *h);
+.endcode
+
+.table
+ Function What it does
+ -------- ------------
+ shellSaverSet Choose the saver (a file in SAVERS is kept by its name there, platformKindNameOf; any other path as it is; "" or NULL: none) and the wait, clamped to the range; at once
+ shellSaverName / Wait The settings in force
+ shellSaverStart Start the saver now (Control Panel's Test); only recorded, the next shellSaverStep starts it. False with none chosen or one running
+ shellSaverRunning True while a saver runs
+ shellSaverStep The main loop's step (shell top level): starts the saver when due, ends a running one at input
+ shellSaverFromPrefs Read [desktop] saver and saverWait (start, Control Panel's Cancel)
+ shellSaverToPrefs Write them (Control Panel's OK and its Cancel snapshot)
+.endtable
+
+BASIC: SysScreenSaver$, SysScreenSaverWait, SysSetScreenSaver, SysScreenSaverStart, SysScreenSaverRunning, SysScreenSaverStore, SysScreenSaverLoad, and SysIdleSeconds (system.bas; see BASIC System Settings).
+
+.h2 Writing a screen saver
+
+A C saver is a main-loop program that opens a window, takes the full screen with dvxExclusiveBegin, and draws (dvxExclusivePresent8, paced with gameFrameWait) until the window's onClose sets a flag; then it destroys the window and returns (src/savers/kpunch/starfld). A BASIC saver is a program with no form: SCREEN 13, FULLSCREEN ON, and a loop drawing with FRAMEWAIT (src/savers/kpunch/blank, marquee, mystify); closing its screen window ends it. Put the .app in SHARED\SAVERS (an add-on package's kind saver) or the user's SAVERS; the name Control Panel lists is the program's (its "name" resource, a BASIC project's Name).
+
.topic shell.assoc
.title File Associations
.toc 1 File Associations
@@ -850,6 +935,8 @@ shellAssocSet adds or replaces an association (programPath may carry ,arguments)
.toc 1 BASIC Entry Points
.index ShellRun
.index ShellRunProgram
+.index ShellRunDos
+.index ShellIsDosProgram
.index ShellRunCommand
.index ShellRunDialog
.index ShellMinimizeApp
@@ -914,12 +1001,13 @@ The shell's services for DVX BASIC programs -- everything Program Manager does,
int32_t ShellRun(const char *program, const char *args);
int32_t ShellRunProgram(const char *program, const char *args, const char *folder);
int32_t ShellRunCommand(const char *command);
+void ShellRunDos(const char *program, const char *args, const char *folder, int32_t pause);
const char *ShellRunDialog(const char *history);
void ShellMinimizeApp(int32_t appId);
int32_t ShellOpenDocument(const char *path);
.endcode
-ShellRunProgram is shellRunProgram (a document opens through its association; file names in args, and a relative document, are looked for in folder; "" for none), returning the new app id or 0. ShellRun is ShellRunProgram without a folder, True when it started; the two are one launch path. ShellRunCommand is shellRunCommand (id or 0). ShellRunDialog splits history at line ends (CHR$(10), a CR before it is dropped, empty lines skipped) into the list shellRunDialog shows, and returns the command chosen or "" when cancelled. ShellMinimizeApp is shellMinimizeApp. ShellOpenDocument is shellAssocOpenDocument (it shows nothing when there is no association; ShellRunProgram does).
+ShellRunProgram is shellRunProgram (a document opens through its association; file names in args, and a relative document, are looked for in folder; "" for none), returning the new app id, -1 (shell.bas SHELL_RUN_DOS) for a DOS program set to run, or 0. ShellRun is ShellRunProgram without a folder, True when it started or a DOS program will run; the two are one launch path. ShellRunCommand is shellRunCommand (id, -1, or 0). ShellRunDos is shellRunDos (pause True or False): the DOS program runs once the calling procedure has returned and the programs besides the desktop, the caller too, have closed. ShellRunDialog splits history at line ends (CHR$(10), a CR before it is dropped, empty lines skipped) into the list shellRunDialog shows, and returns the command chosen or "" when cancelled. ShellMinimizeApp is shellMinimizeApp. ShellOpenDocument is shellAssocOpenDocument (it shows nothing when there is no association; ShellRunProgram does).
.h2 Installed programs
@@ -931,9 +1019,10 @@ void ShellProgramFree(int32_t list);
const char *ShellProgramName(const char *path);
const char *ShellProgramDescription(const char *path);
int32_t ShellIsProgram(const char *path);
+int32_t ShellIsDosProgram(const char *path);
.endcode
-ShellProgramScan is shellProgramScan of folder ("" for SHELL_PROG_DIR) without skipPath ("" for none), paths only (readInfo false), so it never opens a program; it returns a list handle, 0 when nothing was found. ShellProgramCount and ShellProgramPath read it (0 / "" for a bad list or index); ShellProgramFree frees it. ShellProgramName and ShellProgramDescription are shellProgramInfo's name and description, read one program at a time so a long list can be filled with DoEvents in between (the C progress callback). ShellIsProgram is shellIsProgram.
+ShellProgramScan is shellProgramScan of folder ("" for SHELL_PROG_DIR) without skipPath ("" for none), paths only (readInfo false), so it never opens a program; it returns a list handle, 0 when nothing was found. ShellProgramCount and ShellProgramPath read it (0 / "" for a bad list or index); ShellProgramFree frees it. ShellProgramName and ShellProgramDescription are shellProgramInfo's name and description, read one program at a time so a long list can be filled with DoEvents in between (the C progress callback). ShellIsProgram is shellIsProgram, ShellIsDosProgram shellIsDosProgram.
.h2 File types
@@ -1100,13 +1189,17 @@ BASIC (shell.bas, DECLARE LIBRARY "dvxshell"): ShellGroupsLoad / Save / Free / S
.index SysScreenshot
.index SysMemoryTotalKb
.index SysMemoryFreeKb
+.index SysScreenSaver$
+.index SysSetScreenSaver
+.index SysScreenSaverStart
+.index SysIdleSeconds
.index SYS_VIDEO_DEFAULT_WIDTH
.h1 BASIC System Settings
Source: dvxshell/shellSysBas.c; BASIC declarations: src/include/basic/system.bas (DECLARE LIBRARY "dvxshell"); prototypes in shellApp.h.
-What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper, text smoothing, and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).
+What Control Panel changes, for DVX BASIC programs: the video mode, the system colors and themes, the wallpaper, the screen saver, text smoothing, and the mouse, each applied at once to shellContext(). Only BASIC-marshalable types; True (-1) / False (0); colors are BASIC RGB() values (&HBBGGRR, converted with wgtBasicColor); lists are 0-based. Arguments named prefs are prefs.bas handles, resolved with prefsBasHandle (dvxPrefs.h); 0 or a closed handle makes a Store or Load do nothing (a Load returns False).
.code
int32_t SysVideoModeCount(void);
@@ -1131,6 +1224,14 @@ void SysSetWallpaperMode(int32_t mode);
int32_t SysWallpaperMode(void);
void SysWallpaperStore(int32_t prefs);
int32_t SysWallpaperLoad(int32_t prefs);
+const char *SysScreenSaver(void);
+int32_t SysScreenSaverWait(void);
+void SysSetScreenSaver(const char *fileName, int32_t minutes);
+int32_t SysScreenSaverStart(void);
+int32_t SysScreenSaverRunning(void);
+void SysScreenSaverStore(int32_t prefs);
+void SysScreenSaverLoad(int32_t prefs);
+int32_t SysIdleSeconds(void);
void SysSetTextSmoothing(int32_t on);
int32_t SysTextSmoothing(void);
void SysFontsStore(int32_t prefs);
@@ -1174,6 +1275,12 @@ int32_t SysMemoryFreeKb(void);
SysSetWallpaperMode dvxSetWallpaperMode with the value of prefsWallpaperChoices[mode]; other modes are ignored
SysWallpaper / Mode ctx->wallpaperPath; the prefsWallpaperChoices index of ctx->wallpaperMode
SysWallpaperStore / Load dvxWallpaperToPrefs / dvxWallpaperFromPrefs (Load: False when the image could not be loaded)
+ SysScreenSaver / Wait shellSaverName / shellSaverWait
+ SysSetScreenSaver shellSaverSet
+ SysScreenSaverStart shellSaverStart (True when it will start)
+ SysScreenSaverRunning shellSaverRunning
+ SysScreenSaverStore / Load shellSaverToPrefs / shellSaverFromPrefs
+ SysIdleSeconds dvxIdleMs / 1000
SysSetTextSmoothing dvxSetFontSmoothing (nonzero is on)
SysTextSmoothing ctx->fontSmoothing (the setting; text is smoothed only above 8 bits per pixel)
SysFontsStore / Load dvxFontsToPrefs / dvxFontsFromPrefs
diff --git a/src/libs/kpunch/dvxshell/pmModel.c b/src/libs/kpunch/dvxshell/pmModel.c
index 201780e5..cf6d870b 100644
--- a/src/libs/kpunch/dvxshell/pmModel.c
+++ b/src/libs/kpunch/dvxshell/pmModel.c
@@ -61,6 +61,7 @@
#define INI_ITEM_ICON PM_FIELD_ICON
#define INI_ITEM_KEY PM_FIELD_KEY
#define INI_ITEM_MIN PM_FIELD_MINIMIZED
+#define INI_ITEM_PAUSE PM_FIELD_PAUSE
#define INI_ITEM_COL PM_FIELD_COL
#define INI_ITEM_ROW PM_FIELD_ROW
#define INI_KEY_MAX 32 // "item256.minimized" plus room
@@ -224,6 +225,8 @@ static void loadItem(PmModelT *m, int32_t g, PrefsHandleT *h, const char *sectio
itemPathsNative(&item);
itemKey(key, sizeof(key), n, INI_ITEM_MIN);
item.runMinimized = prefsGetBool(h, section, key, false);
+ itemKey(key, sizeof(key), n, INI_ITEM_PAUSE);
+ item.pause = prefsGetBool(h, section, key, false);
itemKey(key, sizeof(key), n, INI_ITEM_COL);
item.col = prefsGetInt(h, section, key, PM_CELL_NONE);
itemKey(key, sizeof(key), n, INI_ITEM_ROW);
@@ -529,6 +532,8 @@ bool pmItemGetField(const PmItemT *item, const char *field, char *out, int32_t o
dvxAccelFormat(item->hotkeyKey, item->hotkeyMods, out, outSize);
} else if (strcasecmp(field, INI_ITEM_MIN) == 0) {
snprintf(out, (size_t)outSize, "%d", item->runMinimized ? 1 : 0);
+ } else if (strcasecmp(field, INI_ITEM_PAUSE) == 0) {
+ snprintf(out, (size_t)outSize, "%d", item->pause ? 1 : 0);
} else if (strcasecmp(field, INI_ITEM_COL) == 0) {
snprintf(out, (size_t)outSize, "%" PRId32, item->col);
} else if (strcasecmp(field, INI_ITEM_ROW) == 0) {
@@ -642,6 +647,8 @@ bool pmItemSetField(PmItemT *item, const char *field, const char *value) {
if (strcasecmp(field, INI_ITEM_MIN) == 0) {
item->runMinimized = number != 0;
+ } else if (strcasecmp(field, INI_ITEM_PAUSE) == 0) {
+ item->pause = number != 0;
} else if (strcasecmp(field, INI_ITEM_COL) == 0) {
item->col = number < 0 ? PM_CELL_NONE : number;
} else if (strcasecmp(field, INI_ITEM_ROW) == 0) {
@@ -869,6 +876,11 @@ static void saveGroup(PrefsHandleT *h, const PmGroupT *g, int32_t n) {
prefsSetBool(h, section, key, true);
}
+ if (item->pause) {
+ itemKey(key, sizeof(key), k, INI_ITEM_PAUSE);
+ prefsSetBool(h, section, key, true);
+ }
+
if (item->col >= 0) {
itemKey(key, sizeof(key), k, INI_ITEM_COL);
prefsSetInt(h, section, key, item->col);
diff --git a/src/libs/kpunch/dvxshell/pmModel.h b/src/libs/kpunch/dvxshell/pmModel.h
index 89e8f14e..1d09142a 100644
--- a/src/libs/kpunch/dvxshell/pmModel.h
+++ b/src/libs/kpunch/dvxshell/pmModel.h
@@ -35,7 +35,7 @@
// [groups] count
// [groupN] name, count, itemM.name, itemM.program, itemM.args,
// itemM.folder, itemM.icon, itemM.key, itemM.minimized,
-// itemM.col, itemM.row (N and M count from 1)
+// itemM.pause, itemM.col, itemM.row (N and M count from 1)
// [run] historyN (newest first)
//
// Items sit in grid cells (col, row). With Auto Arrange the cells follow
@@ -70,6 +70,7 @@
#define PM_FIELD_ICON "icon"
#define PM_FIELD_KEY "key"
#define PM_FIELD_MINIMIZED "minimized"
+#define PM_FIELD_PAUSE "pause"
#define PM_FIELD_COL "col"
#define PM_FIELD_ROW "row"
#define PM_FIELD_DESCRIPTION "description"
@@ -98,12 +99,13 @@ typedef struct {
int32_t col;
int32_t row;
char name[PM_NAME_MAX];
- char program[DVX_MAX_PATH]; // .app, or a document opened by association
+ char program[DVX_MAX_PATH]; // .app, a DOS program, or a document opened by association
char args[PM_ARGS_MAX];
- char folder[DVX_MAX_PATH]; // working folder for relative file arguments
+ char folder[DVX_MAX_PATH]; // working folder for relative file arguments (a DOS program runs in it)
char iconPath[DVX_MAX_PATH]; // empty = the program's own icon
char description[SHELL_PROG_DESC_MAX]; // tooltip (Installed Programs: from the program)
bool runMinimized;
+ bool pause; // a DOS program: wait for a key when it ends
bool iconTried;
} PmItemT;
diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c
index b0557ed8..f3150b0a 100644
--- a/src/libs/kpunch/dvxshell/shellApp.c
+++ b/src/libs/kpunch/dvxshell/shellApp.c
@@ -74,6 +74,7 @@ void (*shellCtrlEscFn)(AppContextT *ctx) = NULL;
// Library cleanup hooks run for every ending app (stb_ds array).
static ShellAppCleanupFnT *sCleanupFns = NULL;
+static ShellAppCleanupFnT *sKillFns = NULL;
// ============================================================
// Prototypes
@@ -99,7 +100,9 @@ static void forceKillNow(AppContextT *ctx, ShellAppT *app);
static void killAppTask(ShellAppT *app);
static bool makeTempPath(const char *origPath, int32_t id, char *out, int32_t outSize);
static int32_t refuseSecondCopy(AppContextT *ctx, const char *path);
+static void registerHook(ShellAppCleanupFnT **hooks, ShellAppCleanupFnT fn);
static void releaseAppResources(ShellAppT *app);
+static void runKillFns(const ShellAppT *app);
bool shellActivateApp(AppContextT *ctx, int32_t appId);
void shellAppCallEnter(ShellAppT *app);
void shellAppCallLeave(ShellAppT *app);
@@ -123,11 +126,14 @@ void shellOpenTaskManager(AppContextT *ctx);
void shellReapApp(AppContextT *ctx, ShellAppT *app);
bool shellReapApps(AppContextT *ctx);
void shellRegisterAppCleanup(ShellAppCleanupFnT fn);
+void shellRegisterAppKill(ShellAppCleanupFnT fn);
int32_t shellRunningAppCount(void);
int32_t shellRunningAppId(int32_t index);
void shellTerminateAllApps(AppContextT *ctx);
void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);
+void shellUnregisterAppKill(ShellAppCleanupFnT fn);
void shellUserPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize);
+static void unregisterHook(ShellAppCleanupFnT **hooks, ShellAppCleanupFnT fn);
// Find the first free slot, starting at 1 (slot 0 is the shell).
@@ -535,6 +541,18 @@ static int32_t refuseSecondCopy(AppContextT *ctx, const char *path) {
}
+// Add fn to a hook list once.
+static void registerHook(ShellAppCleanupFnT **hooks, ShellAppCleanupFnT fn) {
+ for (int32_t i = 0; i < arrlen(*hooks); i++) {
+ if ((*hooks)[i] == fn) {
+ return;
+ }
+ }
+
+ arrput(*hooks, fn);
+}
+
+
// Shared teardown tail for both reap and force-kill: release the widget
// classes, system-wide hotkeys and timer-service timers the app registered
// and its hung-callback bookkeeping (a force kill never
@@ -569,6 +587,20 @@ static void releaseAppResources(ShellAppT *app) {
}
+// The libraries' kill hooks for an app ended by force whose windows are
+// still there: not one that faulted (its state cannot be trusted), nor
+// one whose kill waited for its code to return (its windows went then).
+static void runKillFns(const ShellAppT *app) {
+ if (app->faulted || app->killPending) {
+ return;
+ }
+
+ for (int32_t i = 0; i < arrlen(sKillFns); i++) {
+ sKillFns[i](app->appId);
+ }
+}
+
+
// The app's topmost visible window (appTopWindow) is restored if
// minimized, else raised and focused. False when it has none.
bool shellActivateApp(AppContextT *ctx, int32_t appId) {
@@ -677,6 +709,7 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) {
}
if (!appCodeMayBeLive(ctx, app)) {
+ runKillFns(app);
forceKillNow(ctx, app);
return;
}
@@ -1093,13 +1126,12 @@ bool shellReapApps(AppContextT *ctx) {
void shellRegisterAppCleanup(ShellAppCleanupFnT fn) {
- for (int32_t i = 0; i < arrlen(sCleanupFns); i++) {
- if (sCleanupFns[i] == fn) {
- return;
- }
- }
+ registerHook(&sCleanupFns, fn);
+}
- arrput(sCleanupFns, fn);
+
+void shellRegisterAppKill(ShellAppCleanupFnT fn) {
+ registerHook(&sKillFns, fn);
}
@@ -1137,6 +1169,7 @@ int32_t shellRunningAppId(int32_t index) {
void shellTerminateAllApps(AppContextT *ctx) {
for (int32_t i = 1; i < arrlen(sApps); i++) {
if (sApps[i]->state != AppStateFreeE) {
+ runKillFns(sApps[i]);
forceKillNow(ctx, sApps[i]);
}
}
@@ -1144,14 +1177,25 @@ void shellTerminateAllApps(AppContextT *ctx) {
void shellUnregisterAppCleanup(ShellAppCleanupFnT fn) {
- for (int32_t i = arrlen(sCleanupFns) - 1; i >= 0; i--) {
- if (sCleanupFns[i] == fn) {
- arrdel(sCleanupFns, i);
- }
- }
+ unregisterHook(&sCleanupFns, fn);
+}
+
+
+void shellUnregisterAppKill(ShellAppCleanupFnT fn) {
+ unregisterHook(&sKillFns, fn);
}
void shellUserPath(const DxeAppContextT *ctx, const char *filename, char *outPath, int32_t outSize) {
platformPathJoin(outPath, outSize, ctx->userDir, filename);
}
+
+
+// Remove fn from a hook list (nothing when it is not there).
+static void unregisterHook(ShellAppCleanupFnT **hooks, ShellAppCleanupFnT fn) {
+ for (int32_t i = arrlen(*hooks) - 1; i >= 0; i--) {
+ if ((*hooks)[i] == fn) {
+ arrdel(*hooks, i);
+ }
+ }
+}
diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h
index e7b8d4cb..e1ed6f46 100644
--- a/src/libs/kpunch/dvxshell/shellApp.h
+++ b/src/libs/kpunch/dvxshell/shellApp.h
@@ -155,6 +155,7 @@ typedef struct {
DxeAppContextT *dxeCtx; // heap-allocated; address stable across sApps realloc
int32_t *taskCodeDepth; // stb_ds by task id: shell calls into this app in progress on that task's stack (see SHELL_APP_CALL)
bool killPending; // force kill requested while its code was live; released by shellReapApps
+ bool faulted; // it crashed: its kill runs no kill hooks (shellRegisterAppKill)
} ShellAppT;
// Bracket every direct shell call into an app's code (appMain of a
@@ -402,6 +403,19 @@ void shellRegisterAppCleanup(ShellAppCleanupFnT fn);
// Remove a cleanup hook (no-op if absent).
void shellUnregisterAppCleanup(ShellAppCleanupFnT fn);
+// Called with the app's id when it is ended by force (End Task, a screen
+// saver that would not close, DVX exiting) while none of its code runs
+// and its windows are still there, before they go: a library ends what it
+// runs for the app as the app's own end would (the BASIC runtime frees
+// the program's VM, forms, and screen this way). Not for an app that
+// faulted, nor for one whose kill had to wait for its code to return (its
+// windows went then). For library code that stays loaded; idempotent per
+// function.
+void shellRegisterAppKill(ShellAppCleanupFnT fn);
+
+// Remove a kill hook (no-op if absent).
+void shellUnregisterAppKill(ShellAppCleanupFnT fn);
+
// ============================================================
// Ctrl+Esc handler (set by taskmgr library)
// ============================================================
@@ -606,6 +620,7 @@ void SysFontsLoad(int32_t prefs);
void SysFontsStore(int32_t prefs);
int32_t SysFontWidth(void);
int32_t SysGetColor(int32_t id);
+int32_t SysIdleSeconds(void);
int32_t SysLoadTheme(const char *path);
int32_t SysMemoryFreeKb(void);
int32_t SysMemoryTotalKb(void);
@@ -621,11 +636,18 @@ int32_t SysMouseX(void);
int32_t SysMouseY(void);
void SysResetColors(void);
int32_t SysSaveTheme(const char *path);
+const char *SysScreenSaver(void);
+void SysScreenSaverLoad(int32_t prefs);
+int32_t SysScreenSaverRunning(void);
+int32_t SysScreenSaverStart(void);
+void SysScreenSaverStore(int32_t prefs);
+int32_t SysScreenSaverWait(void);
int32_t SysScreenshot(const char *path);
int32_t SysSendKey(int32_t keyCode, int32_t shift);
void SysSetColor(int32_t id, int32_t rgb);
void SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep);
void SysSetMousePos(int32_t x, int32_t y);
+void SysSetScreenSaver(const char *fileName, int32_t minutes);
void SysSetTextSmoothing(int32_t on);
int32_t SysSetVideoMode(int32_t width, int32_t height, int32_t bpp);
int32_t SysSetWallpaper(const char *path);
diff --git a/src/libs/kpunch/dvxshell/shellBas.c b/src/libs/kpunch/dvxshell/shellBas.c
index d311f7e3..c681d051 100644
--- a/src/libs/kpunch/dvxshell/shellBas.c
+++ b/src/libs/kpunch/dvxshell/shellBas.c
@@ -51,6 +51,7 @@
#define ACCEL_TEXT_MAX 32
#define ARG_NONE (-1) // argWalk: only count
#define PRIORITY_NONE (-1) // ShellPriority without a task of its own
+#define BAS_RUN_DOS (-1) // shell.bas SHELL_RUN_DOS: a DOS program will run
// A program list from ShellProgramScan and the program it belongs to.
typedef struct {
@@ -88,6 +89,7 @@ static int32_t sLastHotkeyId = 0;
static void appEnded(int32_t appId);
static int32_t argWalk(const char *command, int32_t index, char *out, int32_t outSize);
+static int32_t basRunResult(int32_t id);
static const ShellAppT *currentApp(void);
static int32_t currentAppId(void);
int32_t HotkeyAdd(const char *keyText);
@@ -122,6 +124,7 @@ const char *ShellCommandArgs(const char *command);
const char *ShellCommandProgram(const char *command);
void ShellExitDvx(void);
int32_t ShellInstanceCount(void);
+int32_t ShellIsDosProgram(const char *path);
int32_t ShellIsProgram(const char *path);
const char *ShellItemIcon(const char *iconPath, const char *path);
void ShellLog(const char *text);
@@ -138,6 +141,7 @@ const char *ShellProgramsFolder(void);
int32_t ShellRun(const char *program, const char *args);
int32_t ShellRunCommand(const char *command);
const char *ShellRunDialog(const char *history);
+void ShellRunDos(const char *program, const char *args, const char *folder, int32_t pause);
int32_t ShellRunningActivate(int32_t appId);
int32_t ShellRunningClose(int32_t appId);
int32_t ShellRunningCount(void);
@@ -196,6 +200,16 @@ static int32_t argWalk(const char *command, int32_t index, char *out, int32_t ou
// The app whose code is running, or NULL.
+// shellRunProgram's result as BASIC has it: the id, BAS_RUN_DOS, or 0.
+static int32_t basRunResult(int32_t id) {
+ if (id == SHELL_RUN_DOS) {
+ return BAS_RUN_DOS;
+ }
+
+ return id > 0 ? id : 0;
+}
+
+
static const ShellAppT *currentApp(void) {
return shellGetApp(currentAppId());
}
@@ -579,6 +593,11 @@ int32_t ShellInstanceCount(void) {
}
+int32_t ShellIsDosProgram(const char *path) {
+ return shellIsDosProgram(path) ? BAS_TRUE : BAS_FALSE;
+}
+
+
int32_t ShellIsProgram(const char *path) {
return shellIsProgram(path) ? BAS_TRUE : BAS_FALSE;
}
@@ -710,16 +729,14 @@ const char *ShellProgramsFolder(void) {
// Start a program with arguments ("" for none), the way ShellRunProgram
-// does. True when it started.
+// does. True when it started, or a DOS program will run.
int32_t ShellRun(const char *program, const char *args) {
- return ShellRunProgram(program, args, "") > 0 ? BAS_TRUE : BAS_FALSE;
+ return ShellRunProgram(program, args, "") != 0 ? BAS_TRUE : BAS_FALSE;
}
int32_t ShellRunCommand(const char *command) {
- int32_t id = shellRunCommand(shellContext(), command);
-
- return id > 0 ? id : 0;
+ return basRunResult(shellRunCommand(shellContext(), command));
}
@@ -761,6 +778,13 @@ const char *ShellRunDialog(const char *history) {
}
+// The program runs once the calling procedure has returned and the other
+// programs have closed (shellRunDos).
+void ShellRunDos(const char *program, const char *args, const char *folder, int32_t pause) {
+ shellRunDos(program, args, folder, pause != BAS_FALSE);
+}
+
+
// Bring a program to the front (Task Manager's Switch To). True when it
// has a window to show.
int32_t ShellRunningActivate(int32_t appId) {
@@ -839,9 +863,7 @@ const char *ShellRunningPath(int32_t appId) {
// args found in folder ("" for none). The new program's id, or 0 (DVX
// has said why).
int32_t ShellRunProgram(const char *program, const char *args, const char *folder) {
- int32_t id = shellRunProgram(shellContext(), program, args, folder);
-
- return id > 0 ? id : 0;
+ return basRunResult(shellRunProgram(shellContext(), program, args, folder));
}
diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c
index 41e08125..a2d6d3da 100644
--- a/src/libs/kpunch/dvxshell/shellMain.c
+++ b/src/libs/kpunch/dvxshell/shellMain.c
@@ -67,6 +67,7 @@
#include "shellApp.h"
#include "shellInf.h"
#include "shellRun.h"
+#include "shellSaver.h"
#include "dvxDlg.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
@@ -831,6 +832,12 @@ int shellMain(int argc, char *argv[]) {
} else if (sCtx.wallpaperPath[0]) {
dvxLog("Preferences: loaded wallpaper %s (%s)", sCtx.wallpaperPath, prefsWallpaperChoices[sCtx.wallpaperMode].iniName);
}
+
+ shellSaverFromPrefs(sPrefs);
+
+ if (shellSaverName()[0]) {
+ dvxLog("Preferences: screen saver %s after %ld minutes", shellSaverName(), (long)shellSaverWait());
+ }
}
// Safe mode is explained (and can be kept) before anything else opens.
@@ -928,6 +935,7 @@ int shellMain(int argc, char *argv[]) {
if (app) {
char msg[SHELL_MSG_MAX];
snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
+ app->faulted = true;
shellForceKillApp(&sCtx, app);
sCtx.currentAppId = 0;
@@ -951,6 +959,8 @@ int shellMain(int argc, char *argv[]) {
shellFrame(&sCtx);
shellRunCloseRequests(&sCtx);
shellRunExitRequest(&sCtx, sDesktopId);
+ shellRunDosRequest(&sCtx, sDesktopId);
+ shellSaverStep(&sCtx);
// Without a desktop DVX ends with the last program.
if (sDesktopId == 0 && shellRunningAppCount() == 0) {
diff --git a/src/libs/kpunch/dvxshell/shellProg.c b/src/libs/kpunch/dvxshell/shellProg.c
index 052cbcc8..ed88f456 100644
--- a/src/libs/kpunch/dvxshell/shellProg.c
+++ b/src/libs/kpunch/dvxshell/shellProg.c
@@ -44,6 +44,7 @@
static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize);
static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **list);
+bool shellIsDosProgram(const char *path);
bool shellIsProgram(const char *path);
uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch);
bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize);
@@ -54,8 +55,8 @@ int32_t shellProgramScan(const char *dir, const char *skipPath, bool rea
// The item icon rule: the picture file iconPath (NULL or "" for none),
-// else path's own picture, else for a document the icon of the program
-// its file type opens in. chosen (chosenSize bytes, may be NULL) gets
+// else path's own picture, else for a DOS program SHELL_DOS_ICON, else
+// for a document the icon of the program its file type opens in. chosen (chosenSize bytes, may be NULL) gets
// the file the icon came from ("" for none).
static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize) {
char opener[DVX_MAX_PATH];
@@ -68,6 +69,11 @@ static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char
icon = shellProgramIcon(ctx, from, edge, outPitch);
}
+ if (!icon && shellIsDosProgram(path)) {
+ from = SHELL_DOS_ICON;
+ icon = shellProgramIcon(ctx, from, edge, outPitch);
+ }
+
if (!icon && path && !shellIsProgram(path) && shellAssocCommand(path, opener, sizeof(opener), args, sizeof(args))) {
from = opener;
icon = shellProgramIcon(ctx, from, edge, outPitch);
@@ -127,6 +133,20 @@ static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellP
}
+// SHELL_DOS_FILTER lists the same extensions.
+bool shellIsDosProgram(const char *path) {
+ static const char *const exts[] = { ".exe", ".com", ".bat" };
+
+ for (int32_t i = 0; path && i < (int32_t)DVX_ARRAY_LEN(exts); i++) {
+ if (dvxHasExt(path, exts[i])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
bool shellIsProgram(const char *path) {
return path && dvxHasExt(path, SHELL_PROG_EXT);
}
diff --git a/src/libs/kpunch/dvxshell/shellProg.h b/src/libs/kpunch/dvxshell/shellProg.h
index 1346c023..6ac284d6 100644
--- a/src/libs/kpunch/dvxshell/shellProg.h
+++ b/src/libs/kpunch/dvxshell/shellProg.h
@@ -41,6 +41,14 @@
// File extension of a program.
#define SHELL_PROG_EXT ".app"
+// The file dialogs' filter for DOS programs (the extensions
+// shellIsDosProgram takes)
+#define SHELL_DOS_FILTER "DOS programs (*.exe;*.com;*.bat)"
+
+// The icon of a DOS program with no picture of its own (an item's icon
+// file can still name another)
+#define SHELL_DOS_ICON DVX_SYSTEM_DIR DVX_PATH_SEP "DOSPROG.BMP"
+
// ShellProgramT.description capacity.
#define SHELL_PROG_DESC_MAX 128
@@ -61,6 +69,10 @@ typedef void (*ShellProgressFnT)(void *ctx);
// True when path names a program (.app extension, any case).
bool shellIsProgram(const char *path);
+// True when path names a DOS program: .EXE, .COM, or .BAT, any case
+// (shellRunDos runs one with DVX stepped aside).
+bool shellIsDosProgram(const char *path);
+
// The icon an item for path shows, edge x edge pixels in display format:
// the picture file iconPath (NULL or "" for none), else path's own
// picture (a program's icon32, or path itself when it is a picture
diff --git a/src/libs/kpunch/dvxshell/shellRun.c b/src/libs/kpunch/dvxshell/shellRun.c
index 8215d3ee..2afca658 100644
--- a/src/libs/kpunch/dvxshell/shellRun.c
+++ b/src/libs/kpunch/dvxshell/shellRun.c
@@ -34,6 +34,7 @@
#include "comboBox/comboBox.h"
#include "label/label.h"
+#include
#include
#include
#include
@@ -43,6 +44,11 @@
#define RUN_TITLE "Run"
#define RUN_DLG_W 320
#define RUN_ROW_GAP 4
+#define RUN_DOS_TITLE "Run DOS Program"
+#define RUN_DOS_ASK "Running %s closes these programs first:\n%s\n\nEach one asks about unsaved work as it closes. Continue?"
+#define RUN_DOS_LINE "\n %s"
+#define RUN_DOS_FAILED "DVX could not run %s.\n\n%s"
+#define RUN_NOT_FOUND "Cannot find %s."
typedef struct {
WidgetT *command;
@@ -56,11 +62,21 @@ typedef struct {
int32_t framesLeft;
} RunMinimizeT;
+// A DOS program waiting for the main loop (shellRunDos)
+typedef struct {
+ bool pending;
+ bool pause;
+ char program[DVX_MAX_PATH];
+ char args[SHELL_ARGS_MAX];
+ char folder[DVX_MAX_PATH];
+} RunDosT;
+
// ============================================================
// Module state
// ============================================================
static RunMinimizeT *sMinimize = NULL; // stb_ds
+static RunDosT sDosRun;
// ============================================================
// Prototypes
@@ -76,6 +92,8 @@ void shellMinimizeIdle(AppContextT *ctx);
void shellResolveArgs(const char *folder, const char *args, char *out, int32_t outSize);
int32_t shellRunCommand(AppContextT *ctx, const char *command);
bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t historyCount, char *outCommand, int32_t outSize);
+void shellRunDos(const char *program, const char *args, const char *folder, bool pause);
+void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId);
int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args, const char *folder);
bool shellSplitCommand(const char *command, char *program, int32_t programSize, char *args, int32_t argsSize);
@@ -121,6 +139,7 @@ static void runDlgBrowse(WidgetT *w) {
static const char quoteFmt[] = "\"%s\"";
FileFilterT filters[] = {
{ "Programs (*.app)" },
+ { SHELL_DOS_FILTER },
{ "All Files (*.*)" }
};
char path[DVX_MAX_PATH];
@@ -249,8 +268,83 @@ bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t histor
}
-// A document (anything but a .app) must exist; its associated program
-// gets it through the association's argument template.
+// Only recorded (closing the other programs pumps the event loop, which
+// is not safe from the caller's event handler); a request made while one
+// waits replaces it.
+void shellRunDos(const char *program, const char *args, const char *folder, bool pause) {
+ if (!program || !program[0]) {
+ return;
+ }
+
+ snprintf(sDosRun.program, sizeof(sDosRun.program), "%s", program);
+ snprintf(sDosRun.args, sizeof(sDosRun.args), "%s", args ? args : "");
+ snprintf(sDosRun.folder, sizeof(sDosRun.folder), "%s", folder ? folder : "");
+ sDosRun.pause = pause;
+ sDosRun.pending = true;
+}
+
+
+// A program named with a folder must exist (a bare name is looked for on
+// the PATH by the command interpreter). The programs running besides the
+// desktop are named and asked to close first; No, or one that keeps its
+// window, cancels the run.
+void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId) {
+ char names[SHELL_MSG_MAX];
+ char msg[SHELL_MSG_MAX];
+ int32_t pos = 0;
+ int32_t count = 0;
+ struct stat st;
+
+ if (!sDosRun.pending) {
+ return;
+ }
+
+ RunDosT run = sDosRun;
+ const char *base = platformPathBaseName(run.program);
+
+ sDosRun.pending = false;
+ names[0] = '\0';
+
+ if (base != run.program && stat(run.program, &st) != 0) {
+ snprintf(msg, sizeof(msg), RUN_NOT_FOUND, run.program);
+ dvxErrorBox(ctx, RUN_DOS_TITLE, msg);
+ return;
+ }
+
+ for (int32_t i = 0; i < shellRunningAppCount(); i++) {
+ int32_t id = shellRunningAppId(i);
+ const ShellAppT *app = shellGetApp(id);
+ ShellProgramT info;
+
+ if (id == keepAppId || !app) {
+ continue;
+ }
+
+ shellProgramInfo(app->path, &info);
+ pos = dvxStrAppendf(names, sizeof(names), pos, RUN_DOS_LINE, info.name[0] ? info.name : app->name);
+ count++;
+ }
+
+ if (count > 0) {
+ snprintf(msg, sizeof(msg), RUN_DOS_ASK, base, names);
+
+ if (dvxMessageBox(ctx, RUN_DOS_TITLE, msg, MB_YESNO | MB_ICONQUESTION) != ID_YES || !shellCloseAllApps(ctx, keepAppId)) {
+ dvxLog("Shell: %s not run: programs kept running", run.program);
+ return;
+ }
+ }
+
+ if (dvxRunDos(ctx, run.program, run.args, run.folder, run.pause) < 0) {
+ snprintf(msg, sizeof(msg), RUN_DOS_FAILED, base, strerror(errno));
+ dvxErrorBox(ctx, RUN_DOS_TITLE, msg);
+ }
+}
+
+
+// A document (anything but a .app or a DOS program) must exist; its
+// associated program gets it through the association's argument template.
+// A DOS program, or a document whose associated program is one, is run
+// with DVX stepped aside once the main loop gets to it (shellRunDos).
int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args, const char *folder) {
char msg[SHELL_MSG_MAX];
char target[DVX_MAX_PATH];
@@ -270,6 +364,11 @@ int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args,
struct stat st;
const char *doc = fileInFolder(folder, program, target, sizeof(target)) ? target : program;
+ if (shellIsDosProgram(program)) {
+ shellRunDos(doc, args, folder, false);
+ return SHELL_RUN_DOS;
+ }
+
if (stat(doc, &st) != 0 || S_ISDIR(st.st_mode)) {
snprintf(msg, sizeof(msg), "Cannot find %s.", doc);
dvxErrorBox(ctx, RUN_TITLE, msg);
@@ -282,6 +381,20 @@ int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args,
return -1;
}
+ // A DOS program runs in its own folder and knows only short names: the
+ // document goes to it by its full short name.
+ if (shellIsDosProgram(docProgram)) {
+ char full[DVX_MAX_PATH];
+ char shortDoc[DVX_MAX_PATH];
+
+ if (platformPathAbsolute(doc, full, sizeof(full)) && platformShortPath(full, shortDoc, sizeof(shortDoc))) {
+ shellAssocCommand(shortDoc, docProgram, sizeof(docProgram), docArgs, sizeof(docArgs));
+ }
+
+ shellRunDos(docProgram, docArgs, NULL, false);
+ return SHELL_RUN_DOS;
+ }
+
return shellLoadAppWithArgs(ctx, docProgram, docArgs);
}
diff --git a/src/libs/kpunch/dvxshell/shellRun.h b/src/libs/kpunch/dvxshell/shellRun.h
index 5fb900b8..3d567a2e 100644
--- a/src/libs/kpunch/dvxshell/shellRun.h
+++ b/src/libs/kpunch/dvxshell/shellRun.h
@@ -67,12 +67,31 @@ int32_t shellRunCommand(AppContextT *ctx, const char *command);
// history.
bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t historyCount, char *outCommand, int32_t outSize);
+// What shellRunProgram returns when it set a DOS program to run: it has no
+// application id.
+#define SHELL_RUN_DOS 0
+
// Start program with args (file names in them resolved against folder,
// see shellResolveArgs; folder may be NULL). A program that is not a
-// .app is opened as a document with its associated program. Problems
-// are shown to the user. Returns the new application id, or -1.
+// .app is opened as a document with its associated program; a DOS
+// program (shellIsDosProgram), or a document whose associated program is
+// one, is set to run with shellRunDos (folder is where it runs).
+// Problems are shown to the user. Returns the new application id,
+// SHELL_RUN_DOS, or -1.
int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args, const char *folder);
+// Run a DOS program with DVX stepped aside (dvxRunDos: program, args,
+// folder, and pause as it takes them). Only recorded: the main loop
+// carries it out at its top level (shellRunDosRequest), where the
+// programs running besides the desktop are named and asked to close
+// first (No, or one that keeps its window, cancels the run). A request
+// made while one waits replaces it.
+void shellRunDos(const char *program, const char *args, const char *folder, bool pause);
+
+// Main-loop step: carry out a waiting shellRunDos; keepAppId is the
+// desktop, which stays. Pumps the event loop: shell top level only.
+void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId);
+
// Split a command line into the program and its arguments. False when
// there is no program.
bool shellSplitCommand(const char *command, char *program, int32_t programSize, char *args, int32_t argsSize);
diff --git a/src/libs/kpunch/dvxshell/shellSaver.c b/src/libs/kpunch/dvxshell/shellSaver.c
new file mode 100644
index 00000000..7f5405d5
--- /dev/null
+++ b/src/libs/kpunch/dvxshell/shellSaver.c
@@ -0,0 +1,231 @@
+// 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.
+
+// shellSaver.c -- The screen saver (see shellSaver.h)
+
+#include "shellSaver.h"
+#include "shellApp.h"
+#include "dvxPlat.h"
+
+#include
+#include
+#include
+
+#define SAVER_INI_NAME "saver"
+#define SAVER_INI_WAIT "saverWait"
+#define MS_PER_MINUTE 60000
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static uint32_t msSince(PlatformTicksT then);
+static void saverAsk(AppContextT *ctx);
+static void saverBegin(AppContextT *ctx);
+static void saverEnded(int32_t appId);
+static void saverWatch(AppContextT *ctx);
+void shellSaverFromPrefs(PrefsHandleT *h);
+const char *shellSaverName(void);
+bool shellSaverRunning(void);
+void shellSaverSet(const char *name, int32_t minutes);
+bool shellSaverStart(void);
+void shellSaverStep(AppContextT *ctx);
+void shellSaverToPrefs(PrefsHandleT *h);
+int32_t shellSaverWait(void);
+
+// ============================================================
+// Module state
+// ============================================================
+
+static char sName[DVX_MAX_PATH];
+static int32_t sWait = SHELL_SAVER_WAIT_DEFAULT;
+static int32_t sAppId = 0; // the saver running (0: none)
+static PlatformTicksT sStarted = 0;
+static bool sAsked = false; // its windows were asked to close
+static bool sEnding = false; // it did not close and is being ended
+static PlatformTicksT sAskedAt = 0;
+static PlatformTicksT sEnded = 0; // the last start (or try): the wait counts from it too
+static bool sStartWanted = false; // shellSaverStart
+
+
+static uint32_t msSince(PlatformTicksT then) {
+ return PLATFORM_TICKS_TO_MS(platformClock() - then);
+}
+
+
+// Close the saver's windows the way Alt+F4 would; the dispatch depth
+// keeps the window list as it is while it is walked.
+static void saverAsk(AppContextT *ctx) {
+ sAsked = true;
+ sAskedAt = platformClock();
+ ctx->dispatchDepth++;
+
+ for (int32_t i = 0; i < ctx->stack.count; i++) {
+ WindowT *win = ctx->stack.windows[i];
+
+ if (win->appId == sAppId && !win->destroyPending) {
+ dvxRequestWindowClose(ctx, win);
+ }
+ }
+
+ ctx->dispatchDepth--;
+}
+
+
+// A saver that is missing or does not start is tried again a wait later,
+// without a message (nobody is there to read it).
+static void saverBegin(AppContextT *ctx) {
+ char found[DVX_MAX_PATH];
+ struct stat st;
+
+ sEnded = platformClock();
+
+ const char *path = platformKindFind(found, sizeof(found), DvxKindSaversE, sName) ? found : sName;
+
+ if (stat(path, &st) != 0) {
+ dvxLog("Shell: screen saver %s not found", sName);
+ return;
+ }
+
+ shellRegisterAppCleanup(saverEnded);
+
+ int32_t appId = shellLoadAppWithArgs(ctx, path, SHELL_SAVER_ARGS);
+
+ if (appId < 1) {
+ dvxLog("Shell: screen saver %s did not start", sName);
+ return;
+ }
+
+ sAppId = appId;
+ sStarted = platformClock();
+ sAsked = false;
+ sEnding = false;
+ dvxLog("Shell: screen saver %s started", sName);
+}
+
+
+static void saverEnded(int32_t appId) {
+ if (appId == sAppId) {
+ dvxLog("Shell: screen saver %s ended", sName);
+ sAppId = 0;
+ sEnded = platformClock();
+ }
+}
+
+
+// Input since the grace ended (dvxIdleMs no longer than the time past
+// it) asks the saver to close; one still there SHELL_SAVER_END_MS later
+// is ended (saverEnded, perhaps a frame or two later, says so).
+static void saverWatch(AppContextT *ctx) {
+ if (!sAsked) {
+ uint32_t running = msSince(sStarted);
+
+ if (running >= SHELL_SAVER_GRACE_MS && dvxIdleMs(ctx) <= running - SHELL_SAVER_GRACE_MS) {
+ saverAsk(ctx);
+ }
+
+ return;
+ }
+
+ if (sEnding || msSince(sAskedAt) < SHELL_SAVER_END_MS) {
+ return;
+ }
+
+ dvxLog("Shell: screen saver %s did not close; ending it", sName);
+ sEnding = true;
+ shellForceKillApp(ctx, shellGetApp(sAppId));
+}
+
+
+void shellSaverFromPrefs(PrefsHandleT *h) {
+ shellSaverSet(prefsGetString(h, DVX_DESKTOP_SECTION, SAVER_INI_NAME, ""), prefsGetInt(h, DVX_DESKTOP_SECTION, SAVER_INI_WAIT, SHELL_SAVER_WAIT_DEFAULT));
+}
+
+
+const char *shellSaverName(void) {
+ return sName;
+}
+
+
+bool shellSaverRunning(void) {
+ return sAppId > 0;
+}
+
+
+// A file in a SAVERS folder is kept by its name there, so the setting
+// still works when DVX or the user folder moves.
+void shellSaverSet(const char *name, int32_t minutes) {
+ char kindName[DVX_MAX_PATH];
+
+ if (!name) {
+ name = "";
+ }
+
+ snprintf(sName, sizeof(sName), "%s", platformKindNameOf(DvxKindSaversE, name, kindName, sizeof(kindName)) ? kindName : name);
+
+ if (minutes < SHELL_SAVER_WAIT_MIN) {
+ minutes = SHELL_SAVER_WAIT_MIN;
+ }
+
+ if (minutes > SHELL_SAVER_WAIT_MAX) {
+ minutes = SHELL_SAVER_WAIT_MAX;
+ }
+
+ sWait = minutes;
+}
+
+
+bool shellSaverStart(void) {
+ sStartWanted = sName[0] && sAppId == 0;
+ return sStartWanted;
+}
+
+
+// Not while a dialog box is open or a program has the full screen (a
+// game played with a joystick gives no key or mouse input). A start
+// asked for then is dropped.
+void shellSaverStep(AppContextT *ctx) {
+ if (sAppId > 0) {
+ saverWatch(ctx);
+ return;
+ }
+
+ uint32_t waitMs = (uint32_t)sWait * MS_PER_MINUTE;
+ bool due = sStartWanted || (dvxIdleMs(ctx) >= waitMs && msSince(sEnded) >= waitMs);
+
+ sStartWanted = false;
+
+ if (due && sName[0] && !ctx->modalWindow && !ctx->exclusive.owner) {
+ saverBegin(ctx);
+ }
+}
+
+
+void shellSaverToPrefs(PrefsHandleT *h) {
+ prefsSetString(h, DVX_DESKTOP_SECTION, SAVER_INI_NAME, sName);
+ prefsSetInt(h, DVX_DESKTOP_SECTION, SAVER_INI_WAIT, sWait);
+}
+
+
+int32_t shellSaverWait(void) {
+ return sWait;
+}
diff --git a/src/libs/kpunch/dvxshell/shellSaver.h b/src/libs/kpunch/dvxshell/shellSaver.h
new file mode 100644
index 00000000..82268ee1
--- /dev/null
+++ b/src/libs/kpunch/dvxshell/shellSaver.h
@@ -0,0 +1,89 @@
+// 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.
+
+// shellSaver.h -- The screen saver
+//
+// A screen saver is a program (.app) in the SAVERS folders (DVX's in
+// SHARED, the user's; DvxKindSaversE). After the wait with no key or
+// mouse input the shell starts the chosen one with SHELL_SAVER_ARGS; it
+// takes the whole screen (full-screen mode) and draws until the next
+// input, when the shell asks its windows to close. Not while a dialog box
+// is open or a program has the full screen. The settings are DVX.INI
+// [desktop] saver (its name in SAVERS, "" for none) and saverWait
+// (minutes); Control Panel's Desktop page sets them.
+#ifndef SHELL_SAVER_H
+#define SHELL_SAVER_H
+
+#include "dvxApp.h"
+#include "dvxPrefs.h"
+
+#include
+#include
+
+// What a saver's command line is when the shell starts it
+#define SHELL_SAVER_ARGS "/S"
+
+// The wait, in minutes: its range and what it is with none set
+#define SHELL_SAVER_WAIT_MIN 1
+#define SHELL_SAVER_WAIT_MAX 60
+#define SHELL_SAVER_WAIT_DEFAULT 10
+
+// Input this soon after a saver starts does not end it (the click on
+// Test, the key that started it from a program), in milliseconds.
+#define SHELL_SAVER_GRACE_MS 1000
+
+// A saver still running this long after it was asked to close is ended
+// (milliseconds).
+#define SHELL_SAVER_END_MS 3000
+
+// Read the settings from prefs (the shell at start, Control Panel's
+// Cancel).
+void shellSaverFromPrefs(PrefsHandleT *h);
+
+// The saver's name in SAVERS ("" none).
+const char *shellSaverName(void);
+
+// True while a saver runs.
+bool shellSaverRunning(void);
+
+// Choose the saver (its name in SAVERS, or any path; "" or NULL: none)
+// and the wait in minutes (kept within SHELL_SAVER_WAIT_MIN and _MAX).
+// Takes effect at once; shellSaverToPrefs keeps it.
+void shellSaverSet(const char *name, int32_t minutes);
+
+// Start the saver now, as if the wait had passed (Control Panel's Test).
+// Only recorded: the next shellSaverStep starts it. False when there is
+// no saver chosen or one runs already.
+bool shellSaverStart(void);
+
+// Main-loop step: start the saver when it is time; end a running one at
+// input after SHELL_SAVER_GRACE_MS (and force it after SHELL_SAVER_END_MS
+// when it does not close). Shell top level only.
+void shellSaverStep(AppContextT *ctx);
+
+// Write the settings into prefs (Control Panel's OK).
+void shellSaverToPrefs(PrefsHandleT *h);
+
+// The wait in minutes.
+int32_t shellSaverWait(void);
+
+#endif // SHELL_SAVER_H
diff --git a/src/libs/kpunch/dvxshell/shellSysBas.c b/src/libs/kpunch/dvxshell/shellSysBas.c
index 37e79993..517a779d 100644
--- a/src/libs/kpunch/dvxshell/shellSysBas.c
+++ b/src/libs/kpunch/dvxshell/shellSysBas.c
@@ -24,8 +24,9 @@
// (src/include/basic/system.bas)
//
// What Control Panel changes, for BASIC programs: the video mode, the
-// system colors and themes, the wallpaper and the mouse; and the system
-// font's glyphs (what DVX Demo's own text drawing reads). Each change
+// system colors and themes, the wallpaper, the screen saver, and the
+// mouse; and the system font's glyphs (what DVX Demo's own text drawing
+// reads). Each change
// takes effect at once for the whole desktop. The *Store / *Load pairs
// write and read the DVX.INI keys DVX starts with (dvxApp.h), through
// a prefs.bas handle; a PrefsCreate handle filled with *Store is an
@@ -38,6 +39,7 @@
// returning one.
#include "shellApp.h"
+#include "shellSaver.h"
#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxPrefs.h"
@@ -52,6 +54,7 @@
#include
#define MEMORY_UNKNOWN (-1)
+#define SYS_MS_PER_SEC 1000
#define KEY_BYTE_MASK 0xFF // the character / scan code byte of a key code
#define SYS_SEND_KEY_MAX 64 // keys SysSendKey keeps waiting
@@ -87,6 +90,7 @@ void SysFontsLoad(int32_t prefs);
void SysFontsStore(int32_t prefs);
int32_t SysFontWidth(void);
int32_t SysGetColor(int32_t id);
+int32_t SysIdleSeconds(void);
int32_t SysLoadTheme(const char *path);
int32_t SysMemoryFreeKb(void);
int32_t SysMemoryTotalKb(void);
@@ -102,11 +106,18 @@ int32_t SysMouseX(void);
int32_t SysMouseY(void);
void SysResetColors(void);
int32_t SysSaveTheme(const char *path);
+const char *SysScreenSaver(void);
+void SysScreenSaverLoad(int32_t prefs);
+int32_t SysScreenSaverRunning(void);
+int32_t SysScreenSaverStart(void);
+void SysScreenSaverStore(int32_t prefs);
+int32_t SysScreenSaverWait(void);
int32_t SysScreenshot(const char *path);
int32_t SysSendKey(int32_t keyCode, int32_t shift);
void SysSetColor(int32_t id, int32_t rgb);
void SysSetMouse(int32_t wheelDir, int32_t dblClickMs, int32_t accel, int32_t mickeys, int32_t wheelStep);
void SysSetMousePos(int32_t x, int32_t y);
+void SysSetScreenSaver(const char *fileName, int32_t minutes);
void SysSetTextSmoothing(int32_t on);
int32_t SysSetVideoMode(int32_t width, int32_t height, int32_t bpp);
int32_t SysSetWallpaper(const char *path);
@@ -314,6 +325,12 @@ int32_t SysGetColor(int32_t id) {
}
+// Seconds since the last key or mouse input.
+int32_t SysIdleSeconds(void) {
+ return (int32_t)(dvxIdleMs(shellContext()) / SYS_MS_PER_SEC);
+}
+
+
// Apply a theme file. True when it was read.
int32_t SysLoadTheme(const char *path) {
return dvxLoadTheme(shellContext(), path) ? BAS_TRUE : BAS_FALSE;
@@ -411,6 +428,50 @@ int32_t SysSaveTheme(const char *path) {
}
+// The screen saver's file as SAVERS knows it ("" none).
+const char *SysScreenSaver(void) {
+ return shellSaverName();
+}
+
+
+// Use the [desktop] screen saver and wait of a prefs handle.
+void SysScreenSaverLoad(int32_t prefs) {
+ PrefsHandleT *h = prefsBasHandle(prefs);
+
+ if (h) {
+ shellSaverFromPrefs(h);
+ }
+}
+
+
+int32_t SysScreenSaverRunning(void) {
+ return shellSaverRunning() ? BAS_TRUE : BAS_FALSE;
+}
+
+
+// Start the screen saver now (Control Panel's Test). False with none
+// chosen or one running.
+int32_t SysScreenSaverStart(void) {
+ return shellSaverStart() ? BAS_TRUE : BAS_FALSE;
+}
+
+
+// Store the screen saver and its wait in [desktop] of a prefs handle.
+void SysScreenSaverStore(int32_t prefs) {
+ PrefsHandleT *h = prefsBasHandle(prefs);
+
+ if (h) {
+ shellSaverToPrefs(h);
+ }
+}
+
+
+// The minutes with no input before the screen saver starts.
+int32_t SysScreenSaverWait(void) {
+ return shellSaverWait();
+}
+
+
// Save the whole screen as a picture file (BMP when path ends in .bmp,
// else PNG). True when it was written.
int32_t SysScreenshot(const char *path) {
@@ -465,6 +526,13 @@ void SysSetMousePos(int32_t x, int32_t y) {
}
+// Choose the screen saver (a file in SAVERS, any path, "" none) and its
+// wait in minutes.
+void SysSetScreenSaver(const char *fileName, int32_t minutes) {
+ shellSaverSet(fileName, minutes);
+}
+
+
// Smooth the edges of text (nonzero) or stop; text is smoothed only
// when the screen has more than 256 colors.
void SysSetTextSmoothing(int32_t on) {
diff --git a/src/libs/kpunch/http/http.c b/src/libs/kpunch/http/http.c
index 7a0caba4..f764048d 100644
--- a/src/libs/kpunch/http/http.c
+++ b/src/libs/kpunch/http/http.c
@@ -713,8 +713,6 @@ const char *httpMessage(const HttpT *h) {
HttpStateE httpPoll(HttpT *h, uint32_t nowMs) {
- char buf[READ_STEP];
-
if (!h || h->step == StepIdleE || h->step == StepFailedE) {
return HttpFailedE;
}
@@ -763,6 +761,7 @@ HttpStateE httpPoll(HttpT *h, uint32_t nowMs) {
}
if ((h->step == StepHeadE || h->step == StepBodyE) && socketReady(h->fd, POLLIN)) {
+ char buf[READ_STEP];
ssize_t n = recv(h->fd, buf, sizeof(buf), 0);
if (n < 0 && !wouldBlock(errno)) {
@@ -852,7 +851,7 @@ HttpStateE httpPoll(HttpT *h, uint32_t nowMs) {
return finish(h);
}
- if ((uint32_t)(nowMs - h->lastMs) > h->timeoutMs) {
+ if (nowMs - h->lastMs > h->timeoutMs) {
return fail(h, "%s did not answer in time (%lu seconds).", h->target.host, (unsigned long)(h->timeoutMs / MS_PER_SECOND));
}
@@ -969,15 +968,15 @@ static bool isRedirect(int32_t status) {
// one, on the same computer when it starts with /, on another with //,
// else beside the path asked for.
static bool locationResolve(const HttpT *h, const char *loc, char *out, int32_t outSize) {
- char here[HTTP_HOST_MAX + NUMBER_TEXT_MAX];
- const char *slash;
- int n;
+ int n;
if (strncasecmp(loc, HTTP_SCHEME, strlen(HTTP_SCHEME)) == 0 || strncasecmp(loc, HTTPS_SCHEME, strlen(HTTPS_SCHEME)) == 0) {
n = snprintf(out, (size_t)outSize, "%s", loc);
} else if (strncmp(loc, SCHEME_SEP, strlen(SCHEME_SEP)) == 0) {
n = snprintf(out, (size_t)outSize, "http:%s", loc);
} else {
+ char here[HTTP_HOST_MAX + NUMBER_TEXT_MAX];
+
if (h->target.port == HTTP_PORT_DEFAULT) {
snprintf(here, sizeof(here), HTTP_SCHEME "%s", h->target.host);
} else {
@@ -987,8 +986,9 @@ static bool locationResolve(const HttpT *h, const char *loc, char *out, int32_t
if (loc[0] == '/') {
n = snprintf(out, (size_t)outSize, "%s%s", here, loc);
} else {
- slash = strrchr(h->target.path, '/');
- n = snprintf(out, (size_t)outSize, "%s%.*s%s", here, (int)(slash - h->target.path + 1), h->target.path, loc);
+ const char *slash = strrchr(h->target.path, '/');
+
+ n = snprintf(out, (size_t)outSize, "%s%.*s%s", here, (int)(slash - h->target.path + 1), h->target.path, loc);
}
}
diff --git a/src/libs/kpunch/json/json.c b/src/libs/kpunch/json/json.c
index fc110fa7..01893a36 100644
--- a/src/libs/kpunch/json/json.c
+++ b/src/libs/kpunch/json/json.c
@@ -111,10 +111,10 @@ static void skipSpace(ParserT *ps);
static bool fail(ParserT *ps, const char *fmt, ...) {
- char what[JSON_ERR_MAX];
- va_list ap;
-
if (!ps->failed) {
+ char what[JSON_ERR_MAX];
+ va_list ap;
+
va_start(ap, fmt);
vsnprintf(what, sizeof(what), fmt, ap);
va_end(ap);
diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
index 982ef2b5..6e15e5bc 100644
--- a/src/libs/kpunch/libdvx/apiref.dhs
+++ b/src/libs/kpunch/libdvx/apiref.dhs
@@ -717,7 +717,7 @@ const BitmapFontT struct describing the 8x16 font, ready to pass to drawText/dra
.h1 dvxText.h -- Fonts
-The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps.
+The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps. More files of the same family and style (up to four in all) add the sizes the face lacks, each size read from the file it came in: the printer-fonts add-on gives the DejaVu faces their large sizes that way, from FONTS\KPUNCH.
dvxFontOpen gives a BitmapFontT every text call draws and measures with. It never scales: it picks the face's size nearest the one asked for. A style the family has no face for is made from the nearest face -- bold by thickening each stroke a column (two from 24 pixels), italic by slanting a column per four rows -- and the font's flags say so. Every character of code page 437 draws: what a face lacks is filled in when the font loads. Box drawing, block, and shade characters are drawn to fill the character cell (the pitch, or the digit 0's width) so lines join, also in place of a face's own when its file asks; other characters come from FONT_FALLBACK_FAMILY (FONT_FALLBACK_FIXED for a fixed-pitch face, centered in its pitch) at the nearest size and the same style, or from the system font when that family is not installed.
@@ -839,7 +839,7 @@ bool dvxFontAddMemory(int32_t owner, const void *data, uint32_t length, const ch
Add the .DFN at path, or held in data (length bytes, copied at once; name names it in the log), as a face of its family for program owner (an app id; 0 for every program, as SETUP.EXE adds the fonts it carries). Families added for owner 0 are seen by every program and kept by dvxFontRescan.
-Returns: True when added; false, logged, when the font cannot be read, calls its family "System", or owner has a face of that family and style already.
+Returns: True when added; false, logged, when the font cannot be read, calls its family "System", or brings no size that owner's face of that family and style lacks (a second file of a face adds its sizes to it).
.h2 dvxFontAddResources
@@ -2816,6 +2816,12 @@ Install an icon that is already in display pixel format (for example from dvxLoa
.index dvxTextHash
.index dvxChangeVideoMode
.index dvxExclusiveBegin
+.index dvxRunDos
+.index dvxIdleMs
+.index platformDosRun
+.index platformSuspendHookAdd
+.index platformPathAbsolute
+.index DOS programs
.index dvxExclusiveEnd
.index dvxExclusiveDisplay
.index dvxExclusiveBlitOps
@@ -3748,6 +3754,27 @@ void dvxExclusiveWaitRetrace(void);
Wait for the start of the next vertical retrace (at most two frames' time), before a flush, so the frame is not torn.
+
+.h2 DOS Programs: dvxRunDos
+
+.code
+int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const char *folder, bool pause);
+int32_t platformDosRun(const char *command, const char *folder, bool pause, const DisplayT *desktop);
+bool platformPathAbsolute(const char *path, char *out, int32_t outSize);
+.endcode
+
+dvxRunDos runs a DOS program (an .EXE, .COM, or .BAT file, or a command DOS knows) with DVX stepped aside, and comes back. Full screen, menus, and drags end; program is made absolute (a bare name stays one, for the PATH) and given to DOS by its short name, with args after it, in folder (NULL or "": the program's own folder). platformDosRun then runs the suspend hooks (newest first), lets the keyboard hook and the timer service go (the timers are kept and stand still), sets text mode, gives the INT 24h handler and Ctrl+Break back to DOS, makes the folder current, and runs the command line with system() (as at the DOS prompt: a batch file through the command interpreter, PATH, redirection); with pause it prints "Press any key to return to DVX." and waits for a key. Everything is taken back in the opposite order, the desktop's mode (and palette) set again, and the resume hooks run (oldest first); dvxRunDos sets the mouse up again where it was, forgets held keys, and repaints the whole screen. DVX logs "DOS: running" and "DOS: back from" with the command line. Nothing of DVX runs meanwhile: close the other programs first (the shell's shellRunDos asks them). Returns the program's exit code, or -1 when it could not be run (errno: ENOENT, ENOMEM, E2BIG for a command line longer than DOS's 126 characters). DVX stays in memory: the program gets the conventional memory DVX's DOS part and the DPMI host leave. On the host, platformDosRun is system() in the folder with the hooks around it.
+
+platformPathAbsolute makes a path absolute against the current folder (it need not exist).
+
+.h2 Idle Time: dvxIdleMs
+
+.code
+uint32_t dvxIdleMs(const AppContextT *ctx);
+.endcode
+
+The milliseconds since the last key or mouse input: a key (pressed, released, or typed), the mouse moving, a button, or the wheel, injected input (a test script, SysSendKey) included, and input that came while DVX was busy and was thrown away. DVX coming back from a DOS program or from full-screen mode counts as input too. The shell's screen saver starts after DVX.INI [desktop] saverWait minutes of it (shellSaver.h); BASIC: SysIdleSeconds. The time is kept in AppContextT.lastInputTime (platformClock ticks).
+
.h2 Accelerators
.h3 dvxCreateAccelTable
@@ -4458,7 +4485,7 @@ Core widget structure. Generic across all widget types; type-specific data lives
WindowT *window Owning window
int32_t x, y, w, h Computed geometry (relative to content area)
int32_t calcMinW, calcMinH Computed minimum size (from layout pass)
- int32_t minW, minH, maxW, maxH, prefW, prefH Size hints (tagged sizes)
+ int32_t minW, minH, maxW, maxH Size hints (tagged sizes): the least and the most the layout gives the widget
int32_t weight Extra-space distribution weight (0=fixed, 100=normal)
WidgetAlignE align Main-axis alignment for children
int32_t spacing, padding Tagged sizes for child spacing and padding
@@ -7214,12 +7241,20 @@ typedef void (*PlatformShutdownFnT)(void);
bool platformShutdownHookAdd(PlatformShutdownFnT fn);
void platformShutdownHookRemove(PlatformShutdownFnT fn);
+
+#define PLATFORM_SUSPEND_HOOKS 8
+typedef void (*PlatformSuspendFnT)(bool suspending);
+
+bool platformSuspendHookAdd(PlatformSuspendFnT fn);
+void platformSuspendHookRemove(PlatformSuspendFnT fn);
.endcode
platformIrqSave disables interrupts and returns the previous state for platformIrqRestore; bracket the few instructions that change data an interrupt handler reads (nothing happens on the host).
A shutdown hook runs when DVX ends without its normal shutdown (exit() or a crash with no recovery point), before the timer service and the keyboard hook are given back, so a module that drives hardware from an interrupt (a sound card playing by DMA) can stop it. Each hook runs once. A module removes its hook before it is unloaded; at most PLATFORM_SHUTDOWN_HOOKS exist at once (Add returns false when full; adding one already present does nothing).
+A suspend hook runs as fn(true) before DVX steps aside for a DOS program (platformDosRun, dvxRunDos) and as fn(false) once DVX is back, so a module that drives hardware itself lets it go (and takes it again then, or at its next use): audio.lib stops every sound, closes the output and the synthesizers, and opens the synthesizers again; music.lib stops every music and lets go of where it plays, closing its context (and the SoundFont kept on the wavetable synthesizer) when none is left; dvxnet stops the packet driver (it starts again at the next use). Suspending runs the newest hook first (a library built on another, loaded after it, lets go before it), resuming the oldest first; a hook may remove itself as they run. At most PLATFORM_SUSPEND_HOOKS; the same rules as the shutdown hooks otherwise.
+
.h2 Video
.h3 platformVideoInit / platformVideoShutdown
@@ -7628,6 +7663,7 @@ platformBootDrive returns the letter of the drive DOS started from (INT 21h AX=3
#define DVX_THEMES_DIR "THEMES"
#define DVX_WPAPER_DIR "WPAPER"
#define DVX_BANKS_DIR "BANKS"
+#define DVX_SAVERS_DIR "SAVERS"
#define DVX_VENDOR_DIR "KPUNCH"
typedef enum {
@@ -7636,6 +7672,7 @@ typedef enum {
DvxKindThemesE,
DvxKindWpaperE,
DvxKindBanksE,
+ DvxKindSaversE,
DvxKindCountE
} DvxKindE;
@@ -7662,8 +7699,8 @@ The DVX folder (the working directory) holds the program, which Setup installs,
SYSTEM DVX's own tools and files, and the shipped DVX.INI and ASSOC.INI
LIBS, WIDGETS The libraries and widgets
APPS\\ A program, its help, and DATA (the files it ships; read-only)
- SHARED FONTS, SOUNDS, THEMES, WPAPER, and BANKS: what DVX and add-on packages ship for every program (an add-on's in a folder of its vendor's, such as FONTS\ACME)
- USER (the user folder) DVX.INI, ASSOC.INI (the user's changes to SYSTEM's), DVX.LOG, the user's own FONTS, SOUNDS, THEMES, WPAPER, and BANKS, KEYS (publishers' keys the user trusts), APPS\ (each program's settings and saved files), and DOWNLOAD
+ SHARED FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS: what DVX and add-on packages ship for every program (an add-on's in a folder of its vendor's, such as FONTS\ACME)
+ USER (the user folder) DVX.INI, ASSOC.INI (the user's changes to SYSTEM's), DVX.LOG, the user's own FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS, KEYS (publishers' keys the user trusts), APPS\ (each program's settings and saved files), and DOWNLOAD
.endtable
Only packages put files in SHARED: DVX's own, and add-on packages (pkg.lib), each in its vendor's folders. A program reads it but never writes or installs into it; a program's own files live with it (its DATA folder, or resources inside the .app), under its own vendor and program folder, so two programs' files never collide.
@@ -7672,7 +7709,7 @@ platformUserInit, called once by the loader, decides the user folder: the folder
platformAppUserDir is a program's folder in the user folder: APPS\KPUNCH\TERMINAL\TERMINAL.APP gives USER\APPS\KPUNCH\TERMINAL; a program outside the DVX folder gives USER\APPS\. It is not made here (the shell's userDir and BASIC's App.User are made on first use).
-SHARED and the user folder have the same five kinds of folder (DvxKindE): fonts, sounds, themes, desktop pictures, and MIDI instrument banks. Each may hold folders of add-on vendors (SHARED\FONTS\ACME), one level deep, whose files are named VENDOR\NAME. platformKindName is a kind's folder name; platformKindDir is SHARED's (user false) or the user's (user true); platformKindBrowseDir is where a file dialog for the kind starts (the user's when it exists, else SHARED's). platformKindFind finds a file of a kind by name (NAME or VENDOR\NAME, either separator), the user's first, so a user's file of the same name wins; any other name with a folder in it, or an absolute one, is taken as the path it is (a VENDOR\NAME not in the kind's folders is tried that way too). platformKindNameOf is the other way: a file in one of the kind's folders gives its name alone, or VENDOR\NAME in a vendor's folder, which is what settings store (DVX.INI [sounds], [desktop] wallpaper, and [audio] midiBank), so moving the user folder or DVX keeps them working. platformKindReadDir lists both folders: the user's names (sorted, case ignored), then SHARED's not among them, with each vendor folder's files as VENDOR\NAME (the folders themselves are not listed); free it with dvxReadDirFree. BASIC: FileOpsUserDir$, FileOpsUserPath$, FileOpsUserInTemp, FileOpsKindDir$, FileOpsKindBrowseDir$, FileOpsKindFind$, FileOpsKindName$, and FileOpsKindList (fileops.bas, with FILEOPS_KIND_FONTS to FILEOPS_KIND_BANKS), and App.User for a program's own folder.
+SHARED and the user folder have the same six kinds of folder (DvxKindE): fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers. Each may hold folders of add-on vendors (SHARED\FONTS\ACME), one level deep, whose files are named VENDOR\NAME. platformKindName is a kind's folder name; platformKindDir is SHARED's (user false) or the user's (user true); platformKindBrowseDir is where a file dialog for the kind starts (the user's when it exists, else SHARED's). platformKindFind finds a file of a kind by name (NAME or VENDOR\NAME, either separator), the user's first, so a user's file of the same name wins; any other name with a folder in it, or an absolute one, is taken as the path it is (a VENDOR\NAME not in the kind's folders is tried that way too). platformKindNameOf is the other way: a file in one of the kind's folders gives its name alone, or VENDOR\NAME in a vendor's folder, which is what settings store (DVX.INI [sounds], [desktop] wallpaper and saver, and [audio] midiBank), so moving the user folder or DVX keeps them working. platformKindReadDir lists both folders: the user's names (sorted, case ignored), then SHARED's not among them, with each vendor folder's files as VENDOR\NAME (the folders themselves are not listed); free it with dvxReadDirFree. BASIC: FileOpsUserDir$, FileOpsUserPath$, FileOpsUserInTemp, FileOpsKindDir$, FileOpsKindBrowseDir$, FileOpsKindFind$, FileOpsKindName$, and FileOpsKindList (fileops.bas, with FILEOPS_KIND_FONTS to FILEOPS_KIND_SAVERS), and App.User for a program's own folder.
.h3 Read-only media: platformSetReadOnly / platformIsReadOnly / platformDirIsWritable / platformTempDir / platformSetLogMemory / platformLogMemory
diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
index 9e10bc02..e266a3c8 100644
--- a/src/libs/kpunch/libdvx/dvxApp.c
+++ b/src/libs/kpunch/libdvx/dvxApp.c
@@ -62,6 +62,7 @@
#include "thirdparty/stb_ds_wrap.h"
#include
+#include
#include
#include
#include
@@ -88,6 +89,7 @@ DVX_ABI_STAMP(libdvx);
#define SUBMENU_ARROW_WIDTH 12
#define SUBMENU_ARROW_HALF 3 // half-size of submenu arrow glyph
#define TOOLTIP_DELAY_MS 500
+#define DVX_DOS_CMD_MAX 256 // a DOS command line as dvxRunDos builds it (DOS takes less)
#define TOOLTIP_PAD 3
#define POPUP_BEVEL_WIDTH 2 // popup menu border bevel thickness
#define POPUP_ITEM_PAD_H 8 // extra horizontal padding in popup items
@@ -165,7 +167,6 @@ DVX_ABI_STAMP(libdvx);
#define MOUSE_KEY_ACCEL "acceleration"
#define MOUSE_KEY_SPEED "speed"
#define MOUSE_KEY_WHEEL_STEP "wheelspeed"
-#define DESKTOP_SECTION "desktop"
#define DESKTOP_KEY_WALLPAPER "wallpaper"
#define DESKTOP_KEY_MODE "mode"
#define FONTS_SECTION "fonts"
@@ -277,6 +278,7 @@ static int32_t countVisibleWindows(const AppContextT *ctx);
static int32_t cursorForResizeEdge(int32_t edge);
static void cycleWindowFocus(AppContextT *ctx, bool reverse);
static void deferDestroyWindow(AppContextT *ctx, WindowT *win);
+static void desktopTakeBack(AppContextT *ctx, int32_t mouseX, int32_t mouseY);
static void destroyWindowPrep(AppContextT *ctx, WindowT *win);
static void destroyWindowSync(AppContextT *ctx, WindowT *win);
static void dirtyCursorArea(AppContextT *ctx, int32_t x, int32_t y);
@@ -1574,6 +1576,25 @@ static void deferDestroyWindow(AppContextT *ctx, WindowT *win) {
}
+// Everything the screen shows is gone after a DOS program or a full-
+// screen one: the mouse set up again where it was, held keys forgotten,
+// and the whole screen repainted. The user was there: the idle time
+// starts again.
+static void desktopTakeBack(AppContextT *ctx, int32_t mouseX, int32_t mouseY) {
+ ctx->lastInputTime = platformClock();
+ platformMouseInit(ctx->display.width, ctx->display.height);
+ platformMouseWarp(mouseX, mouseY);
+ ctx->mouseX = mouseX;
+ ctx->mouseY = mouseY;
+ ctx->prevMouseX = mouseX;
+ ctx->prevMouseY = mouseY;
+
+ resetClipRect(&ctx->display);
+ dirtyListInit(&ctx->dirty);
+ dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
+}
+
+
// ============================================================
// destroyWindowPrep
// ============================================================
@@ -1591,8 +1612,9 @@ static void destroyWindowPrep(AppContextT *ctx, WindowT *win) {
// guaranteed same-frame use-after-free without this.
closeWindowTransients(ctx, win);
- // A window put off for destroying already had its close sound.
- if (!win->modal && !win->destroyPending) {
+ // A window put off for destroying already had its close sound; one
+ // with the whole screen (a screen saver, a game) makes none.
+ if (!win->modal && !win->destroyPending && ctx->exclusive.owner != win) {
dvxPlaySound(SoundWindowCloseE);
}
@@ -3552,6 +3574,15 @@ void dvxHideWindow(AppContextT *ctx, WindowT *win) {
}
+// ============================================================
+// dvxIdleMs
+// ============================================================
+
+uint32_t dvxIdleMs(const AppContextT *ctx) {
+ return PLATFORM_TICKS_TO_MS(platformClock() - ctx->lastInputTime);
+}
+
+
bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH) {
if (!path) {
return false;
@@ -3656,6 +3687,7 @@ int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_
ctx->lastCloseClickTime = 0;
ctx->lastTitleClickId = -1;
ctx->lastTitleClickTime = 0;
+ ctx->lastInputTime = platformClock();
ctx->wheelDirection = MOUSE_WHEEL_DIR_NORMAL;
ctx->wheelStep = MOUSE_WHEEL_STEP_DEFAULT;
ctx->dblClickMs = MOUSE_DBLCLICK_DEFAULT_MS;
@@ -4233,6 +4265,74 @@ void dvxRun(AppContextT *ctx) {
}
+// A program named by a relative path is made absolute first (it runs in
+// its own folder); a long name reaches DOS by its short one.
+int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const char *folder, bool pause) {
+ char full[DVX_MAX_PATH];
+ char shortName[DVX_MAX_PATH];
+ char where[DVX_MAX_PATH];
+ char command[DVX_DOS_CMD_MAX];
+
+ if (!ctx || !program || !program[0]) {
+ errno = ENOENT;
+ return -1;
+ }
+
+ // A bare name stays one: the command interpreter looks for it on the PATH
+ if (platformPathBaseName(program) == program || !platformPathAbsolute(program, full, sizeof(full))) {
+ snprintf(full, sizeof(full), "%s", program);
+ }
+
+ if (folder && folder[0]) {
+ snprintf(where, sizeof(where), "%s", folder);
+ } else if (!platformPathDirName(full, where, sizeof(where))) {
+ where[0] = '\0';
+ }
+
+ if (!platformShortPath(full, shortName, sizeof(shortName))) {
+ snprintf(shortName, sizeof(shortName), "%s", full);
+ }
+
+ int32_t len = snprintf(command, sizeof(command), args && args[0] ? "%s %s" : "%s", shortName, args);
+
+ if (len < 0 || len >= (int32_t)sizeof(command)) {
+ errno = E2BIG;
+ return -1;
+ }
+
+ exclusiveLeave(ctx);
+ closeWindowTransients(ctx, NULL);
+ dvxInvalidateTooltip(ctx, NULL);
+
+ if (ctx->stack.dragWindow >= 0) {
+ wmDragEnd(&ctx->stack);
+ }
+
+ if (ctx->stack.resizeWindow >= 0) {
+ wmResizeEnd(&ctx->stack);
+ }
+
+ int32_t mouseX = ctx->mouseX;
+ int32_t mouseY = ctx->mouseY;
+
+ dvxLog("DOS: running %s", command);
+
+ int32_t rc = platformDosRun(command, where, pause, &ctx->display);
+ int32_t err = errno;
+
+ if (rc < 0) {
+ dvxLog("DOS: %s could not be run: %s", command, strerror(err));
+ } else {
+ dvxLog("DOS: back from %s", command);
+ }
+
+ platformKeyStateClear();
+ desktopTakeBack(ctx, mouseX, mouseY);
+ errno = err;
+ return rc;
+}
+
+
// ============================================================
// dvxSaveImage
// ============================================================
@@ -4892,8 +4992,8 @@ bool dvxUpdate(AppContextT *ctx) {
// when the path or the mode differs from the current one.
bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h) {
- WallpaperModeE mode = (WallpaperModeE)prefsWallpaperChoices[prefsGetChoice(h, DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)].value;
- const char *value = prefsGetString(h, DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, "");
+ WallpaperModeE mode = (WallpaperModeE)prefsWallpaperChoices[prefsGetChoice(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)].value;
+ const char *value = prefsGetString(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, "");
char found[DVX_MAX_PATH];
// A name alone is a file in the wallpaper folders (the user's, then
@@ -4922,12 +5022,12 @@ void dvxWallpaperToPrefs(const AppContextT *ctx, PrefsHandleT *h) {
if (ctx->wallpaperPath[0]) {
char name[DVX_MAX_PATH];
- prefsSetString(h, DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, platformKindNameOf(DvxKindWpaperE, ctx->wallpaperPath, name, sizeof(name)) ? name : ctx->wallpaperPath);
+ prefsSetString(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, platformKindNameOf(DvxKindWpaperE, ctx->wallpaperPath, name, sizeof(name)) ? name : ctx->wallpaperPath);
} else {
- prefsRemove(h, DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER);
+ prefsRemove(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER);
}
- prefsSetString(h, DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices[ctx->wallpaperMode].iniName);
+ prefsSetString(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices[ctx->wallpaperMode].iniName);
}
@@ -5083,16 +5183,7 @@ static void exclusiveLeave(AppContextT *ctx) {
dvxLog("Video: the desktop mode could not be set again");
}
- platformMouseInit(ctx->display.width, ctx->display.height);
- platformMouseWarp(ex->savedMouseX, ex->savedMouseY);
- ctx->mouseX = ex->savedMouseX;
- ctx->mouseY = ex->savedMouseY;
- ctx->prevMouseX = ctx->mouseX;
- ctx->prevMouseY = ctx->mouseY;
-
- resetClipRect(&ctx->display);
- dirtyListInit(&ctx->dirty);
- dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
+ desktopTakeBack(ctx, ex->savedMouseX, ex->savedMouseY);
}
@@ -6563,9 +6654,9 @@ static inline void pixelToRgb(const ChannelLutT *lut, uint32_t pixel, uint8_t *d
}
-// True when (mx, my) lies inside the current (deepest) popup level.
// The open sound of the window created last, unless it has become a
-// dialog (modal) or is gone.
+// dialog (modal), has taken the whole screen (a screen saver, a game
+// starting full screen) or is gone.
static void playOpenSound(AppContextT *ctx) {
if (!sOpenSoundWinId) {
return;
@@ -6575,12 +6666,13 @@ static void playOpenSound(AppContextT *ctx) {
sOpenSoundWinId = 0;
- if (win && !win->modal) {
+ if (win && !win->modal && ctx->exclusive.owner != win) {
dvxPlaySound(SoundWindowOpenE);
}
}
+// True when (mx, my) lies inside the current (deepest) popup level.
static bool pointInPopup(const AppContextT *ctx, int32_t mx, int32_t my) {
return mx >= ctx->popup.popupX && mx < ctx->popup.popupX + ctx->popup.popupW && my >= ctx->popup.popupY && my < ctx->popup.popupY + ctx->popup.popupH;
}
@@ -6605,7 +6697,7 @@ static void pollKeyboard(AppContextT *ctx) {
PlatformKeyEventT drain;
while (platformKeyboardRead(&drain)) {
- // discard
+ ctx->lastInputTime = platformClock();
}
// The keyboard hook keeps queueing raw events regardless of busy,
@@ -6615,7 +6707,7 @@ static void pollKeyboard(AppContextT *ctx) {
RawKeyEventT rawDrain;
while (platformKeyEventRead(&rawDrain)) {
- // discard
+ ctx->lastInputTime = platformClock();
}
return;
@@ -6634,12 +6726,14 @@ static void pollKeyboard(AppContextT *ctx) {
uint8_t keyRec[ENTROPY_KEY_BYTES] = { (uint8_t)evt.scancode, (uint8_t)(evt.scancode >> BYTE_SHIFT), (uint8_t)evt.ascii, (uint8_t)evt.modifiers };
entropyFeed(keyRec, ENTROPY_KEY_BYTES);
+ ctx->lastInputTime = platformClock();
dispatchKey(ctx, evt.scancode, evt.ascii, evt.modifiers);
}
// Presses and releases after the keystrokes, so a key tapped within one
// frame reaches onKey before its onKeyUp.
while (platformKeyEventRead(&raw)) {
+ ctx->lastInputTime = platformClock();
dispatchRawKey(ctx, &raw);
}
@@ -6661,11 +6755,13 @@ static void pollMouse(AppContextT *ctx) {
int32_t wheel = platformMouseWheelPoll();
- // Only real input is entropy; an idle mouse polled every frame is not.
+ // Only real input is entropy (and input for dvxIdleMs); an idle mouse
+ // polled every frame is not.
if (mx != ctx->mouseX || my != ctx->mouseY || buttons != ctx->mouseButtons || wheel != 0) {
uint8_t mouseRec[ENTROPY_MOUSE_BYTES] = { (uint8_t)mx, (uint8_t)(mx >> BYTE_SHIFT), (uint8_t)my, (uint8_t)(my >> BYTE_SHIFT), (uint8_t)buttons, (uint8_t)wheel, (uint8_t)(wheel >> BYTE_SHIFT) };
entropyFeed(mouseRec, ENTROPY_MOUSE_BYTES);
+ ctx->lastInputTime = platformClock();
}
ctx->mouseX = mx;
diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h
index bc793c80..b4e8c4c2 100644
--- a/src/libs/kpunch/libdvx/dvxApp.h
+++ b/src/libs/kpunch/libdvx/dvxApp.h
@@ -127,6 +127,7 @@ typedef struct AppContextT {
int32_t lastCloseClickId; // window ID of last-clicked close gadget (-1 = none)
PlatformTicksT lastTitleClickTime;
int32_t lastTitleClickId; // window ID of last-clicked title bar (-1 = none)
+ PlatformTicksT lastInputTime; // the last key or mouse input (dvxIdleMs)
// Minimized icon thumbnails are refreshed one per frame (round-robin)
// rather than all at once, to amortize the cost of scaling the content
// buffer down to icon size.
@@ -342,6 +343,35 @@ void dvxExclusiveSetPalette(AppContextT *ctx, const uint8_t *pal, int32_t first,
// torn.
void dvxExclusiveWaitRetrace(void);
+// ============================================================
+// DOS programs
+// ============================================================
+
+// Run a DOS program (an .EXE, .COM, or .BAT file, or a command the DOS
+// prompt knows) with DVX stepped aside, and come back: full screen,
+// menus, and drags end; the screen and DVX's hardware go back to DOS
+// (platformDosRun); the program runs as at the DOS prompt (a batch file
+// through the command interpreter) with args after it, in folder (NULL or "": the program's own, or the
+// current one for a bare name); then DVX takes everything back, sets the
+// mouse up again, and repaints. With pause, "Press any key to return to
+// DVX." waits once it ends, so what it printed can be read. Nothing of
+// DVX runs meanwhile, so the other programs should be closed first (the
+// shell's shellRunDos asks them). Returns the program's exit code, or -1
+// when it could not be started (errno: ENOENT, ENOMEM, or
+// E2BIG for a command line longer than DOS takes).
+int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const char *folder, bool pause);
+
+// ============================================================
+// Idle time
+// ============================================================
+
+// Milliseconds since the last key or mouse input: a key pressed,
+// released or typed, the mouse moving, a button, the wheel (injected
+// input too, and input thrown away while busy). DVX coming back from a
+// DOS program or full-screen mode counts as input. The shell's screen
+// saver starts after its wait of it (shellSaver.h).
+uint32_t dvxIdleMs(const AppContextT *ctx);
+
// ============================================================
// Color scheme
// ============================================================
@@ -394,6 +424,10 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path);
// again. The mode is kept for the next wallpaper when none is loaded.
void dvxSetWallpaperMode(AppContextT *ctx, WallpaperModeE mode);
+// DVX.INI's section of the desktop settings: the wallpaper and its mode
+// here, the screen saver (dvxshell, shellSaver.h)
+#define DVX_DESKTOP_SECTION "desktop"
+
// Apply the DVX.INI [desktop] settings in h: mode (WALLPAPER_MODE_DEFAULT
// when missing) and wallpaper (none when missing). The image is only
// rebuilt when the path or mode differs from the current one. Returns
diff --git a/src/libs/kpunch/libdvx/dvxText.c b/src/libs/kpunch/libdvx/dvxText.c
index dd0a932d..e7f39afd 100644
--- a/src/libs/kpunch/libdvx/dvxText.c
+++ b/src/libs/kpunch/libdvx/dvxText.c
@@ -68,6 +68,7 @@
#include
#define FACE_STYLES 4 // regular, bold, italic, bold italic: indexed by style bits
+#define FACE_FILES_MAX 4 // .DFN files one face's sizes may come from (its own, and add-ons with more sizes)
#define FONT_IDLE_MAX 8 // fonts no one holds kept loaded
#define SYSTEM_FAMILY_INDEX 0
#define NO_FAMILY (-1)
@@ -124,11 +125,15 @@ typedef struct {
uint32_t length;
} FontSourceT;
-// One .DFN: a face of a family, installed (in the font folders) or added
+// A face of a family, installed (in the font folders) or added: one
+// .DFN, or several of the same family and style, each bringing sizes the
+// others lack (an add-on with large sizes for printing)
typedef struct {
- FontSourceT *src; // held; NULL: no face of this style (sizeCount 0)
+ FontSourceT *src[FACE_FILES_MAX]; // held, srcCount of them; none: no face of this style (sizeCount 0)
+ int32_t srcCount;
bool installed; // read from the font folders (a rescan reads it again)
uint8_t sizes[DFN_STRIKE_MAX]; // rising, each once
+ uint8_t sizeSrc[DFN_STRIKE_MAX]; // the src each size is read from
int32_t sizeCount;
uint32_t grayMask; // bit i: sizes[i] has a gray strike too
uint16_t style; // FONT_STYLE_BOLD / ITALIC
@@ -257,10 +262,10 @@ static void entryLinesCopy(FontEntryT *e, uint32_t lines);
static void entryPut(FontEntryT *e, const BuildT *b, int32_t ch, const FontGlyphT *g);
static void entryRebuild(FontEntryT *e);
static bool faceAdd(int32_t owner, const DfnSourceT *src);
-static void faceFill(FontFaceT *face, const DfnFileT *file);
static void faceFree(FontFaceT *face);
static bool faceGlyph(const BuildT *b, int32_t ch, FontGlyphT *g, const uint8_t **bits);
static bool faceGray(const FontFaceT *face, int32_t size);
+static bool faceMerge(FontFaceT *face, const DfnSourceT *dfn, const DfnFileT *file);
static void familyDropEmpty(int32_t fam);
static int32_t familyFind(const char *name, int32_t owner);
static void familyFree(FontFamilyT *fam);
@@ -677,7 +682,9 @@ static FontEntryT *entryBuild(int32_t owner, const FontPickT *pick, uint32_t sty
int32_t index = 0;
bool ok;
- sourceDfn(f->src, &src);
+ FontSourceT *fs = f->src[f->sizeSrc[DVX_MAX(sizeIndex(f, size), 0)]];
+
+ sourceDfn(fs, &src);
ok = dfnOpen(&src, &file, err, sizeof(err));
// The size's 1-bit or gray strike
@@ -687,7 +694,7 @@ static FontEntryT *entryBuild(int32_t owner, const FontPickT *pick, uint32_t sty
if (ok && dfnLoadStrike(&src, &file, index, &strike, err, sizeof(err))) {
b.strike = &strike;
- e->src = f->src;
+ e->src = fs;
e->src->refs++;
b.bpp = gray ? FONT_GRAY_BPP : 1;
b.synth = style & ~(uint32_t)f->style;
@@ -972,22 +979,12 @@ static bool faceAdd(int32_t owner, const DfnSourceT *src) {
return false;
}
- int32_t prev = memAsSystem();
- int32_t fam = familyMake(file.header.family, owner);
- uint32_t style = file.header.style & FONT_STYLE_FACE;
- FontFaceT *face = &sFamilies[fam].faces[style];
- bool ok = face->sizeCount == 0;
+ int32_t prev = memAsSystem();
+ int32_t fam = familyMake(file.header.family, owner);
+ bool ok = faceMerge(&sFamilies[fam].faces[file.header.style & FONT_STYLE_FACE], src, &file);
- if (!ok) {
- dvxLog("Fonts: %s is a second %s face of style %u; skipped", src->name ? src->name : "a font", file.header.family, (unsigned)style);
- } else {
- face->src = sourceMake(src);
- ok = face->src != NULL;
-
- if (ok) {
- faceFill(face, &file);
- ownerDetach(owner, file.header.family);
- }
+ if (ok) {
+ ownerDetach(owner, file.header.family);
}
familyDropEmpty(fam);
@@ -997,28 +994,12 @@ static bool faceAdd(int32_t owner, const DfnSourceT *src) {
}
-// face's sizes, smoothed sizes, style and flags from its file's header
-// and strike table (each size once; a gray strike follows its size's
-// 1-bit one)
-static void faceFill(FontFaceT *face, const DfnFileT *file) {
- face->style = file->header.style & FONT_STYLE_FACE;
- face->faceFlags = file->header.faceFlags;
- face->sizeCount = 0;
- face->grayMask = 0;
-
- for (int32_t i = 0; i < file->header.strikeCount; i++) {
- if (file->strikes[i].flags & DFN_STRIKE_GRAY2) {
- face->grayMask |= (face->sizeCount > 0) ? 1u << (face->sizeCount - 1) : 0;
- } else {
- face->sizes[face->sizeCount++] = (uint8_t)file->strikes[i].pixelSize;
- }
- }
-}
-
-
-// A face's source given up; the face left empty
+// A face's sources given up; the face left empty
static void faceFree(FontFaceT *face) {
- sourceRelease(face->src);
+ for (int32_t i = 0; i < face->srcCount; i++) {
+ sourceRelease(face->src[i]);
+ }
+
memset(face, 0, sizeof(*face));
}
@@ -1044,6 +1025,87 @@ static bool faceGray(const FontFaceT *face, int32_t size) {
}
+// Add a .DFN to face: the sizes it has that the face lacks, each in its
+// place (rising), with their smoothed strikes; the face's first file
+// gives its style and flags. False, logged, when it brings no size, the
+// face has FACE_FILES_MAX files already, or out of memory.
+static bool faceMerge(FontFaceT *face, const DfnSourceT *dfn, const DfnFileT *file) {
+ const char *name = dfn->name ? dfn->name : "a font";
+ int32_t index = face->srcCount;
+ int32_t added = 0;
+ bool gray[DFN_STRIKE_MAX];
+ FontSourceT *made;
+
+ if (index >= FACE_FILES_MAX) {
+ dvxLog("Fonts: %s is one %s file of style %u too many; skipped", name, file->header.family, (unsigned)(file->header.style & FONT_STYLE_FACE));
+ return false;
+ }
+
+ made = sourceMake(dfn);
+
+ if (!made) {
+ dvxLog("Fonts: no memory for %s; skipped", name);
+ return false;
+ }
+
+ for (int32_t i = 0; i < face->sizeCount; i++) {
+ gray[i] = (face->grayMask & (1u << i)) != 0;
+ }
+
+ // Its 1-bit sizes the face lacks
+ for (int32_t s = 0; s < file->header.strikeCount && face->sizeCount < DFN_STRIKE_MAX; s++) {
+ int32_t size = file->strikes[s].pixelSize;
+ int32_t at = 0;
+
+ if ((file->strikes[s].flags & DFN_STRIKE_GRAY2) || sizeIndex(face, size) >= 0) {
+ continue;
+ }
+
+ while (at < face->sizeCount && face->sizes[at] < size) {
+ at++;
+ }
+
+ memmove(&face->sizes[at + 1], &face->sizes[at], (size_t)(face->sizeCount - at));
+ memmove(&face->sizeSrc[at + 1], &face->sizeSrc[at], (size_t)(face->sizeCount - at));
+ memmove(&gray[at + 1], &gray[at], (size_t)(face->sizeCount - at) * sizeof(gray[0]));
+ face->sizes[at] = (uint8_t)size;
+ face->sizeSrc[at] = (uint8_t)index;
+ gray[at] = false;
+ face->sizeCount++;
+ added++;
+ }
+
+ if (added == 0) {
+ sourceRelease(made);
+ dvxLog("Fonts: %s has no size the %s face of style %u lacks; skipped", name, file->header.family, (unsigned)(file->header.style & FONT_STYLE_FACE));
+ return false;
+ }
+
+ // The smoothed strikes of the sizes it brought
+ for (int32_t s = 0; s < file->header.strikeCount; s++) {
+ int32_t i = sizeIndex(face, file->strikes[s].pixelSize);
+
+ if ((file->strikes[s].flags & DFN_STRIKE_GRAY2) && i >= 0 && face->sizeSrc[i] == index) {
+ gray[i] = true;
+ }
+ }
+
+ face->grayMask = 0;
+
+ for (int32_t i = 0; i < face->sizeCount; i++) {
+ face->grayMask |= gray[i] ? 1u << i : 0;
+ }
+
+ if (index == 0) {
+ face->style = file->header.style & FONT_STYLE_FACE;
+ face->faceFlags = file->header.faceFlags;
+ }
+
+ face->src[face->srcCount++] = made;
+ return true;
+}
+
+
// Family fam taken out when it has no face (never the system font's)
static void familyDropEmpty(int32_t fam) {
for (int32_t i = 0; i < FACE_STYLES; i++) {
@@ -1078,7 +1140,7 @@ static int32_t familyFind(const char *name, int32_t owner) {
// fam's added faces given up and left empty (its installed ones stay)
static void familyFree(FontFamilyT *fam) {
for (int32_t i = 0; i < FACE_STYLES; i++) {
- if (fam->faces[i].src && !fam->faces[i].installed) {
+ if (fam->faces[i].srcCount && !fam->faces[i].installed) {
faceFree(&fam->faces[i]);
}
}
@@ -1421,20 +1483,11 @@ static void scanFonts(void) {
continue;
}
- int32_t fam = familyMake(file.header.family, OWNER_ALL);
- uint32_t style = file.header.style & FONT_STYLE_FACE;
- FontFaceT *face = &sFamilies[fam].faces[style];
+ int32_t fam = familyMake(file.header.family, OWNER_ALL);
+ FontFaceT *face = &sFamilies[fam].faces[file.header.style & FONT_STYLE_FACE];
- if (face->sizeCount) {
- dvxLog("Fonts: %s is a second %s face of style %u; skipped", path, file.header.family, (unsigned)style);
- continue;
- }
-
- face->src = sourceMake(&src);
-
- if (face->src) {
+ if (faceMerge(face, &src, &file)) {
face->installed = true;
- faceFill(face, &file);
}
familyDropEmpty(fam);
diff --git a/src/libs/kpunch/libdvx/dvxText.h b/src/libs/kpunch/libdvx/dvxText.h
index 475bfa6d..9c9c88da 100644
--- a/src/libs/kpunch/libdvx/dvxText.h
+++ b/src/libs/kpunch/libdvx/dvxText.h
@@ -26,7 +26,9 @@
// A family is one name ("DejaVu Sans") with up to four faces (regular,
// bold, italic, bold italic), each a .DFN file holding the face at the
// pixel sizes it was made at (dvxFontFmt.h; made on the build host by
-// mkfont from TrueType, Type 1, and bitmap fonts). "System" is the
+// mkfont from TrueType, Type 1, and bitmap fonts). More .DFN files of
+// the same family and style add the sizes the face lacks (an add-on with
+// large sizes for printing). "System" is the
// built-in 8x16 font, always there.
//
// dvxFontOpen gives a BitmapFontT (dvxTypes.h) every text call draws and
@@ -117,9 +119,9 @@ bool dvxFontSmooth(void);
void dvxFontRescan(void);
// Add the .DFN file at path as a face of its family for program owner
-// (0: for every program). False, logged, when it cannot be read, calls
-// its family "System", or owner has a face of that family and style
-// already.
+// (0: for every program), or its sizes to owner's face of that family
+// and style. False, logged, when it cannot be read, calls its family
+// "System", or brings no size that face lacks.
bool dvxFontAddFile(int32_t owner, const char *path);
// The same for a .DFN in memory, length bytes (copied), named name in
diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h
index 1e04c595..b70de196 100644
--- a/src/libs/kpunch/libdvx/dvxWgt.h
+++ b/src/libs/kpunch/libdvx/dvxWgt.h
@@ -321,14 +321,14 @@ typedef struct WidgetT {
// Size hints (tagged: wgtPixels/wgtChars/wgtPercent, 0 = auto).
// These are set by the application and influence the layout engine:
- // minW/minH override calcMinW/H if larger, maxW/maxH clamp the final
- // size, and prefW/prefH request a specific size (layout may override).
+ // minW/minH override calcMinW/H if larger (a fixed width in a row, such
+ // as a button's or a label column's, is minW with weight 0), and
+ // maxW/maxH clamp the final size (across its box a clamped widget is
+ // centred).
int32_t minW;
int32_t minH;
int32_t maxW; // 0 = no limit
int32_t maxH;
- int32_t prefW; // preferred size, 0 = auto
- int32_t prefH;
// weight controls how extra space beyond minimum is distributed among
// siblings in a VBox/HBox. weight=0 means fixed size (no stretching),
// weight=100 is the default for flexible widgets. A widget with
diff --git a/src/libs/kpunch/libdvx/games.dhs b/src/libs/kpunch/libdvx/games.dhs
index 4a3841c0..b0ba0bb1 100644
--- a/src/libs/kpunch/libdvx/games.dhs
+++ b/src/libs/kpunch/libdvx/games.dhs
@@ -252,7 +252,7 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
.h1 Sound, Music, and MIDI
-Games play sound on whatever the computer has: a Sound Blaster family card, an FM synthesizer chip, a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.
+Games play sound on whatever the computer has: a Sound Blaster family card, a Windows Sound System card, a Gravis UltraSound, a DAC on the printer port, an FM synthesizer chip, a wavetable synthesizer (a Gravis UltraSound's or a Sound Blaster AWE's), a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.
.h2 Sound Effects and Tones (audio.h)
@@ -267,7 +267,7 @@ Games play sound on whatever the computer has: a Sound Blaster family card, an F
.h2 Music (music.h)
-musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position, and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on any card that plays sampled sound. musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
+musicLoad or musicLoadMem load a tracker module (MOD, S3M, XM, IT) or a Standard MIDI File; musicPlay(music, loop) plays it, with pause, resume, stop, volume, position, and an end notification. MIDI plays on the best synthesizer there is: an external one on the MPU-401 port when the user chose it, else a wavetable card's recorded instruments (a Gravis UltraSound or a Sound Blaster AWE with a SoundFont), else the FM chip with a General MIDI bank, else DVX's own FM software synthesizer; modules play on a wavetable card's own voices (taking little of the CPU), else on any card that plays sampled sound (mixed in software). musicMidiOpen gives live MIDI (note on and off, program change, controllers, pitch bend), and musicMmlPlay plays tunes written in QBasic's music macro language.
.h2 The FM Chip and the MIDI Port
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h
index ef94e151..65fd6782 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlat.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h
@@ -153,6 +153,7 @@ const char *platformTempDir(void);
#define DVX_THEMES_DIR "THEMES"
#define DVX_WPAPER_DIR "WPAPER"
#define DVX_BANKS_DIR "BANKS" // MIDI instrument banks (OP2), in SHARED
+#define DVX_SAVERS_DIR "SAVERS" // screen savers (programs the shell starts after the wait, shellSaver.h)
#define DVX_VENDOR_DIR "KPUNCH" // DVX's own vendor folder (APPS\KPUNCH, LIBS\KPUNCH, ...); add-ons have their own
// DVX /SAFE (or Shift held while it starts): a safe start, in safe video
@@ -166,6 +167,7 @@ typedef enum {
DvxKindThemesE,
DvxKindWpaperE,
DvxKindBanksE,
+ DvxKindSaversE,
DvxKindCountE
} DvxKindE;
@@ -199,7 +201,8 @@ const char *platformDvxIniPath(void);
// extension>. Not made here.
bool platformAppUserDir(char *out, int32_t outSize, const char *appPath);
-// A kind's folder name ("FONTS", "SOUNDS", "THEMES", "WPAPER", "BANKS");
+// A kind's folder name ("FONTS", "SOUNDS", "THEMES", "WPAPER", "BANKS",
+// "SAVERS");
// NULL for a kind out of range.
const char *platformKindName(DvxKindE kind);
@@ -369,6 +372,21 @@ typedef void (*PlatformShutdownFnT)(void);
bool platformShutdownHookAdd(PlatformShutdownFnT fn);
void platformShutdownHookRemove(PlatformShutdownFnT fn);
+// Suspend hooks: fn(true) runs before DVX steps aside for a DOS program
+// (platformDosRun) and fn(false) once it is back, so a module that drives
+// hardware itself (a sound card's DMA and interrupt, a network card's
+// packet driver) lets it go, and takes it again then or at its next use.
+// Suspending runs the newest first (a library built on another, loaded
+// after it, lets go before it), resuming the oldest first.
+// At most PLATFORM_SUSPEND_HOOKS at once; Add returns false when full
+// (adding one already present is a no-op).
+#define PLATFORM_SUSPEND_HOOKS 8
+
+typedef void (*PlatformSuspendFnT)(bool suspending);
+
+bool platformSuspendHookAdd(PlatformSuspendFnT fn);
+void platformSuspendHookRemove(PlatformSuspendFnT fn);
+
// ============================================================
// System lifecycle
// ============================================================
@@ -382,6 +400,22 @@ void platformShutdownHookRemove(PlatformShutdownFnT fn);
// on the hidden text screen. DOS restores its own handler when DVX exits.
void platformInit(void);
+// Run a DOS command line with DVX stepped aside, and come back: the
+// suspend hooks run, the timer service and the keyboard hook let go (the
+// timers kept), the screen in text mode, and Ctrl+Break and the critical-
+// error handler as DOS had them; folder (NULL or "": the current one) is
+// made current and command runs as at the DOS prompt (system(): a
+// program, a batch file through the command interpreter, PATH, and
+// redirection) and is waited for; with pause,
+// "Press any key to return to DVX." waits for a key. Then all of it is
+// taken back, DVX's folder made current again, and desktop's mode (and
+// palette) set again: the caller repaints and resets the mouse
+// (dvxRunDos). Returns what system() does (the program's exit code), or
+// -1 when it could not be started (errno: ENOENT, ENOMEM, or E2BIG for a
+// command longer than DOS takes). On the host: system() too, the hooks
+// around it.
+int32_t platformDosRun(const char *command, const char *folder, bool pause, const DisplayT *desktop);
+
// Cooperative yield -- give up the CPU timeslice when the event loop has
// nothing to do. On DOS this calls __dpmi_yield() to be friendly to
// multitaskers (Windows 3.x, OS/2).
@@ -637,7 +671,9 @@ void platformInjectModifiers(int32_t modifiers);
// buffer does not.
void platformInjectScan(uint8_t scan);
-// Override the mouse position and buttons platformMousePoll reports
+// Override the mouse position and buttons platformMousePoll reports:
+// each poll reports the next state given (they queue, so a press and its
+// release given between two polls are both seen), and the last stays
// until the next call. The driver is still polled, so its state machine
// keeps running; only the reported values are replaced.
void platformInjectMouse(int32_t x, int32_t y, int32_t buttons);
@@ -742,6 +778,11 @@ int32_t platformChdir(const char *path);
// when switching between graphics modes live.
void platformVideoFreeBuffers(DisplayT *d);
+// out = path made absolute against the current folder (as it is when it
+// already is; it need not exist). False when it does not fit or the
+// current folder cannot be read.
+bool platformPathAbsolute(const char *path, char *out, int32_t outSize);
+
// Return a pointer to the last directory separator in path
// (platformPathIsSep), or NULL if none is found.
char *platformPathDirEnd(const char *path);
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatIsr.h b/src/libs/kpunch/libdvx/platform/dvxPlatIsr.h
index bf900201..96de01e9 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatIsr.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatIsr.h
@@ -130,6 +130,10 @@ void platformTimerProgram(uint32_t divisor);
// calls it when DVX ends without its normal shutdown.
void platformShutdownHooksRun(void);
+// Run every suspend hook (dvxPlatformIrq.c): fn(true) newest first before
+// a DOS program (platformDosRun), fn(false) oldest first after it.
+void platformSuspendHooksRun(bool suspending);
+
// Decode one byte from the keyboard.
ISR_INLINE void keyScanFeed(KeyScanStateT *s, uint8_t byte) {
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
index 815c82ba..13d22e14 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
@@ -275,6 +275,9 @@
// INT 21h DOS functions
#define DOS_FN_GET_VERSION 0x3000
+#define DOS_FN_GET_BREAK 0x3300 // DL = break checking (0 off, 1 on)
+#define DOS_CMD_TAIL_MAX 126 // a command line after the program's name
+#define DOS_PAUSE_TEXT "\r\nPress any key to return to DVX."
#define DOS_FN_SET_BREAK 0x3301
#define DOS_BREAK_OFF 0x0000
#define DOS_FN_GET_BOOT_DRIVE 0x3305 // DL = boot drive (1 = A:)
@@ -352,7 +355,6 @@
#define CRTC_UNPROTECT_MASK 0x7F // clears the write-protect bit of CRTC 0-7
#define MISC_MODEX 0xE3 // 25 MHz dot clock, 480-line sync polarities
#define REG_DATA_SHIFT 8 // outportw: index in the low byte, data in the high
-#define BYTE_MASK 0xFF
// Vertical retrace: VGA input status 1, bit 3 is set during retrace. The
// wait gives up after RETRACE_WAIT_MAX_US (two frames at 50 Hz), so a card
@@ -377,6 +379,10 @@
#define MOUSE_TRAP_BOX_H 240
#define MOUSE_PROMOTE_CONSEC 2
+// Scripted mouse states waiting for platformMousePoll (a test script gives
+// one a frame; more than a few only pile up when frames are slow)
+#define INJECT_MOUSE_MAX 16
+
// ============================================================
// Types
@@ -444,6 +450,13 @@ typedef struct {
uint32_t size;
} LfbMapT;
+// A scripted mouse state (platformInjectMouse)
+typedef struct {
+ int32_t x;
+ int32_t y;
+ int32_t buttons;
+} InjectMouseT;
+
// Scores a VBE mode against a request (-1: unusable); findBestMode keeps
// the highest.
typedef int32_t (*ModeScoreFnT)(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
@@ -453,11 +466,16 @@ typedef int32_t (*ModeScoreFnT)(const VbeModeT *m, int32_t requestedW, int32_t r
// Prototypes
// ============================================================
+static dxe_symbol_table sDxeExportTable[];
static void bankCopy(uint32_t offset, const uint8_t *src, uint32_t bytes);
static void bankSet(uint8_t window, uint32_t winStart);
+static void biosKeyFlush(void);
static uint8_t biosVideoMode(void);
static inline void copySpan(uint8_t *dst, const uint8_t *src, uint32_t bytes);
static bool cpuHasTsc(void);
+static void critErrorOwn(bool ours);
+static int32_t desktopModeRestore(const DisplayT *desktop);
+static void dosBreakSet(uint16_t state);
static uint32_t estimateClockMhz(void);
static uint32_t farPtrToLinear(uint32_t addr);
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, ModeScoreFnT score, uint16_t *outMode, VbeModeT *outInfo, DisplayT *d);
@@ -474,15 +492,16 @@ static void modeXSet(void);
static uint32_t pagesToKb(unsigned long pages);
static uint32_t pitLatchElapsed(void);
static void platformCrashHandler(int sig);
+int32_t platformDosRun(const char *command, const char *folder, bool pause, const DisplayT *desktop);
ISR_INLINE uint64_t readTsc(void);
static bool regionFromNear(const void *addr, uint32_t size, bool isCode, __dpmi_meminfo *out);
static int32_t scoreExclusive(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
static int32_t scoreMode(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
-static dxe_symbol_table sDxeExportTable[];
static void setBiosMode(uint16_t mode);
static int32_t setVesaMode(uint16_t mode, bool linear);
static void sysInfoAppend(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static void sysInfoDrives(void);
+static void timerHold(bool hold);
static bool timerLock(void);
static void timerUnlock(void);
static void unmapLfb(DisplayT *d, LfbMapT *map);
@@ -503,6 +522,13 @@ static uint8_t sSplashPal[SPLASH_PAL_BYTES];
// INT 24h stub: MOV AL,3 (fail the call); IRET. Installed once.
static const uint8_t sCritStub[] = { 0xB0, 0x03, 0xCF };
static bool sCritErrorInstalled = false;
+static __dpmi_raddr sCritVector; // sCritStub's
+static __dpmi_raddr sCritVectorDos; // DOS's, given back while a DOS program runs
+
+// DOS's break checking as DVX found it (platformDosRun gives it back to a
+// DOS program), and platformInit's one run
+static uint16_t sBreakDos = DOS_BREAK_OFF;
+static bool sPlatformInitDone = false;
// TSC availability for platformEntropyStamp, probed on first use.
typedef enum {
@@ -568,11 +594,14 @@ static int32_t sMickeyH = MOUSE_SETTING_UNSET;
static int32_t sMickeyV = MOUSE_SETTING_UNSET;
static int32_t sAccelThreshold = MOUSE_SETTING_UNSET;
-// Test-script mouse override (platformInjectMouse); never set otherwise.
-static bool sInjectMouse = false;
-static int32_t sInjectX = 0;
-static int32_t sInjectY = 0;
-static int32_t sInjectButtons = 0;
+// Test-script mouse override (platformInjectMouse): the states not
+// reported yet, oldest first (a poll reports one), and the last one
+// reported, which stays; never set otherwise.
+static bool sInjectMouse = false;
+static InjectMouseT sInjectQueue[INJECT_MOUSE_MAX];
+static int32_t sInjectHead = 0;
+static int32_t sInjectCount = 0;
+static InjectMouseT sInjectLast;
// System information static buffer
static char sSysInfoBuf[PLATFORM_SYSINFO_MAX];
@@ -693,6 +722,26 @@ static void bankSet(uint8_t window, uint32_t winStart) {
}
+// The BIOS keyboard buffer emptied.
+static void biosKeyFlush(void) {
+ __dpmi_regs r;
+
+ for (;;) {
+ memset(&r, 0, sizeof(r));
+ r.x.ax = KBD_FN_PEEK_EXT;
+ __dpmi_int(INT_KEYBOARD, &r);
+
+ if (r.x.flags & CPU_FLAG_ZF) {
+ return;
+ }
+
+ memset(&r, 0, sizeof(r));
+ r.x.ax = KBD_FN_READ_EXT;
+ __dpmi_int(INT_KEYBOARD, &r);
+ }
+}
+
+
// Current BIOS video mode (INT 10h AH=0Fh).
static uint8_t biosVideoMode(void) {
__dpmi_regs r;
@@ -747,6 +796,46 @@ static bool cpuHasTsc(void) {
}
+// INT 24h: DVX's handler (critical errors fail the call), or DOS's own
+// while a DOS program runs.
+static void critErrorOwn(bool ours) {
+ if (sCritErrorInstalled) {
+ __dpmi_set_real_mode_interrupt_vector(INT_CRIT_ERROR, ours ? &sCritVector : &sCritVectorDos);
+ }
+}
+
+
+// The desktop's mode (and, at 8 bpp, its palette) again after something
+// else had the screen: the desktop's window too, at an unknown position
+// after the mode set.
+static int32_t desktopModeRestore(const DisplayT *desktop) {
+ sBank = sDesktopBank;
+ sBank.winStart = PLATFORM_BANK_NONE;
+
+ if (setVesaMode(sDesktopMode, !sDesktopBank.active) != 0) {
+ videoError("VBE: Could not set the desktop's mode 0x%04X again", sDesktopMode);
+ return -1;
+ }
+
+ if (desktop->format.bitsPerPixel == BPP_8 && desktop->palette) {
+ platformVideoSetPalette(desktop->palette, 0, PAL_ENTRIES);
+ }
+
+ return 0;
+}
+
+
+// DOS's break checking during its calls (INT 21h 3301h).
+static void dosBreakSet(uint16_t state) {
+ __dpmi_regs r;
+
+ memset(&r, 0, sizeof(r));
+ r.x.ax = DOS_FN_SET_BREAK;
+ r.x.dx = state;
+ __dpmi_int(INT_DOS, &r);
+}
+
+
// RDTSC calibration via BIOS timer.
// Measures TSC ticks over CLOCK_MEAS_TICKS BIOS timer ticks (~165 ms).
// Using several ticks instead of 1 reduces jitter from interrupt latency
@@ -909,9 +998,10 @@ static bool hasCpuid(void) {
// Point the real-mode INT 24h vector at sCritStub, copied into a paragraph
-// of DOS memory, so every critical error fails the DOS call. Never
-// removed: DOS restores the vector saved in the PSP when DVX exits, and the
-// DPMI host frees the DOS memory.
+// of DOS memory, so every critical error fails the DOS call. Given back
+// to DOS's handler only while a DOS program runs (critErrorOwn): DOS
+// restores the vector saved in the PSP when DVX exits, and the DPMI host
+// frees the DOS memory.
static void installCritErrorHandler(void) {
if (sCritErrorInstalled) {
return;
@@ -925,12 +1015,11 @@ static void installCritErrorHandler(void) {
return;
}
- __dpmi_raddr vector;
-
dosmemput(sCritStub, sizeof(sCritStub), (uint32_t)segment << PARAGRAPH_SHIFT);
- vector.segment = (uint16_t)segment;
- vector.offset16 = 0;
- sCritErrorInstalled = __dpmi_set_real_mode_interrupt_vector(INT_CRIT_ERROR, &vector) == 0;
+ __dpmi_get_real_mode_interrupt_vector(INT_CRIT_ERROR, &sCritVectorDos);
+ sCritVector.segment = (uint16_t)segment;
+ sCritVector.offset16 = 0;
+ sCritErrorInstalled = __dpmi_set_real_mode_interrupt_vector(INT_CRIT_ERROR, &sCritVector) == 0;
if (!sCritErrorInstalled) {
__dpmi_free_dos_memory(selector);
@@ -1239,6 +1328,67 @@ static void platformCrashHandler(int sig) {
}
+// system() runs the command line as DOS would (a batch file through the
+// command interpreter, PATH, redirection); spawn would quote the arguments
+// that hold blanks. DVX's interrupt hooks are let go first and taken back
+// after, in the opposite order.
+int32_t platformDosRun(const char *command, const char *folder, bool pause, const DisplayT *desktop) {
+ char cwd[DVX_MAX_PATH];
+ int32_t rc = -1;
+
+ if (strlen(command) > DOS_CMD_TAIL_MAX) {
+ errno = E2BIG;
+ return -1;
+ }
+
+ if (!getcwd(cwd, sizeof(cwd))) {
+ cwd[0] = '\0';
+ }
+
+ platformSuspendHooksRun(true);
+ platformKeyHookShutdown();
+ timerHold(true);
+ setBiosMode(BIOS_MODE_TEXT80);
+ critErrorOwn(false);
+ dosBreakSet(sBreakDos);
+ biosKeyFlush();
+
+ if (!folder || !folder[0] || platformChdir(folder) == 0) {
+ rc = system(command);
+ } else {
+ errno = ENOENT;
+ }
+
+ int32_t savedErrno = errno;
+
+ if (pause) {
+ fputs(DOS_PAUSE_TEXT, stdout);
+ fflush(stdout);
+ biosKeyFlush();
+
+ __dpmi_regs r;
+
+ memset(&r, 0, sizeof(r));
+ r.x.ax = KBD_FN_READ_EXT;
+ __dpmi_int(INT_KEYBOARD, &r);
+ }
+
+ if (cwd[0]) {
+ platformChdir(cwd);
+ }
+
+ biosKeyFlush();
+ dosBreakSet(DOS_BREAK_OFF);
+ critErrorOwn(true);
+ desktopModeRestore(desktop);
+ timerHold(false);
+ platformKeyHookInit();
+ platformSuspendHooksRun(false);
+ errno = savedErrno;
+ return rc;
+}
+
+
// IOCTL 4409h (remote?), 440Eh (the letter a shared floppy answers to)
// and 4408h (removable?) never read the media.
int32_t platformDriveList(PlatformDriveT *out, int32_t max) {
@@ -1629,6 +1779,12 @@ const char *platformGetSystemInfo(const DisplayT *display) {
void platformInit(void) {
+ if (sPlatformInitDone) {
+ return;
+ }
+
+ sPlatformInitDone = true;
+
// Disable Ctrl+C/Break at every level so the user can't
// accidentally kill the GUI while in graphics mode.
@@ -1638,13 +1794,15 @@ void platformInit(void) {
// 2. C-level: ignore SIGINT if it somehow fires
signal(SIGINT, SIG_IGN);
- // 3. DOS-level: disable break checking during INT 21h calls
- // (function 33h, subfunction 01h, DL=0 = break off)
+ // 3. DOS-level: disable break checking during INT 21h calls (kept
+ // first, for a DOS program DVX runs)
__dpmi_regs r;
+
memset(&r, 0, sizeof(r));
- r.x.ax = DOS_FN_SET_BREAK;
- r.x.dx = DOS_BREAK_OFF;
+ r.x.ax = DOS_FN_GET_BREAK;
__dpmi_int(INT_DOS, &r);
+ sBreakDos = r.h.dl;
+ dosBreakSet(DOS_BREAK_OFF);
// 4. Critical errors fail the call instead of prompting
installCritErrorHandler();
@@ -1681,11 +1839,18 @@ void platformInjectModifiers(int32_t modifiers) {
}
+// Queued, so a press and its release given between two polls are both
+// seen; when the queue is full the newest state waiting is replaced.
void platformInjectMouse(int32_t x, int32_t y, int32_t buttons) {
- sInjectMouse = true;
- sInjectX = x;
- sInjectY = y;
- sInjectButtons = buttons & MOUSE_BUTTON_MASK;
+ InjectMouseT state = { x, y, buttons & MOUSE_BUTTON_MASK };
+
+ if (sInjectCount == INJECT_MOUSE_MAX) {
+ sInjectCount--;
+ }
+
+ sInjectQueue[(sInjectHead + sInjectCount) % INJECT_MOUSE_MAX] = state;
+ sInjectCount++;
+ sInjectMouse = true;
}
@@ -2066,10 +2231,16 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
sCurY += (int16_t)r.x.dx;
}
+ if (sInjectCount > 0) {
+ sInjectLast = sInjectQueue[sInjectHead];
+ sInjectHead = (sInjectHead + 1) % INJECT_MOUSE_MAX;
+ sInjectCount--;
+ }
+
if (sInjectMouse) {
- sCurX = sInjectX;
- sCurY = sInjectY;
- *buttons = sInjectButtons;
+ sCurX = sInjectLast.x;
+ sCurY = sInjectLast.y;
+ *buttons = sInjectLast.buttons;
}
if (sCurX < 0) {
@@ -2144,6 +2315,11 @@ void platformMouseWarp(int32_t x, int32_t y) {
sCurX = x;
sCurY = y;
+ // A script's mouse moves too, as the host's does, or the next poll puts
+ // it back (and full screen would see that as the user moving it).
+ sInjectLast.x = x;
+ sInjectLast.y = y;
+
if (sMouseMode == MouseModeAbsoluteE) {
// Function 04h: move the driver cursor so the next 03h read agrees.
memset(&r, 0, sizeof(r));
@@ -2759,21 +2935,7 @@ int32_t platformVideoExclusiveEnd(DisplayT *d, const DisplayT *desktop) {
d->backBuf = NULL;
sModeX = false;
unmapLfb(d, &sExclusiveLfb);
-
- // The desktop's window again, at an unknown position after the mode set
- sBank = sDesktopBank;
- sBank.winStart = PLATFORM_BANK_NONE;
-
- if (setVesaMode(sDesktopMode, !sDesktopBank.active) != 0) {
- videoError("VBE: Could not set the desktop's mode 0x%04X again", sDesktopMode);
- return -1;
- }
-
- if (desktop->format.bitsPerPixel == BPP_8 && desktop->palette) {
- platformVideoSetPalette(desktop->palette, 0, PAL_ENTRIES);
- }
-
- return 0;
+ return desktopModeRestore(desktop);
}
@@ -3176,6 +3338,28 @@ static void sysInfoDrives(void) {
}
+// A DOS program runs (platformDosRun): the PIT at the BIOS rate and INT 8
+// the BIOS's again, the timers kept (they stand still meanwhile); then the
+// service's rate and handler again. The wrapper and the locks stay.
+static void timerHold(bool hold) {
+ if (!platformTimer.running) {
+ return;
+ }
+
+ uint32_t divisor = hold ? PLATFORM_PIT_BIOS_DIVISOR : platformTimer.divisor;
+ uint32_t state = platformIrqSave();
+
+ outportb(PIT_CMD_PORT, hold ? PIT_CH0_SQUARE_WAVE : PIT_CH0_RATE_GEN);
+ outportb(PIT_CH0_PORT, divisor & BYTE_MASK);
+ outportb(PIT_CH0_PORT, (divisor >> BYTE_BITS) & BYTE_MASK);
+ _go32_dpmi_set_protected_mode_interrupt_vector(TIMER_IRQ_VECTOR, hold ? &sOldInt8 : &sNewInt8);
+
+ // The TSC interpolation starts over with the next interrupt.
+ platformTimer.tscAtIrq = 0;
+ platformIrqRestore(state);
+}
+
+
// Pin the timer interrupt's code and data; all or nothing.
static bool timerLock(void) {
if (platformLockRegion((const void *)int8Handler, ISR_LOCK_SIZE, true) != 0) {
@@ -3536,6 +3720,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformCopyFile)
DXE_EXPORT(platformDirIsWritable)
DXE_EXPORT(platformDiskSpace)
+ DXE_EXPORT(platformDosRun)
DXE_EXPORT(platformDriveList)
DXE_EXPORT(platformDvxIniPath)
DXE_EXPORT(platformEntropyStamp)
@@ -3585,6 +3770,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformNormalizeFKeyScan)
DXE_EXPORT(platformPathBaseName)
DXE_EXPORT(platformPathDirEnd)
+ DXE_EXPORT(platformPathAbsolute)
DXE_EXPORT(platformPathDirName)
DXE_EXPORT(platformPathIsAbsolute)
DXE_EXPORT(platformPathIsSep)
@@ -3613,6 +3799,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformSpanFill8)
DXE_EXPORT(platformStatSize)
DXE_EXPORT(platformStripLineEndings)
+ DXE_EXPORT(platformSuspendHookAdd)
+ DXE_EXPORT(platformSuspendHookRemove)
DXE_EXPORT(platformTempDir)
DXE_EXPORT(platformTimerAdd)
DXE_EXPORT(platformTimerAddFast)
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
index 9e757560..9e64e264 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
@@ -60,8 +60,11 @@
#include
#include
#include
+#include
#include
+#include
#include
+#include
// Default mode when the caller passes zero for a dimension.
#define HOST_DEFAULT_W 640
@@ -490,6 +493,39 @@ PlatformTicksT platformClockHz(void) {
}
+// The host has no DOS: the command runs with system() in folder, with the
+// suspend hooks around it as on DOS (pause and desktop mean nothing here).
+int32_t platformDosRun(const char *command, const char *folder, bool pause, const DisplayT *desktop) {
+ char cwd[DVX_MAX_PATH];
+ int status = -1;
+
+ (void)pause;
+ (void)desktop;
+
+ if (!getcwd(cwd, sizeof(cwd))) {
+ cwd[0] = '\0';
+ }
+
+ platformSuspendHooksRun(true);
+
+ if (!folder || !folder[0] || platformChdir(folder) == 0) {
+ status = system(command);
+ } else {
+ errno = ENOENT;
+ }
+
+ int32_t savedErrno = errno;
+
+ if (cwd[0]) {
+ platformChdir(cwd);
+ }
+
+ platformSuspendHooksRun(false);
+ errno = savedErrno;
+ return status < 0 ? -1 : WEXITSTATUS(status);
+}
+
+
int32_t platformDriveList(PlatformDriveT *out, int32_t max) {
if (max < 1) {
return 0;
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
index 6c9cff31..00821a4f 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
@@ -36,10 +36,16 @@
// "No timer registered" as a rate (timerApplyRate).
#define TIMER_RATE_NONE 0
+// A shutdown or suspend hook as its table keeps it (one type, so adding
+// and removing is shared; converted back to its own type to be called).
+typedef void (*HookFnT)(void);
+
// ============================================================
// Prototypes
// ============================================================
+static bool hookAdd(HookFnT *table, int32_t count, HookFnT fn);
+static void hookRemove(HookFnT *table, int32_t count, HookFnT fn);
void platformInjectScan(uint8_t scan);
bool platformKeyEventRead(RawKeyEventT *evt);
bool platformKeyIsDown(int32_t rawCode);
@@ -48,6 +54,9 @@ void platformKeyStateGet(uint8_t out[KEY_RAW_COUNT]);
bool platformShutdownHookAdd(PlatformShutdownFnT fn);
void platformShutdownHookRemove(PlatformShutdownFnT fn);
void platformShutdownHooksRun(void);
+bool platformSuspendHookAdd(PlatformSuspendFnT fn);
+void platformSuspendHookRemove(PlatformSuspendFnT fn);
+void platformSuspendHooksRun(bool suspending);
int32_t platformTimerAdd(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
int32_t platformTimerAddFast(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId);
void platformTimerRemove(int32_t timerId);
@@ -64,7 +73,42 @@ static uint32_t timerDivisorForHz(uint32_t hz);
KeyScanStateT platformKeyScan;
TimerServiceT platformTimer;
-static PlatformShutdownFnT sShutdownHooks[PLATFORM_SHUTDOWN_HOOKS];
+static HookFnT sShutdownHooks[PLATFORM_SHUTDOWN_HOOKS];
+static HookFnT sSuspendHooks[PLATFORM_SUSPEND_HOOKS];
+
+
+// fn after the hooks already in table (kept in the order they were
+// added, with no gaps: hookRemove closes them); true when it is there
+// (already, or now).
+static bool hookAdd(HookFnT *table, int32_t count, HookFnT fn) {
+ for (int32_t i = 0; i < count; i++) {
+ if (table[i] == fn) {
+ return true;
+ }
+
+ if (!table[i]) {
+ table[i] = fn;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+static void hookRemove(HookFnT *table, int32_t count, HookFnT fn) {
+ int32_t kept = 0;
+
+ for (int32_t i = 0; i < count; i++) {
+ if (table[i] != fn) {
+ table[kept++] = table[i];
+ }
+ }
+
+ while (kept < count) {
+ table[kept++] = NULL;
+ }
+}
void platformInjectScan(uint8_t scan) {
@@ -128,33 +172,12 @@ void platformKeyStateGet(uint8_t out[KEY_RAW_COUNT]) {
bool platformShutdownHookAdd(PlatformShutdownFnT fn) {
- int32_t freeSlot = -1;
-
- for (int32_t i = 0; i < PLATFORM_SHUTDOWN_HOOKS; i++) {
- if (sShutdownHooks[i] == fn) {
- return true;
- }
-
- if (!sShutdownHooks[i] && freeSlot < 0) {
- freeSlot = i;
- }
- }
-
- if (freeSlot < 0) {
- return false;
- }
-
- sShutdownHooks[freeSlot] = fn;
- return true;
+ return hookAdd(sShutdownHooks, PLATFORM_SHUTDOWN_HOOKS, (HookFnT)fn);
}
void platformShutdownHookRemove(PlatformShutdownFnT fn) {
- for (int32_t i = 0; i < PLATFORM_SHUTDOWN_HOOKS; i++) {
- if (sShutdownHooks[i] == fn) {
- sShutdownHooks[i] = NULL;
- }
- }
+ hookRemove(sShutdownHooks, PLATFORM_SHUTDOWN_HOOKS, (HookFnT)fn);
}
@@ -173,6 +196,35 @@ void platformShutdownHooksRun(void) {
}
+bool platformSuspendHookAdd(PlatformSuspendFnT fn) {
+ return hookAdd(sSuspendHooks, PLATFORM_SUSPEND_HOOKS, (HookFnT)fn);
+}
+
+
+void platformSuspendHookRemove(PlatformSuspendFnT fn) {
+ hookRemove(sSuspendHooks, PLATFORM_SUSPEND_HOOKS, (HookFnT)fn);
+}
+
+
+// Newest first when suspending (a library built on another lets go
+// before it), oldest first when resuming. The hooks run from a copy, so
+// one may remove itself (or another) as it runs.
+void platformSuspendHooksRun(bool suspending) {
+ HookFnT hooks[PLATFORM_SUSPEND_HOOKS];
+
+ memcpy(hooks, sSuspendHooks, sizeof(hooks));
+
+ for (int32_t n = 0; n < PLATFORM_SUSPEND_HOOKS; n++) {
+ int32_t i = suspending ? PLATFORM_SUSPEND_HOOKS - 1 - n : n;
+ PlatformSuspendFnT fn = (PlatformSuspendFnT)hooks[i];
+
+ if (fn) {
+ fn(suspending);
+ }
+ }
+}
+
+
int32_t platformTimerAdd(uint32_t hz, PlatformTimerFnT fn, void *userData, int32_t ownerAppId) {
return timerAdd(hz, PLATFORM_TIMER_MAX_HZ, fn, userData, ownerAppId);
}
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
index 0c077645..d1fc8e1f 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
@@ -171,7 +171,6 @@ static bool globMatchLen(const char *pat, int32_t patLen, const char
static char **kindList(const char *dir);
static int kindNameCompare(const void *a, const void *b);
static void logMemAppend(const char *fmt, va_list ap);
-static bool pathAbsolute(const char *path, char *out, int32_t outSize);
static bool pathCharEq(char a, char b);
static int32_t pathSepCount(const char *path);
static int32_t pathTrimmedLen(const char *path);
@@ -203,6 +202,7 @@ int32_t platformMkdirRecursive(const char *path);
int32_t platformNormalizeFKeyScan(int32_t scancode);
void platformNormalizeKeyEvent(PlatformKeyEventT *evt, int32_t liveMods);
void platformOutOfMemory(size_t bytes);
+bool platformPathAbsolute(const char *path, char *out, int32_t outSize);
const char *platformPathBaseName(const char *path);
char *platformPathDirEnd(const char *path);
bool platformPathDirName(const char *path, char *out, int32_t outSize);
@@ -254,7 +254,7 @@ static char sBootNotes[PLATFORM_BOOT_NOTES_MAX];
static bool sBootNotesFull = false;
// The folder of each kind, in DvxKindE order
-static const char *const sKindDirs[DvxKindCountE] = { DVX_FONTS_DIR, DVX_SOUNDS_DIR, DVX_THEMES_DIR, DVX_WPAPER_DIR, DVX_BANKS_DIR };
+static const char *const sKindDirs[DvxKindCountE] = { DVX_FONTS_DIR, DVX_SOUNDS_DIR, DVX_THEMES_DIR, DVX_WPAPER_DIR, DVX_BANKS_DIR, DVX_SAVERS_DIR };
// The user folder (platformUserDir; "" until first asked), whether it
// went in TEMP, and platformDvxIniPath's answer
@@ -649,19 +649,6 @@ static void logMemAppend(const char *fmt, va_list ap) {
}
-// out = path made absolute against the working directory (as it is when
-// it already is)
-static bool pathAbsolute(const char *path, char *out, int32_t outSize) {
- char cwd[DVX_MAX_PATH];
-
- if (platformPathIsAbsolute(path)) {
- return snprintf(out, (size_t)outSize, "%s", path) < outSize;
- }
-
- return getcwd(cwd, sizeof(cwd)) && platformPathJoin(out, outSize, cwd, path);
-}
-
-
// DVX's path rules are DOS's on every platform: the separators are
// interchangeable and names compare without case.
static bool pathCharEq(char a, char b) {
@@ -1099,12 +1086,12 @@ bool platformKindNameOf(DvxKindE kind, const char *path, char *out, int32_t outS
char dir[DVX_MAX_PATH];
char absDir[DVX_MAX_PATH];
- if (!path || !platformPathDirName(path, fileDir, sizeof(fileDir)) || !pathAbsolute(fileDir, absFile, sizeof(absFile)) || !platformPathDirName(absFile, vendorDir, sizeof(vendorDir))) {
+ if (!path || !platformPathDirName(path, fileDir, sizeof(fileDir)) || !platformPathAbsolute(fileDir, absFile, sizeof(absFile)) || !platformPathDirName(absFile, vendorDir, sizeof(vendorDir))) {
return false;
}
for (int32_t i = 0; i < DVX_ARRAY_LEN(userFirst); i++) {
- if (!platformKindDir(dir, sizeof(dir), kind, userFirst[i]) || !pathAbsolute(dir, absDir, sizeof(absDir))) {
+ if (!platformKindDir(dir, sizeof(dir), kind, userFirst[i]) || !platformPathAbsolute(dir, absDir, sizeof(absDir))) {
continue;
}
@@ -1271,6 +1258,17 @@ void platformOutOfMemory(size_t bytes) {
}
+bool platformPathAbsolute(const char *path, char *out, int32_t outSize) {
+ char cwd[DVX_MAX_PATH];
+
+ if (platformPathIsAbsolute(path)) {
+ return snprintf(out, (size_t)outSize, "%s", path) < outSize;
+ }
+
+ return getcwd(cwd, sizeof(cwd)) && platformPathJoin(out, outSize, cwd, path);
+}
+
+
// The name after the last separator, or after a drive ("C:A.TXT").
const char *platformPathBaseName(const char *path) {
if (!path) {
diff --git a/src/libs/kpunch/libdvx/widgetLayout.c b/src/libs/kpunch/libdvx/widgetLayout.c
index 393e6752..e1ba622e 100644
--- a/src/libs/kpunch/libdvx/widgetLayout.c
+++ b/src/libs/kpunch/libdvx/widgetLayout.c
@@ -533,26 +533,9 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) {
}
}
- // Assign geometry
+ // Assign geometry: the box's whole cross size, but for a max size
int32_t crossSize = availCross;
- // Apply preferred size as cross-axis cap (e.g. Width in a VBox).
- // When set, the widget won't stretch beyond this on the cross axis.
- if (horiz && c->prefH) {
- int32_t prefPx = wgtResolveSize(c->prefH, innerH, charW);
-
- if (crossSize > prefPx) {
- crossSize = prefPx;
- }
- } else if (!horiz && c->prefW) {
- int32_t prefPx = wgtResolveSize(c->prefW, innerW, charW);
-
- if (crossSize > prefPx) {
- crossSize = prefPx;
- }
- }
-
- // Apply max size on cross axis
if (horiz && c->maxH) {
int32_t maxPx = wgtResolveSize(c->maxH, innerH, charW);
diff --git a/src/libs/kpunch/music/Makefile b/src/libs/kpunch/music/Makefile
index f384de93..a07bac2a 100644
--- a/src/libs/kpunch/music/Makefile
+++ b/src/libs/kpunch/music/Makefile
@@ -22,9 +22,9 @@
# Music Library Makefile for DJGPP cross-compilation
#
-# music.lib (see music.h): libxmp-lite (thirdparty/libxmp-lite, built
-# unmodified with warnings off, as its own options say below) and our own
-# sources. The files that run at interrupt time (ISR_SRCS) are linked
+# music.lib (see music.h): libxmp-lite (thirdparty/libxmp-lite, with
+# DVX's patches from its dvx-patches folder, built with warnings off, as
+# its own options say below) and our own sources. The files that run at interrupt time (ISR_SRCS) are linked
# last, so musicLock pins one span from their first function to the end
# of the library's data; gcc must not move parts of them elsewhere or
# turn loops into library calls. The OP2 instrument bank is copied
@@ -34,10 +34,11 @@ include ../../../common.mk
XMP_DIR = thirdparty/libxmp-lite
XMP_SRCS = $(wildcard $(XMP_DIR)/src/*.c) $(wildcard $(XMP_DIR)/src/loaders/*.c)
+XMP_HDRS = $(wildcard $(XMP_DIR)/include/*.h $(XMP_DIR)/src/*.h $(XMP_DIR)/src/loaders/*.h)
XMP_OPTS = -DLIBXMP_CORE_PLAYER -DLIBXMP_NO_PROWIZARD -DLIBXMP_NO_DEPACKERS -DBUILDING_STATIC -I$(XMP_DIR)/include -I$(XMP_DIR)/src
XMP_CFLAGS = $(filter-out $(WARNFLAGS) $(DEPFLAGS),$(DOS_CFLAGS)) -w -fno-analyzer $(XMP_OPTS)
-ISR_SRCS = musicFm.c musicMpu.c musicSeq.c
+ISR_SRCS = musicCents.c musicFm.c musicModTick.c musicMpu.c musicWave.c musicSeq.c
MAIN_SRCS = $(filter-out $(ISR_SRCS),$(wildcard *.c))
ISR_CFLAGS = -fno-reorder-blocks-and-partition -fno-reorder-functions -fno-tree-loop-distribute-patterns
CFLAGS = $(DOS_CFLAGS) $(ISR_CFLAGS) -I. -I$(XMP_DIR)/include -I../audio -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../libtasks -I../dvxshell
@@ -65,7 +66,7 @@ $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/music.dxe -U $(OBJS)
mv $(TARGETDIR)/music.dxe $@
-$(OBJDIR)/xmp/%.o: $(XMP_DIR)/%.c $(DVX_MAKEFILES)
+$(OBJDIR)/xmp/%.o: $(XMP_DIR)/%.c $(XMP_HDRS) $(DVX_MAKEFILES)
@mkdir -p $(dir $@)
$(CC) $(XMP_CFLAGS) -c -o $@ $<
diff --git a/src/libs/kpunch/music/music.c b/src/libs/kpunch/music/music.c
index 2e4ceae1..a6a921b1 100644
--- a/src/libs/kpunch/music/music.c
+++ b/src/libs/kpunch/music/music.c
@@ -31,12 +31,27 @@
// Where a MIDI music plays is decided at its first play (or when a live
// MIDI music opens): DVX.INI [audio] midi = mpu401 and the port is free:
// the MPU-401 port (one music at a time); = software: the software
-// synthesizer; otherwise the FM chip when there is one and no program has
-// it to itself, else the software synthesizer. On the FM chip and the
-// port the music timer (musicTick) plays the song; FM notes without a
-// voice go through the event ring to a software synthesizer on a
-// stream. With the software synthesizer the stream's fill function runs
-// the sequencer, SW_CHUNK_FRAMES frames at a time.
+// synthesizer; otherwise the wavetable synthesizer when there is one, its
+// SoundFont is there, and no other music plays on it (one at a time; a
+// stopped music gives it up and decides again at its next play), unless
+// midi = fm; else the FM chip when there is one and no program has it to
+// itself, else the software synthesizer. On the synthesizers and the
+// port the music timer (musicTick) plays the song; wavetable notes whose
+// samples did not fit go to the FM chip (or the software synthesizer),
+// and FM notes without a voice go through the event ring to a software
+// synthesizer on a stream. With the software synthesizer the stream's
+// fill function runs the sequencer, SW_CHUNK_FRAMES frames at a time.
+//
+// A module plays on the wavetable synthesizer's voices when it is free
+// in the same way (musicModHw.c: libxmp's voices as frames, made ahead
+// in musicPump and played by the music timer), else on a stream that
+// libxmp mixes in software.
+//
+// The SoundFont stays loaded, its samples in the synthesizer's memory,
+// from song to song (a song loads only what it lacks; a module's samples
+// that do not fit beside them put them out); the library's context,
+// which owns that memory, stays open FONT_KEEP_US after the last music
+// for the next one.
//
// The state a player is in lives in its IsrPlayerT for every kind of
// music, so there is one place to look: the timer, the fill functions
@@ -50,6 +65,7 @@
#include "musicInt.h"
#include "stb_ds.h"
+#include
#include
#include
#include
@@ -59,7 +75,6 @@
DVX_ABI_STAMP(music);
-#define MODULE_STREAM_MS 400
#define MIDI_STREAM_MS 150
#define SW_CHUNK_FRAMES 64
#define SW_VOICES SYNTH_VOICES_MAX
@@ -67,9 +82,30 @@ DVX_ABI_STAMP(music);
#define STREAM_CHANNELS 2
#define US_PER_SEC 1000000ULL
#define US_PER_MS 1000
+#define MS_PER_SEC 1000
#define GAIN_SHIFT 8
#define TIMER_NONE (-1)
#define ERROR_TEXT_UNKNOWN "Unknown error"
+#define FONT_KEEP_US (MUSIC_FONT_KEEP_SEC * US_PER_SEC)
+#define GM_BANK 0 // General MIDI's instruments; live MIDI starts every channel ...
+#define GM_PROGRAM_FIRST 0 // ... on the first (and channel 10 on the standard kit)
+
+// The SoundFont the wavetable synthesizer plays, with its samples in the
+// synthesizer's memory (the library's context's). Pinned with the
+// bank's arrays (the timer reads them).
+typedef struct {
+ WaveBankT bank;
+ WaveCacheT cache;
+ char name[DVX_MAX_PATH]; // as DVX.INI's soundFont names it
+} WaveFontT;
+
+// The SoundFont software synthesizer's, one per music: its own copy of
+// the SoundFont's headers, the song's samples in memory, where each is
+typedef struct {
+ WaveBankT bank;
+ WaveSoftT soft;
+ WaveLoadedT *loaded; // per sample of bank
+} SoftFontT;
struct MusicT {
IsrPlayerT isr; // first: the timer reads it (the whole struct is pinned)
@@ -79,9 +115,14 @@ struct MusicT {
MusicT *next;
MidiFileT file;
XmpPlayerT *xmp;
+ SoundSrcT *sound; // MUSIC_KIND_SOUND: the file's frames
FmSynthT *swSynth; // the software synthesizer (MIDI on it, or FM overflow)
+ WaveSynthT *wave; // MUSIC_OUT_WAVE, _SOFTWAVE (pinned)
+ uint8_t *waveNeed; // ... the samples it needs, per sample of the SoundFont
+ SoftFontT *softFont; // MUSIC_OUT_SOFTWAVE
SynthChipT *chip;
AudioStreamT *stream;
+ MusicBankT *bank; // MIDI: its instruments (a reference); before the first play, musicSetBank's choice or NULL
MusicEndFnT endFn;
void *endData;
uint64_t usAcc; // software sequencer: frames x 10^6 not yet turned into time
@@ -94,7 +135,10 @@ struct MusicT {
int32_t volume;
int32_t isrSlot; // in musicIsrPlayers, -1: none
bool loop;
+ bool atEnd; // a module played to its end: its next play starts over
bool draining; // rendered to its end, not yet heard to it
+ bool fmChip; // hwSynth plays (the music, or the wavetable synthesizer's overflow)
+ char soundFont[DVX_MAX_PATH]; // musicSetBank's SoundFont; "": DVX.INI's
};
static const char *sErrorText[MUSIC_ERR_COUNT] = {
@@ -102,68 +146,150 @@ static const char *sErrorText[MUSIC_ERR_COUNT] = {
"Invalid argument",
"Not enough memory",
"The file could not be read",
- "Not a music file (MOD, S3M, XM, IT, or MIDI)",
+ "Not a music or sound file (MOD, S3M, XM, IT, MIDI, WAV, or VOC)",
"The music file is damaged or incomplete",
"No sound device can play this music",
"The instrument bank for MIDI music is missing or damaged",
- "The music string has a mistake in it"
+ "The music string has a mistake in it",
+ NULL, // MUSIC_ERR_UNSUPPORTED: audio.lib's text (it decodes sounds)
+ "The file could not be written"
};
-static MusicT *sMusics = NULL;
-static AudioCtxT *sCtx = NULL;
-static Op2BankT *sBank = NULL;
-static MusicT *sMpuUser = NULL;
-static int32_t sTimerId = TIMER_NONE;
+static MusicT *sMusics = NULL;
+static AudioCtxT *sCtx = NULL;
+static MusicBankT *sBank = NULL; // DVX.INI's choice as last loaded (a reference)
+static MusicT *sMpuUser = NULL;
+static int32_t sTimerId = TIMER_NONE;
+static WaveFontT *sWave = NULL; // the SoundFont loaded for the wavetable synthesizer
+static MusicT *sWaveUser = NULL; // the music on it
+static uint64_t sQuietSince = 0; // when the last music went (sCtx kept for sWave)
+static char sFontFailed[DVX_MAX_PATH]; // the SoundFont last found missing or damaged (said once)
// ============================================================
// Prototypes
// ============================================================
-static bool bankRead(const char *path, Op2BankT *bank);
-static int32_t ctxEnsure(void);
-static int32_t decideMidi(MusicT *m);
-static int32_t gainOf(const MusicT *m);
-static void isrAdd(MusicT *m);
-static void isrRemove(MusicT *m);
-static bool isrSlotFree(void);
-static uint32_t midiFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
-static void midiSeek(MusicT *m, uint32_t us);
-static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
-int32_t musicAppCurrent(void);
-void musicAppEnded(int32_t appId);
-int32_t musicAppEnter(int32_t appId);
-const Op2BankT *musicBank(int32_t *err);
-const char *musicErrorText(int32_t err);
-void musicFree(MusicT *music);
-void musicGetInfo(const MusicT *music, MusicInfoT *info);
-uint32_t musicGetPosition(const MusicT *music);
-int32_t musicGetVolume(const MusicT *music);
-void musicIdle(void *unused);
-bool musicIsPlaying(const MusicT *music);
-int32_t musicLoad(const char *path, MusicT **out);
-int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out);
-int32_t musicMidiControl(MusicT *music, int32_t channel, int32_t controller, int32_t value);
-int32_t musicMidiNoteOff(MusicT *music, int32_t channel, int32_t note);
-int32_t musicMidiNoteOn(MusicT *music, int32_t channel, int32_t note, int32_t velocity);
-int32_t musicMidiOpen(MusicT **out);
-int32_t musicMidiPitchBend(MusicT *music, int32_t channel, int32_t bend);
-int32_t musicMidiProgram(MusicT *music, int32_t channel, int32_t program);
-static MusicT *musicNew(int32_t kind);
-int32_t musicPause(MusicT *music);
-int32_t musicPlay(MusicT *music, bool loop);
-void musicPump(void);
-int32_t musicResume(MusicT *music);
-void musicSetEndFn(MusicT *music, MusicEndFnT fn, void *userData);
-int32_t musicSetPosition(MusicT *music, uint32_t ms);
-int32_t musicSetVolume(MusicT *music, int32_t volume);
-void musicShutdown(void);
-int32_t musicStop(MusicT *music);
-static int32_t openSoftware(MusicT *m);
-static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill);
-static int32_t outputOpen(MusicT *m);
-static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1, int32_t data2);
-static void silence(MusicT *m);
-static void timerUpdate(void);
+static MusicBankT *bankLoad(const char *name, bool fallback, int32_t *err);
+static bool bankRead(const char *path, Op2BankT *bank);
+static void bankRelease(MusicBankT *bank);
+static MusicBankT *bankShared(const char *name, bool current, int32_t *err);
+static void ctxClose(void);
+static int32_t ctxEnsure(void);
+static int32_t decideMidi(MusicT *m);
+static int32_t fontLoad(const char *want, WaveBankT *bank);
+static const char *fontName(const char *name);
+static void fontSaid(const char *want, int32_t rc);
+static int32_t gainOf(const MusicT *m);
+static void isrAdd(MusicT *m);
+static void isrRemove(MusicT *m);
+static bool isrSlotFree(void);
+static uint32_t midiFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
+static void midiSeek(MusicT *m, uint32_t us);
+static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
+static int32_t moduleOpen(MusicT *m);
+static int32_t moduleWave(MusicT *m);
+int32_t musicAppCurrent(void);
+void musicAppEnded(int32_t appId);
+int32_t musicAppEnter(int32_t appId);
+const char *musicErrorText(int32_t err);
+void musicFree(MusicT *music);
+void musicGetInfo(const MusicT *music, MusicInfoT *info);
+uint32_t musicGetPosition(const MusicT *music);
+int32_t musicGetVolume(const MusicT *music);
+void musicIdle(void *unused);
+bool musicIsPlaying(const MusicT *music);
+int32_t musicLoad(const char *path, MusicT **out);
+int32_t musicLoadMacro(const char *macro, MusicT **out);
+int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out);
+int32_t musicMidiControl(MusicT *music, int32_t channel, int32_t controller, int32_t value);
+int32_t musicMidiNoteOff(MusicT *music, int32_t channel, int32_t note);
+int32_t musicMidiNoteOn(MusicT *music, int32_t channel, int32_t note, int32_t velocity);
+int32_t musicMidiOpen(MusicT **out);
+int32_t musicMidiPitchBend(MusicT *music, int32_t channel, int32_t bend);
+int32_t musicMidiProgram(MusicT *music, int32_t channel, int32_t program);
+int32_t musicMidiSend(MusicT *music, uint8_t status, uint8_t data1, uint8_t data2);
+static MusicT *musicNew(int32_t kind);
+int32_t musicPause(MusicT *music);
+int32_t musicPlay(MusicT *music, bool loop);
+void musicPump(void);
+int32_t musicResume(MusicT *music);
+int32_t musicSetBank(MusicT *music, const char *bank);
+void musicSetEndFn(MusicT *music, MusicEndFnT fn, void *userData);
+int32_t musicSetPosition(MusicT *music, uint32_t ms);
+int32_t musicSetVolume(MusicT *music, int32_t volume);
+void musicShutdown(void);
+int32_t musicStop(MusicT *music);
+void musicSuspend(bool suspending);
+static int32_t openSoftware(MusicT *m);
+static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill);
+static void outputClose(MusicT *m);
+static int32_t outputOpen(MusicT *m);
+static void rewindToStart(MusicT *m);
+static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1, int32_t data2);
+static void silence(MusicT *m);
+static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want);
+static int32_t softOpen(MusicT *m, const char *font);
+static uint32_t soundFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
+static bool soundFontValid(const char *name);
+static uint32_t soundHeard(const MusicT *m);
+static int32_t soundMusic(SoundSrcT *src, MusicT **out);
+static void streamHold(const MusicT *m, bool hold);
+static void timerUpdate(void);
+static bool waveBankLock(const WaveBankT *bank, bool lock);
+static bool waveClaim(AudioWtInfoT *info);
+static void waveDrop(MusicT *m);
+static void waveFontFree(WaveFontT *font);
+static int32_t waveFontUse(const char *name);
+static void waveNeedsSong(const MusicT *m, const WaveBankT *bank, uint8_t *need);
+static int32_t waveOpen(MusicT *m, const char *font, MidiSinkT *overflow);
+static void waveProgram(MusicT *m, int32_t channel, int32_t program);
+static int32_t waveSynthNew(MusicT *m, uint32_t samples);
+
+
+// A bank named as DVX.INI's midiBank names one ("" = the standard one),
+// with one reference and pinned, or NULL with *err. fallback: the
+// standard one in place of a named one that is missing or damaged.
+static MusicBankT *bankLoad(const char *name, bool fallback, int32_t *err) {
+ MusicBankT *bank = (MusicBankT *)calloc(1, sizeof(MusicBankT));
+ bool standard = !name[0];
+ bool ok = false;
+
+ if (!bank) {
+ *err = MUSIC_ERR_NO_MEMORY;
+ return NULL;
+ }
+
+ snprintf(bank->name, sizeof(bank->name), "%s", name);
+
+ if (!standard) {
+ char path[DVX_MAX_PATH];
+
+ ok = platformKindFind(path, sizeof(path), DvxKindBanksE, name) && bankRead(path, &bank->op2);
+
+ if (!ok && fallback) {
+ dvxLog("Music: the MIDI bank %s is missing or damaged; using the standard one", name);
+ standard = true;
+ }
+ }
+
+ if (standard) {
+ ok = bankRead(MUSIC_BANK_DEFAULT, &bank->op2);
+
+ if (!ok) {
+ dvxLog("Music: no usable MIDI instrument bank");
+ }
+ }
+
+ if (!ok || platformLockRegion(bank, sizeof(*bank), false) != 0) {
+ free(bank);
+ *err = ok ? MUSIC_ERR_NO_MEMORY : MUSIC_ERR_BANK;
+ return NULL;
+ }
+
+ bank->refs = 1;
+ *err = MUSIC_OK;
+ return bank;
+}
// An OP2 bank file into bank; false when it is missing or damaged.
@@ -177,6 +303,54 @@ static bool bankRead(const char *path, Op2BankT *bank) {
}
+// Drop a reference to bank; it goes with its last (the timer no longer
+// reads it: every music holding one has left the timer first). NULL is
+// ignored.
+static void bankRelease(MusicBankT *bank) {
+ if (bank && --bank->refs == 0) {
+ platformUnlockRegion(bank, sizeof(*bank), false);
+ free(bank);
+ }
+}
+
+
+// A reference to the bank name chooses. With current, name is DVX.INI's
+// choice: the library's own bank while it is still that, else the new
+// one, loaded in its place (the standard one when it is missing or
+// damaged), so a choice changed in DVX.INI reaches the next MIDI music to
+// start and the musics playing keep theirs. Without current
+// (musicSetBank) a bank that is missing or damaged is an error.
+static MusicBankT *bankShared(const char *name, bool current, int32_t *err) {
+ MusicBankT *bank;
+
+ if (sBank && strcasecmp(sBank->name, name) == 0) {
+ sBank->refs++;
+ *err = MUSIC_OK;
+ return sBank;
+ }
+
+ bank = bankLoad(name, current, err);
+
+ if (bank && current) {
+ bankRelease(sBank);
+ sBank = bank;
+ bank->refs++;
+ }
+
+ return bank;
+}
+
+
+// The library's context closed, and the SoundFont with it (its samples
+// are in the context's memory).
+static void ctxClose(void) {
+ waveFontFree(sWave);
+ sWave = NULL;
+ audioClose(sCtx);
+ sCtx = NULL;
+}
+
+
// The library's own context, opened for no program.
static int32_t ctxEnsure(void) {
if (!sCtx) {
@@ -191,16 +365,24 @@ static int32_t ctxEnsure(void) {
// Where a MIDI music plays (see the top); sets it up. Without an
-// instrument bank only the MPU-401 port can play it.
+// instrument bank only the MPU-401 port can play it. The banks are the
+// ones musicSetBank chose, else DVX.INI's choices as they are now.
static int32_t decideMidi(MusicT *m) {
PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
const char *want = audioMidiChoices[audioSettingMidi(prefs)].iniName;
int32_t mode = mpuModeChoices[prefsGetChoice(prefs, AUDIO_INI_SECTION, MUSIC_INI_MPU_MODE, mpuModeChoices, MPU_MODE_COUNT, mpuModeChoices[MPU_MODE_GM].iniName)].value;
bool mpu = strcasecmp(want, AUDIO_MIDI_MPU401) == 0;
bool soft = strcasecmp(want, AUDIO_MIDI_SOFTWARE) == 0;
+ bool sw = strcasecmp(want, AUDIO_MIDI_SOFTWAVE) == 0;
+ bool wave = !soft && !sw && strcasecmp(want, AUDIO_MIDI_FM) != 0;
+ char bank[DVX_MAX_PATH];
+ char font[DVX_MAX_PATH];
AudioFmInfoT fm;
+ MidiSinkT *fmSink;
int32_t rc;
+ snprintf(bank, sizeof(bank), "%s", prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK, ""));
+ snprintf(font, sizeof(font), "%s", m->soundFont[0] ? m->soundFont : prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_SOUND_FONT, ""));
prefsClose(prefs);
if (mpu && !sMpuUser && isrSlotFree() && audioMidiAcquire(sCtx) == AUDIO_OK) {
@@ -214,8 +396,12 @@ static int32_t decideMidi(MusicT *m) {
return MUSIC_OK;
}
- if (!musicBank(&rc)) {
- return rc;
+ if (!m->bank) {
+ m->bank = bankShared(bank, true, &rc);
+
+ if (!m->bank) {
+ return rc;
+ }
}
rc = openSoftware(m);
@@ -224,25 +410,89 @@ static int32_t decideMidi(MusicT *m) {
return rc;
}
- audioFmGetInfo(&fm);
-
- if (!soft && fm.voices > 0 && !fm.rawOwned && isrSlotFree()) {
- FmBackendT hw;
-
- m->where = MUSIC_OUT_FM;
- fmHwBackend(sCtx, &hw);
- fmSynthInit(&m->hwSynth, &hw, sBank, &m->ring.sink, fm.stereo, gainOf(m));
- m->isr.sink = &m->hwSynth.sink;
- isrAdd(m);
+ // The SoundFont in software: on the stream, after the FM software
+ // synthesizer (which plays what the bank lacks)
+ if (sw && softOpen(m, font) == MUSIC_OK) {
+ m->where = MUSIC_OUT_SOFTWAVE;
+ m->isr.sink = &m->wave->sink;
return MUSIC_OK;
}
- m->where = MUSIC_OUT_SOFTWARE;
- m->isr.sink = &m->swSynth->sink;
+ // FM notes: on the chip when there is one to share, else on the
+ // software synthesizer through the ring.
+ audioFmGetInfo(&fm);
+ m->fmChip = !soft && !sw && fm.voices > 0 && !fm.rawOwned && isrSlotFree();
+ fmSink = &m->ring.sink;
+
+ if (m->fmChip) {
+ FmBackendT hw;
+
+ fmHwBackend(sCtx, &hw);
+ fmSynthInit(&m->hwSynth, &hw, &m->bank->op2, &m->ring.sink, fm.stereo, gainOf(m));
+ fmSink = &m->hwSynth.sink;
+ }
+
+ if (wave && waveOpen(m, font, fmSink) == MUSIC_OK) {
+ m->where = MUSIC_OUT_WAVE;
+ m->isr.sink = &m->wave->sink;
+ m->isr.wave = m->wave;
+ } else if (m->fmChip) {
+ m->where = MUSIC_OUT_FM;
+ m->isr.sink = fmSink;
+ } else {
+ m->where = MUSIC_OUT_SOFTWARE;
+ m->isr.sink = &m->swSynth->sink;
+ return MUSIC_OK;
+ }
+
+ isrAdd(m);
return MUSIC_OK;
}
+// The SoundFont want names (as DVX.INI's soundFont does) into bank: a
+// SoundFont, or an UltraSound's patches by their ULTRASND.INI; for the
+// standard one when it is not installed, the UltraSound's own patches,
+// else the AWE's ROM bank. MUSIC_ERR_OPEN when none is found.
+static int32_t fontLoad(const char *want, WaveBankT *bank) {
+ char path[DVX_MAX_PATH];
+
+ if (platformKindFind(path, sizeof(path), DvxKindBanksE, want)) {
+ return strcasecmp(platformPathBaseName(path), MUSIC_PATCH_SET_INI) == 0 ? patLoadSet(path, bank) : sf2Load(path, bank);
+ }
+
+ if (strcasecmp(want, MUSIC_SOUND_FONT_DEFAULT) != 0) {
+ return MUSIC_ERR_OPEN;
+ }
+
+ if (musicPatchSetPath(path, sizeof(path))) {
+ return patLoadSet(path, bank);
+ }
+
+ return musicRomBankPath(path, sizeof(path)) ? sf2Load(path, bank) : MUSIC_ERR_OPEN;
+}
+
+
+// A SoundFont's name as given, or the standard one's for "".
+static const char *fontName(const char *name) {
+ return name[0] ? name : MUSIC_SOUND_FONT_DEFAULT;
+}
+
+
+// A SoundFont found missing or damaged (rc) is said once, until one loads.
+static void fontSaid(const char *want, int32_t rc) {
+ if (rc == MUSIC_OK) {
+ sFontFailed[0] = '\0';
+ return;
+ }
+
+ if (strcasecmp(sFontFailed, want) != 0) {
+ dvxLog("Music: SoundFont %s: %s; MIDI music plays on the FM synthesizer", want, musicErrorText(rc));
+ snprintf(sFontFailed, sizeof(sFontFailed), "%s", want);
+ }
+}
+
+
// The music's volume times the master volume, for synthesizers the mixer
// does not scale.
static int32_t gainOf(const MusicT *m) {
@@ -293,36 +543,49 @@ static bool isrSlotFree(void) {
}
-// The software synthesizer's stream: overflow notes from the FM chip, or
-// the song itself, SW_CHUNK_FRAMES at a time.
+// The software synthesizer's stream: overflow notes from the FM chip or
+// the wavetable synthesizer (the ring), or the song itself (on the FM
+// software synthesizer, or the SoundFont one with the FM one for what
+// its bank lacks), SW_CHUNK_FRAMES at a time. Nothing while paused:
+// the stream's voice is held with what it has.
static uint32_t midiFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) {
MusicT *m = (MusicT *)userData;
int16_t *out = (int16_t *)buf;
(void)stream;
- if (m->where == MUSIC_OUT_FM) {
- eventRingDrain(&m->ring, &m->swSynth->sink);
+ if (m->isr.state == PLAY_PAUSED) {
+ return 0;
}
+ eventRingDrain(&m->ring, &m->swSynth->sink);
+
for (uint32_t done = 0; done < frames;) {
uint32_t n = frames - done < SW_CHUNK_FRAMES ? frames - done : SW_CHUNK_FRAMES;
- if (m->where == MUSIC_OUT_SOFTWARE && m->isr.state == PLAY_PLAYING && !m->draining) {
+ if ((m->where == MUSIC_OUT_SOFTWARE || m->where == MUSIC_OUT_SOFTWAVE) && m->isr.state == PLAY_PLAYING && !m->draining) {
m->usAcc += (uint64_t)n * US_PER_SEC;
uint32_t us = (uint32_t)(m->usAcc / m->rate);
m->usAcc -= (uint64_t)us * m->rate;
- if (!seqAdvance(&m->isr.seq, us, &m->swSynth->sink)) {
- m->swSynth->sink.silence(&m->swSynth->sink);
+ if (!seqAdvance(&m->isr.seq, us, m->isr.sink)) {
+ m->isr.sink->silence(m->isr.sink);
m->draining = true;
m->endFrame = m->framesOut + done + n;
}
+
+ if (m->softFont) {
+ waveSynthTick(m->wave, us);
+ }
}
synthRender(m->chip, out + done * STREAM_CHANNELS, n, STREAM_CHANNELS);
+
+ if (m->softFont) {
+ waveSoftRender(&m->softFont->soft, out + done * STREAM_CHANNELS, n, STREAM_CHANNELS);
+ }
done += n;
}
@@ -341,11 +604,17 @@ static void midiSeek(MusicT *m, uint32_t us) {
}
+// A module's frames; silence when stopped or at its end, nothing while
+// paused (as midiFill).
static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) {
MusicT *m = (MusicT *)userData;
(void)stream;
+ if (m->isr.state == PLAY_PAUSED) {
+ return 0;
+ }
+
if (m->isr.state != PLAY_PLAYING || m->draining) {
memset(buf, 0, (size_t)frames * STREAM_CHANNELS * sizeof(int16_t));
} else if (!xmpRender(m->xmp, (int16_t *)buf, frames, m->loop)) {
@@ -358,6 +627,55 @@ static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, voi
}
+// Where a module plays: on the wavetable synthesizer's voices when it can
+// (moduleWave) and DVX.INI [audio] modules does not say software, else on
+// a stream mixed in software.
+static int32_t moduleOpen(MusicT *m) {
+ PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ const char *want = audioModulesChoices[audioSettingModules(prefs)].iniName;
+ bool soft = strcasecmp(want, AUDIO_MODULES_SOFTWARE) == 0;
+
+ prefsClose(prefs);
+
+ if (!soft && moduleWave(m) == MUSIC_OK) {
+ return MUSIC_OK;
+ }
+
+ m->where = MUSIC_OUT_STREAM;
+ return openStream(m, MUSIC_MODULE_AHEAD_MS, moduleFill);
+}
+
+
+// The module on the wavetable synthesizer's voices, when it is free (see
+// waveClaim) and the module plays there (modHwOpen); the SoundFont kept
+// in the synthesizer's memory gives way when the samples do not fit
+// beside it.
+static int32_t moduleWave(MusicT *m) {
+ AudioWtInfoT info;
+
+ if (!waveClaim(&info)) {
+ return MUSIC_ERR_NO_DEVICE;
+ }
+
+ int32_t rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod);
+
+ if (rc == MODHW_ERR_ROOM && sWave) {
+ waveFontFree(sWave);
+ sWave = NULL;
+ rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod);
+ }
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ sWaveUser = m;
+ m->where = MUSIC_OUT_WAVE;
+ isrAdd(m);
+ return MUSIC_OK;
+}
+
+
// The program running now (0: none, or no shell).
int32_t musicAppCurrent(void) {
return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
@@ -368,6 +686,7 @@ int32_t musicAppCurrent(void) {
void musicAppEnded(int32_t appId) {
musicBasForget(appId);
musicMmlAppEnded(appId);
+ musicMidiRecordAppEnded(appId);
MusicT *m = sMusics;
@@ -395,56 +714,22 @@ int32_t musicAppEnter(int32_t appId) {
}
-// Loaded once (DVX.INI [audio] midiBank) and pinned (the timer reads it).
-const Op2BankT *musicBank(int32_t *err) {
- if (sBank) {
- *err = MUSIC_OK;
- return sBank;
- }
-
- PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
- const char *setting = prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK, "");
- char path[DVX_MAX_PATH];
- Op2BankT *bank = (Op2BankT *)malloc(sizeof(Op2BankT));
- bool ok = false;
-
- *err = bank ? MUSIC_ERR_BANK : MUSIC_ERR_NO_MEMORY;
-
- // The bank DVX.INI names (in the banks folders, or by its path); the
- // bundled one when it names none, or one that is gone or damaged
- if (bank && setting[0]) {
- ok = platformKindFind(path, sizeof(path), DvxKindBanksE, setting) && bankRead(path, bank);
-
- if (!ok) {
- dvxLog("Music: the MIDI bank %s is missing or damaged; using the standard one", setting);
- }
- }
-
- ok = ok || (bank && bankRead(MUSIC_BANK_DEFAULT, bank));
- prefsClose(prefs);
-
- if (ok && platformLockRegion(bank, sizeof(Op2BankT), false) == 0) {
- sBank = bank;
- *err = MUSIC_OK;
- } else {
- dvxLog("Music: no usable MIDI instrument bank");
- free(bank);
- }
-
- return sBank;
-}
-
-
// Anything from MUSIC_OK up is no error.
const char *musicErrorText(int32_t err) {
if (err >= MUSIC_OK) {
return sErrorText[MUSIC_OK];
}
+ if (err == MUSIC_ERR_UNSUPPORTED) {
+ return audioErrorText(AUDIO_ERR_UNSUPPORTED);
+ }
+
return err > -MUSIC_ERR_COUNT ? sErrorText[-err] : ERROR_TEXT_UNKNOWN;
}
+// The context stays open a while for the SoundFont's samples (musicIdle
+// closes it).
void musicFree(MusicT *music) {
if (!music) {
return;
@@ -452,19 +737,7 @@ void musicFree(MusicT *music) {
MusicT *m = music;
- isrRemove(m);
- silence(m);
- audioStreamClose(m->stream);
-
- // The port gets what is queued (the notes off) before it is let go.
- if (sMpuUser == m) {
- for (uint32_t i = 0; i < MPU_QUEUE_BYTES && m->mpu.tail != m->mpu.head; i++) {
- mpuSinkDrain(&m->mpu, MPU_SYSEX_GAP_US);
- }
-
- audioMidiRelease(sCtx);
- sMpuUser = NULL;
- }
+ outputClose(m);
for (MusicT **mp = &sMusics; *mp; mp = &(*mp)->next) {
if (*mp == m) {
@@ -480,14 +753,17 @@ void musicFree(MusicT *music) {
midiFileFree(&m->file);
xmpClose(m->xmp);
- free(m->swSynth);
- free(m->chip);
+ soundClose(m->sound);
+ bankRelease(m->bank);
platformUnlockRegion(m, sizeof(*m), false);
free(m);
if (!sMusics && sCtx) {
- audioClose(sCtx);
- sCtx = NULL;
+ sQuietSince = platformMicros();
+
+ if (!sWave) {
+ ctxClose();
+ }
}
}
@@ -501,6 +777,7 @@ void musicGetInfo(const MusicT *music, MusicInfoT *info) {
AudioFmInfoT fm;
AudioMidiInfoT port;
+ AudioWtInfoT wt;
info->kind = music->kind;
info->where = music->where;
@@ -512,7 +789,10 @@ void musicGetInfo(const MusicT *music, MusicInfoT *info) {
if (music->xmp) {
snprintf(info->title, sizeof(info->title), "%s", xmpTitle(music->xmp));
info->lengthMs = xmpLength(music->xmp);
+ } else if (music->sound) {
+ info->lengthMs = (uint32_t)((uint64_t)soundFrames(music->sound) * MS_PER_SEC / soundRate(music->sound));
} else {
+ snprintf(info->title, sizeof(info->title), "%s", music->file.title);
info->lengthMs = music->file.length / US_PER_MS;
}
@@ -531,6 +811,15 @@ void musicGetInfo(const MusicT *music, MusicInfoT *info) {
snprintf(info->output, sizeof(info->output), "FM software synthesizer");
break;
+ case MUSIC_OUT_WAVE:
+ audioWtGetInfo(&wt);
+ snprintf(info->output, sizeof(info->output), "%s", wt.description);
+ break;
+
+ case MUSIC_OUT_SOFTWAVE:
+ snprintf(info->output, sizeof(info->output), "SoundFont software synthesizer");
+ break;
+
case MUSIC_OUT_STREAM:
snprintf(info->output, sizeof(info->output), "Sound device");
break;
@@ -546,6 +835,14 @@ uint32_t musicGetPosition(const MusicT *music) {
return 0;
}
+ if (music->isr.mod) {
+ return modHwPosition(music->isr.mod);
+ }
+
+ if (music->sound) {
+ return (uint32_t)((uint64_t)soundHeard(music) * MS_PER_SEC / soundRate(music->sound));
+ }
+
return music->xmp ? xmpPosition(music->xmp) : music->isr.seq.now / US_PER_MS;
}
@@ -555,10 +852,15 @@ int32_t musicGetVolume(const MusicT *music) {
}
+// After a while with no music the SoundFont's samples are let go.
void musicIdle(void *unused) {
(void)unused;
musicMmlIdle();
musicPump();
+
+ if (!sMusics && sCtx && platformMicros() - sQuietSince >= FONT_KEEP_US) {
+ ctxClose();
+ }
}
@@ -567,26 +869,84 @@ bool musicIsPlaying(const MusicT *music) {
}
+// A WAV is read as it plays (soundOpenFile); anything else is read whole.
int32_t musicLoad(const char *path, MusicT **out) {
- int32_t len = 0;
- char *data = path ? platformReadFile(path, &len) : NULL;
+ SoundSrcT *src = NULL;
+ int32_t len = 0;
+ int32_t rc;
*out = NULL;
- if (!data) {
- return path ? MUSIC_ERR_OPEN : MUSIC_ERR_ARG;
+ if (!path) {
+ return MUSIC_ERR_ARG;
}
- int32_t rc = musicLoadMem(data, (uint32_t)len, out);
+ rc = soundOpenFile(path, &src);
+
+ if (rc == MUSIC_OK) {
+ return soundMusic(src, out);
+ }
+
+ if (rc != MUSIC_ERR_FORMAT) {
+ return rc;
+ }
+
+ char *data = platformReadFile(path, &len);
+
+ if (!data) {
+ return MUSIC_ERR_OPEN;
+ }
+
+ rc = musicLoadMem(data, (uint32_t)len, out);
free(data);
return rc;
}
-// MIDI by its header; anything else is offered to libxmp.
+// The macro's notes as a MIDI file (mmlSmfBuild), loaded like one.
+// Notes with no instrument (before any I, or after I0) are the piano's.
+int32_t musicLoadMacro(const char *macro, MusicT **out) {
+ MmlStateT st;
+ MmlNoteT *notes = NULL;
+ uint8_t *smf = NULL;
+ uint32_t size = 0;
+ int32_t rc;
+
+ *out = NULL;
+
+ if (!macro) {
+ return MUSIC_ERR_ARG;
+ }
+
+ mmlStateInit(&st);
+ rc = mmlParse(&st, macro, ¬es);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(notes); i++) {
+ if (notes[i].program == MML_PROGRAM_TONE) {
+ notes[i].program = MML_PROGRAM_PIANO;
+ }
+ }
+
+ if (rc == MUSIC_OK) {
+ rc = mmlSmfBuild(notes, (int32_t)arrlen(notes), &smf, &size);
+ }
+
+ if (rc == MUSIC_OK) {
+ rc = musicLoadMem(smf, size, out);
+ }
+
+ free(smf);
+ arrfree(notes);
+ return rc;
+}
+
+
+// A WAV or VOC file, MIDI by its header; anything else is offered to
+// libxmp.
int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) {
const uint8_t *bytes = (const uint8_t *)data;
+ SoundSrcT *src = NULL;
*out = NULL;
@@ -594,8 +954,15 @@ int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) {
return MUSIC_ERR_ARG;
}
- bool midi = midiIsFile(bytes, size);
- int32_t rc = ctxEnsure();
+ int32_t rc = soundOpenMem(bytes, size, &src);
+
+ if (rc != MUSIC_ERR_FORMAT) {
+ return rc == MUSIC_OK ? soundMusic(src, out) : rc;
+ }
+
+ bool midi = midiIsFile(bytes, size);
+
+ rc = ctxEnsure();
if (rc != MUSIC_OK) {
return rc;
@@ -694,6 +1061,17 @@ int32_t musicMidiProgram(MusicT *music, int32_t channel, int32_t program) {
}
+int32_t musicMidiSend(MusicT *music, uint8_t status, uint8_t data1, uint8_t data2) {
+ if ((status & MIDI_STATUS_BIT) == 0 || status >= MIDI_SYSEX) {
+ return MUSIC_ERR_ARG;
+ }
+
+ uint8_t type = status & MIDI_TYPE_MASK;
+
+ return sendMidi(music, status & MIDI_CHANNEL_MASK, type, data1, MIDI_ONE_DATA_BYTE(type) ? 0 : data2);
+}
+
+
// A new music of the calling program, pinned (the timer may read it),
// in the list; NULL when out of memory.
static MusicT *musicNew(int32_t kind) {
@@ -726,6 +1104,7 @@ int32_t musicPause(MusicT *music) {
music->isr.state = PLAY_PAUSED;
silence(music);
+ streamHold(music, true);
return MUSIC_OK;
}
@@ -750,11 +1129,16 @@ int32_t musicPlay(MusicT *music, bool loop) {
if (m->draining) {
m->draining = false;
m->isr.state = PLAY_STOPPED;
+ m->atEnd = m->xmp || m->sound;
}
- if (m->xmp) {
- if (m->isr.state == PLAY_STOPPED && xmpPosition(m->xmp) > 0) {
- xmpRestart(m->xmp);
+ if (m->xmp || m->sound) {
+ if (m->isr.state == PLAY_STOPPED && (m->atEnd || m->isr.ended)) {
+ rewindToStart(m);
+ }
+
+ if (m->isr.mod) {
+ modHwFill(m->isr.mod, m->xmp, loop);
}
} else {
m->isr.seq.loop = loop || m->kind == MUSIC_KIND_LIVE;
@@ -764,6 +1148,12 @@ int32_t musicPlay(MusicT *music, bool loop) {
}
}
+ // The silence a stopped stream was given is not heard first.
+ if (m->isr.state == PLAY_STOPPED) {
+ audioStreamFlush(m->stream);
+ }
+
+ streamHold(m, false);
m->isr.ended = false;
m->isr.state = PLAY_PLAYING;
return MUSIC_OK;
@@ -784,6 +1174,10 @@ void musicPump(void) {
m->isr.state = PLAY_STOPPED;
m->isr.ended = true;
}
+
+ if (m->isr.mod && m->isr.state == PLAY_PLAYING) {
+ modHwFill(m->isr.mod, m->xmp, m->loop);
+ }
}
while (again) {
@@ -795,6 +1189,7 @@ void musicPump(void) {
}
m->isr.ended = false;
+ m->atEnd = m->xmp || m->sound;
if (m->endFn) {
int32_t prev = musicAppEnter(m->appId);
@@ -814,11 +1209,44 @@ int32_t musicResume(MusicT *music) {
return MUSIC_ERR_ARG;
}
+ streamHold(music, false);
music->isr.state = PLAY_PLAYING;
return MUSIC_OK;
}
+// Before the first play: the bank goes with the music (or back to
+// DVX.INI's choices for NULL), read now so a missing one is said now. A
+// SoundFont is for the wavetable synthesizer (the FM bank stays), an OP2
+// bank for the FM ones.
+int32_t musicSetBank(MusicT *music, const char *bank) {
+ MusicBankT *chosen = NULL;
+ int32_t rc = MUSIC_OK;
+
+ if (!music || music->kind != MUSIC_KIND_MIDI || music->where != MUSIC_OUT_NONE) {
+ return MUSIC_ERR_ARG;
+ }
+
+ if (bank && soundFontValid(bank)) {
+ snprintf(music->soundFont, sizeof(music->soundFont), "%s", bank);
+ return MUSIC_OK;
+ }
+
+ if (bank) {
+ chosen = bankShared(bank, false, &rc);
+
+ if (!chosen) {
+ return rc;
+ }
+ }
+
+ music->soundFont[0] = '\0';
+ bankRelease(music->bank);
+ music->bank = chosen;
+ return MUSIC_OK;
+}
+
+
void musicSetEndFn(MusicT *music, MusicEndFnT fn, void *userData) {
if (music) {
music->endFn = fn;
@@ -832,18 +1260,30 @@ int32_t musicSetPosition(MusicT *music, uint32_t ms) {
return MUSIC_ERR_ARG;
}
- if (music->xmp) {
+ if (music->sound) {
+ soundSeek(music->sound, (uint32_t)((uint64_t)ms * soundRate(music->sound) / MS_PER_SEC));
+ } else if (music->xmp) {
xmpSeek(music->xmp, ms);
- return MUSIC_OK;
- }
- int32_t rc = outputOpen(music);
+ if (music->isr.mod) {
+ modHwFlush(music->isr.mod, xmpPosition(music->xmp));
+ modHwFill(music->isr.mod, music->xmp, music->loop);
+ }
+ } else {
+ int32_t rc = outputOpen(music);
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
- if (rc == MUSIC_OK) {
midiSeek(music, ms * US_PER_MS);
}
- return rc;
+ // What the stream held is from the old place: heard no more
+ audioStreamFlush(music->stream);
+ music->atEnd = false;
+ music->draining = false;
+ return MUSIC_OK;
}
@@ -860,9 +1300,19 @@ int32_t musicSetVolume(MusicT *music, int32_t volume) {
uint32_t flags = platformIrqSave();
- if (music->where == MUSIC_OUT_FM) {
+ if (music->fmChip) {
fmSynthSetGain(&music->hwSynth, gainOf(music));
- } else if (music->where == MUSIC_OUT_MPU401) {
+ }
+
+ if (music->isr.wave) {
+ waveSynthSetGain(music->wave, gainOf(music));
+ }
+
+ if (music->isr.mod) {
+ modHwSetGain(music->isr.mod, gainOf(music));
+ }
+
+ if (music->where == MUSIC_OUT_MPU401) {
mpuSinkSetGain(&music->mpu, gainOf(music));
}
@@ -871,7 +1321,8 @@ int32_t musicSetVolume(MusicT *music, int32_t volume) {
}
-// The library is being unloaded: free every music, the bank.
+// The library is being unloaded: free every music, the banks, the
+// context.
void musicShutdown(void) {
musicMmlShutdown();
@@ -879,11 +1330,12 @@ void musicShutdown(void) {
musicFree(sMusics);
}
- if (sBank) {
- platformUnlockRegion(sBank, sizeof(Op2BankT), false);
- free(sBank);
- sBank = NULL;
+ if (sCtx) {
+ ctxClose();
}
+
+ bankRelease(sBank);
+ sBank = NULL;
}
@@ -896,9 +1348,11 @@ int32_t musicStop(MusicT *music) {
music->isr.ended = false;
music->draining = false;
silence(music);
+ audioStreamFlush(music->stream);
+ streamHold(music, false);
- if (music->xmp) {
- xmpRestart(music->xmp);
+ if (music->xmp || music->sound) {
+ rewindToStart(music);
} else if (music->isr.sink) {
midiSeek(music, 0);
}
@@ -907,6 +1361,27 @@ int32_t musicStop(MusicT *music) {
}
+// DVX steps aside for a DOS program (a platform suspend hook, run before
+// audio.lib's): every music stops and lets go of where it plays (decided
+// again at its next play), and with none left the context closes, so
+// nothing of music.lib stays on the synthesizers the program may use.
+// Nothing to do once DVX is back.
+void musicSuspend(bool suspending) {
+ if (!suspending) {
+ return;
+ }
+
+ for (MusicT *m = sMusics; m; m = m->next) {
+ musicStop(m);
+ outputClose(m);
+ }
+
+ if (!sMusics && sCtx) {
+ ctxClose();
+ }
+}
+
+
// The software synthesizer (and its stream): for MIDI on it, or as the
// FM chip's overflow. Its gain is full: the stream's volume is the
// music's.
@@ -925,11 +1400,13 @@ static int32_t openSoftware(MusicT *m) {
synthInit(m->chip, m->rate, SW_VOICES);
synthBackend(m->chip, &soft);
- fmSynthInit(m->swSynth, &soft, sBank, NULL, true, MUSIC_VOLUME_MAX);
+ fmSynthInit(m->swSynth, &soft, &m->bank->op2, NULL, true, MUSIC_VOLUME_MAX);
return openStream(m, MIDI_STREAM_MS, midiFill);
}
+// A sound's stream has the file's rate and channels; the others are
+// stereo at the device's rate.
static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill) {
AudioInfoT info;
AudioFormatT fmt;
@@ -937,7 +1414,7 @@ static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill) {
audioGetInfo(&info);
fmt.rate = m->rate ? m->rate : info.rate;
fmt.bits = STREAM_BITS;
- fmt.channels = STREAM_CHANNELS;
+ fmt.channels = m->sound ? soundChannels(m->sound) : STREAM_CHANNELS;
int32_t rc = audioStreamOpen(sCtx, &fmt, bufferMs, fill, m, &m->stream);
@@ -951,6 +1428,49 @@ static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill) {
}
+// Undo outputOpen: the music leaves the timer, the synthesizers, its
+// stream, and the MPU-401 port or the wavetable synthesizer it held (a
+// module's samples leave its memory); where it plays is decided again at
+// the next need.
+static void outputClose(MusicT *m) {
+ isrRemove(m);
+ silence(m);
+ audioStreamClose(m->stream);
+
+ // The port gets what is queued (the notes off) before it is let go.
+ if (sMpuUser == m) {
+ for (uint32_t i = 0; i < MPU_QUEUE_BYTES && m->mpu.tail != m->mpu.head; i++) {
+ mpuSinkDrain(&m->mpu, MPU_SYSEX_GAP_US);
+ }
+
+ audioMidiRelease(sCtx);
+ sMpuUser = NULL;
+ }
+
+ if (sWaveUser == m) {
+ sWaveUser = NULL;
+ }
+
+ waveDrop(m);
+ modHwClose(m->isr.mod, m->xmp);
+ free(m->swSynth);
+ free(m->chip);
+ eventRingInit(&m->ring);
+ m->stream = NULL;
+ m->swSynth = NULL;
+ m->chip = NULL;
+ m->isr.sink = NULL;
+ m->isr.mpu = NULL;
+ m->isr.wave = NULL;
+ m->isr.mod = NULL;
+ m->fmChip = false;
+ m->draining = false;
+ m->usAcc = 0;
+ m->framesOut = 0;
+ m->where = MUSIC_OUT_NONE;
+}
+
+
// Where the music plays, decided and set up at the first need: a
// module's stream, or where MIDI goes.
static int32_t outputOpen(MusicT *m) {
@@ -961,24 +1481,43 @@ static int32_t outputOpen(MusicT *m) {
int32_t rc;
if (m->kind == MUSIC_KIND_MODULE) {
+ rc = moduleOpen(m);
+ } else if (m->kind == MUSIC_KIND_SOUND) {
m->where = MUSIC_OUT_STREAM;
- rc = openStream(m, MODULE_STREAM_MS, moduleFill);
+ rc = openStream(m, MUSIC_MODULE_AHEAD_MS, soundFill);
} else {
rc = decideMidi(m);
}
if (rc != MUSIC_OK) {
- m->where = MUSIC_OUT_NONE;
+ outputClose(m);
}
return rc;
}
+// A module or a sound back to its start (a module's frames queued for the
+// synthesizer dropped).
+static void rewindToStart(MusicT *m) {
+ if (m->xmp) {
+ xmpRestart(m->xmp);
+ } else {
+ soundSeek(m->sound, 0);
+ }
+
+ if (m->isr.mod) {
+ modHwFlush(m->isr.mod, 0);
+ }
+
+ m->atEnd = false;
+}
+
+
// A live channel message to wherever the music plays (with interrupts
// off while the timer may be sending too).
static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1, int32_t data2) {
- if (!m || m->kind == MUSIC_KIND_MODULE || channel < 0 || channel >= MUSIC_MIDI_CHANNELS || data1 < 0 || data1 > MUSIC_MIDI_DATA_MAX || data2 < 0 || data2 > MUSIC_MIDI_DATA_MAX) {
+ if (!m || m->kind == MUSIC_KIND_MODULE || m->kind == MUSIC_KIND_SOUND || channel < 0 || channel >= MUSIC_MIDI_CHANNELS || data1 < 0 || data1 > MUSIC_MIDI_DATA_MAX || data2 < 0 || data2 > MUSIC_MIDI_DATA_MAX) {
return MUSIC_ERR_ARG;
}
@@ -988,6 +1527,10 @@ static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1,
return rc;
}
+ if ((m->where == MUSIC_OUT_WAVE || m->where == MUSIC_OUT_SOFTWAVE) && type == MIDI_PROGRAM) {
+ waveProgram(m, channel, data1);
+ }
+
uint32_t flags = platformIrqSave();
m->isr.sink->message(m->isr.sink, (uint8_t)(type | channel), (uint8_t)data1, (uint8_t)data2);
@@ -996,8 +1539,14 @@ static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1,
}
-// Every sounding note off now (MIDI; modules stop by themselves).
+// Every sounding note off now (MIDI), a module's voices on the
+// synthesizer stopped (the frame playing starts them again when it plays
+// on; on a stream a module stops by itself).
static void silence(MusicT *m) {
+ if (m->isr.mod) {
+ modHwSilence(m->isr.mod);
+ }
+
if (m->isr.sink) {
uint32_t flags = platformIrqSave();
@@ -1007,6 +1556,176 @@ static void silence(MusicT *m) {
}
+// softOpen's SoftFontT sf filled: the SoundFont want names, the music's
+// renderer, the song's samples, the backend.
+static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want) {
+ int32_t rc = fontLoad(want, &sf->bank);
+ WaveBackendT be;
+
+ if (rc == MUSIC_OK) {
+ rc = waveSynthNew(m, sf->bank.sampleCount);
+ }
+
+ if (rc == MUSIC_OK) {
+ sf->loaded = (WaveLoadedT *)calloc(sf->bank.sampleCount ? sf->bank.sampleCount : 1, sizeof(WaveLoadedT));
+ rc = sf->loaded ? MUSIC_OK : MUSIC_ERR_NO_MEMORY;
+ }
+
+ if (rc == MUSIC_OK) {
+ for (uint32_t i = 0; i < sf->bank.sampleCount; i++) {
+ sf->loaded[i].addr = WAVE_NOT_LOADED;
+ }
+
+ waveNeedsSong(m, &sf->bank, m->waveNeed);
+ rc = waveSoftLoad(&sf->soft, &sf->bank, sf->loaded, m->waveNeed);
+ }
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ waveSoftBackend(&sf->soft, m->rate, &be);
+ waveSynthInit(m->wave, &be, &sf->bank, sf->loaded, &m->swSynth->sink, MUSIC_VOLUME_MAX);
+ return MUSIC_OK;
+}
+
+
+// The SoundFont software synthesizer for the music (DVX.INI midi =
+// softwave): its own copy of the SoundFont's headers, the song's samples
+// in memory at their own rates, mixed into the stream after the FM
+// software synthesizer, which plays the notes the bank lacks.
+static int32_t softOpen(MusicT *m, const char *font) {
+ const char *want = fontName(font);
+ SoftFontT *sf = (SoftFontT *)calloc(1, sizeof(SoftFontT));
+ int32_t rc = MUSIC_ERR_NO_MEMORY;
+
+ m->softFont = sf;
+
+ if (sf) {
+ rc = softLoad(m, sf, want);
+ }
+
+ fontSaid(want, rc);
+
+ if (rc != MUSIC_OK) {
+ waveDrop(m);
+ }
+
+ return rc;
+}
+
+
+// A sound's frames from where it is; at its end it starts over (loop),
+// or, as a module does, notes the frame and plays silence (draining). A
+// file cut short ends where it ends. Nothing while paused (as midiFill).
+static uint32_t soundFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData) {
+ MusicT *m = (MusicT *)userData;
+ int16_t *out = (int16_t *)buf;
+ uint32_t channels = soundChannels(m->sound);
+ uint32_t done = 0;
+
+ (void)stream;
+
+ if (m->isr.state == PLAY_PAUSED) {
+ return 0;
+ }
+
+ if (m->isr.state == PLAY_PLAYING && !m->draining) {
+ done = soundRead(m->sound, out, frames);
+
+ while (done < frames && m->loop && soundFrames(m->sound) > 0) {
+ soundSeek(m->sound, 0);
+
+ uint32_t got = soundRead(m->sound, out + (size_t)done * channels, frames - done);
+
+ done += got;
+
+ if (got == 0) {
+ break;
+ }
+ }
+
+ if (done < frames) {
+ m->draining = true;
+ m->endFrame = m->framesOut + done;
+ }
+ }
+
+ memset(out + (size_t)done * channels, 0, (size_t)(frames - done) * channels * sizeof(int16_t));
+ m->framesOut += frames;
+ return frames;
+}
+
+
+// The bank name names (as DVX.INI's soundFont would) is a whole
+// SoundFont (or patch set).
+static bool soundFontValid(const char *name) {
+ WaveBankT bank;
+ bool ok = fontLoad(name, &bank) == MUSIC_OK;
+
+ if (ok) {
+ waveBankFree(&bank);
+ }
+
+ return ok;
+}
+
+
+// The frame being heard: the frame next read less what the stream still
+// holds (a loop's turn counted back from the sound's end), or, once the
+// end is rendered, the end less what is still to be heard of it.
+// Stopped, the stream holds only silence: the frame next read.
+static uint32_t soundHeard(const MusicT *m) {
+ uint32_t frames = soundFrames(m->sound);
+ uint32_t next = soundTell(m->sound);
+ uint32_t queued = m->stream ? audioStreamQueued(m->stream) : 0;
+
+ if (m->isr.state == PLAY_STOPPED) {
+ return next;
+ }
+
+ if (m->draining) {
+ uint64_t heard = m->framesOut - queued;
+ uint64_t left = heard >= m->endFrame ? 0 : m->endFrame - heard;
+
+ return left >= frames ? 0 : frames - (uint32_t)left;
+ }
+
+ if (queued <= next) {
+ return next - queued;
+ }
+
+ return m->loop && frames > 0 ? frames - (queued - next) % frames : 0;
+}
+
+
+// A music for a sound file's frames (it owns them from now on, and frees
+// them when it cannot be made).
+static int32_t soundMusic(SoundSrcT *src, MusicT **out) {
+ int32_t rc = ctxEnsure();
+ MusicT *m = rc == MUSIC_OK ? musicNew(MUSIC_KIND_SOUND) : NULL;
+
+ if (!m) {
+ soundClose(src);
+ return rc == MUSIC_OK ? MUSIC_ERR_NO_MEMORY : rc;
+ }
+
+ m->sound = src;
+ m->rate = soundRate(src);
+ *out = m;
+ return MUSIC_OK;
+}
+
+
+// The music's stream held where it is (a pause) or let go on; no stream:
+// nothing.
+static void streamHold(const MusicT *m, bool hold) {
+ if (m->stream) {
+ audioVoicePause(sCtx, audioStreamVoice(m->stream), hold);
+ }
+}
+
+
// The timer runs while any music is on the synthesizers; the code it
// runs is pinned meanwhile.
static void timerUpdate(void) {
@@ -1028,3 +1747,234 @@ static void timerUpdate(void) {
musicLock(false);
}
}
+
+
+// The bank's arrays the synthesizer reads at interrupt time (presets,
+// regions, samples), each pinned or let go; all are tried, so a failed
+// pin is undone with the same call.
+static bool waveBankLock(const WaveBankT *bank, bool lock) {
+ const void *arrays[] = { bank->presets, bank->regions, bank->samples };
+ const uint32_t sizes[] = { bank->presetCount * sizeof(WavePresetT), bank->regionCount * sizeof(WaveRegionT), bank->sampleCount * sizeof(WaveSampleT) };
+ bool ok = true;
+
+ for (uint32_t i = 0; i < sizeof(arrays) / sizeof(arrays[0]); i++) {
+ if (arrays[i] && sizes[i]) {
+ int32_t rc = lock ? platformLockRegion(arrays[i], sizes[i], false) : platformUnlockRegion(arrays[i], sizes[i], false);
+
+ ok = ok && rc == 0;
+ }
+ }
+
+ return ok;
+}
+
+
+// The wavetable synthesizer for a music about to play on it (*info
+// filled): there, no other music playing on it (a stopped one gives it
+// up), and room for one more player on the timer.
+static bool waveClaim(AudioWtInfoT *info) {
+ audioWtGetInfo(info);
+
+ if (!info->present || (sWaveUser && sWaveUser->isr.state != PLAY_STOPPED)) {
+ return false;
+ }
+
+ if (sWaveUser) {
+ outputClose(sWaveUser);
+ }
+
+ return isrSlotFree();
+}
+
+
+// The music's wavetable synthesizer and what it holds let go (whatever
+// part of it there is).
+static void waveDrop(MusicT *m) {
+ if (m->wave) {
+ platformUnlockRegion(m->wave, sizeof(*m->wave), false);
+ }
+
+ if (m->softFont) {
+ waveSoftFree(&m->softFont->soft);
+ waveBankFree(&m->softFont->bank);
+ free(m->softFont->loaded);
+ }
+
+ free(m->softFont);
+ free(m->wave);
+ free(m->waveNeed);
+ m->softFont = NULL;
+ m->wave = NULL;
+ m->waveNeed = NULL;
+}
+
+
+// A SoundFont and its samples (in the library's context's memory) let go;
+// NULL is ignored.
+static void waveFontFree(WaveFontT *font) {
+ if (!font) {
+ return;
+ }
+
+ waveCacheFree(sCtx, &font->cache);
+ waveBankLock(&font->bank, false);
+ waveBankFree(&font->bank);
+ platformUnlockRegion(font, sizeof(*font), false);
+ free(font);
+}
+
+
+// sWave as name names ("": the standard one), read in place of another
+// (whose samples leave the synthesizer's memory). One that is missing or
+// damaged is said once.
+static int32_t waveFontUse(const char *name) {
+ const char *want = fontName(name);
+ int32_t rc;
+
+ if (sWave && strcasecmp(sWave->name, want) == 0) {
+ return MUSIC_OK;
+ }
+
+ waveFontFree(sWave);
+ sWave = NULL;
+
+ WaveFontT *font = (WaveFontT *)calloc(1, sizeof(WaveFontT));
+
+ if (!font || platformLockRegion(font, sizeof(*font), false) != 0) {
+ free(font);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ snprintf(font->name, sizeof(font->name), "%s", want);
+ rc = fontLoad(want, &font->bank);
+
+ if (rc == MUSIC_OK) {
+ rc = waveCacheInit(&font->cache, &font->bank);
+ }
+
+ if (rc == MUSIC_OK && !waveBankLock(&font->bank, true)) {
+ rc = MUSIC_ERR_NO_MEMORY;
+ }
+
+ fontSaid(want, rc);
+
+ if (rc != MUSIC_OK) {
+ waveFontFree(font);
+ return rc;
+ }
+
+ sWave = font;
+ return MUSIC_OK;
+}
+
+
+// The samples the music needs from bank: the song's, or for live MIDI
+// the first program and the standard kit (the rest as they are chosen).
+static void waveNeedsSong(const MusicT *m, const WaveBankT *bank, uint8_t *need) {
+ if (m->kind == MUSIC_KIND_LIVE) {
+ waveNeedsPreset(bank, GM_BANK, GM_PROGRAM_FIRST, need);
+ waveNeedsPreset(bank, SF_DRUM_BANK, GM_PROGRAM_FIRST, need);
+ } else {
+ waveNeedsFile(bank, &m->file, need);
+ }
+}
+
+
+// The music on the wavetable synthesizer, when it is free (waveClaim):
+// the SoundFont font names in place of another, the samples the song
+// plays (live MIDI: the first program and the standard kit; the rest as
+// they are chosen) in the synthesizer's memory. Notes whose samples do
+// not fit go to overflow.
+static int32_t waveOpen(MusicT *m, const char *font, MidiSinkT *overflow) {
+ AudioWtInfoT info;
+ WaveBackendT hw;
+ uint32_t missing = 0;
+ int32_t rc;
+
+ if (!waveClaim(&info)) {
+ return MUSIC_ERR_NO_DEVICE;
+ }
+
+ rc = waveFontUse(font);
+
+ if (rc == MUSIC_OK) {
+ rc = waveSynthNew(m, sWave->bank.sampleCount);
+ }
+
+ if (rc == MUSIC_OK) {
+ waveNeedsSong(m, &sWave->bank, m->waveNeed);
+ rc = waveLoadNeeded(sCtx, &sWave->bank, &sWave->cache, m->waveNeed, info.flags, &missing);
+
+ if (rc != MUSIC_OK) {
+ dvxLog("Music: the SoundFont's samples could not be put in the synthesizer: %s", musicErrorText(rc));
+ }
+ }
+
+ if (rc != MUSIC_OK) {
+ waveDrop(m);
+ return rc;
+ }
+
+ if (missing > 0) {
+ dvxLog("Music: %" PRIu32 " samples do not fit in the synthesizer's memory; their notes play on the FM synthesizer", missing);
+ }
+
+ waveHwBackend(sCtx, &hw);
+ waveSynthInit(m->wave, &hw, &sWave->bank, sWave->cache.loaded, overflow, gainOf(m));
+ sWaveUser = m;
+ return MUSIC_OK;
+}
+
+
+// A program chosen live on a wavetable synthesizer: its samples loaded
+// when some are not there yet (on the chip the notes sounding are
+// silenced first, as samples may move to make room).
+static void waveProgram(MusicT *m, int32_t channel, int32_t program) {
+ SoftFontT *sf = m->softFont;
+ const WaveBankT *bank = sf ? &sf->bank : &sWave->bank;
+ uint16_t bankNum = channel == MIDI_PERCUSSION ? SF_DRUM_BANK : m->wave->channels[channel].bankMsb;
+ AudioWtInfoT info;
+ uint32_t missing = 0;
+
+ if (waveNeedsPreset(bank, bankNum, (uint8_t)program, m->waveNeed) == 0) {
+ return;
+ }
+
+ // In software the samples join the others in memory
+ if (sf) {
+ if (waveSoftLoad(&sf->soft, bank, sf->loaded, m->waveNeed) != MUSIC_OK) {
+ dvxLog("Music: program %" PRId32 "'s samples could not be read; its notes play on the FM synthesizer", program);
+ }
+
+ return;
+ }
+
+ // Its notes off before samples move in the synthesizer's memory (the
+ // timer may be sending)
+ uint32_t flags = platformIrqSave();
+
+ m->wave->sink.silence(&m->wave->sink);
+ platformIrqRestore(flags);
+ audioWtGetInfo(&info);
+
+ if (waveLoadNeeded(sCtx, &sWave->bank, &sWave->cache, m->waveNeed, info.flags, &missing) != MUSIC_OK || missing > 0) {
+ dvxLog("Music: not every sample of program %" PRId32 " fits in the synthesizer's memory; those notes play on the FM synthesizer", program);
+ }
+}
+
+
+// The music's renderer (pinned: the timer may run it) and its needs for
+// a SoundFont of samples samples.
+static int32_t waveSynthNew(MusicT *m, uint32_t samples) {
+ WaveSynthT *wave = (WaveSynthT *)calloc(1, sizeof(WaveSynthT));
+
+ m->waveNeed = (uint8_t *)calloc(samples ? samples : 1, 1);
+
+ if (!wave || platformLockRegion(wave, sizeof(*wave), false) != 0) {
+ free(wave);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ m->wave = wave;
+ return m->waveNeed ? MUSIC_OK : MUSIC_ERR_NO_MEMORY;
+}
diff --git a/src/libs/kpunch/music/music.dhs b/src/libs/kpunch/music/music.dhs
index 406915ac..5a4b851e 100644
--- a/src/libs/kpunch/music/music.dhs
+++ b/src/libs/kpunch/music/music.dhs
@@ -31,17 +31,30 @@
.index musicStop
.index musicMidiOpen
.index musicMidiNoteOn
+.index musicMidiSend
+.index musicMidiRecordNew
+.index musicMidiRecordSave
+.index MIDI recording
+.index MidiRecordNew
+.index MusicMidiSend
+.index MUSIC_ERR_WRITE
.index MusicInfoT
.index MIDI
.index MOD
.index S3M
.index XM
.index IT
+.index WAV
+.index VOC
+.index MUSIC_KIND_SOUND
+.index MUSIC_ERR_UNSUPPORTED
.index General MIDI
.index MT-32
.index GENMIDI
.index OP2
.index libxmp-lite
+.index Modules on the wavetable synthesizer
+.index musicModHw
.index music.bas
.index MusicLoad
.index MusicPlay
@@ -50,12 +63,29 @@
.index MusicMmlT
.index Music macro language
.index musicBasPlay
+.index musicLoadMacro
+.index musicSetBank
+.index MusicLoadMacro
+.index MusicSetBank
+.index SoundFont
+.index SF2
+.index SBK
+.index DVXGM.SF2
+.index Wavetable MIDI
+.index Gravis UltraSound patches
+.index ULTRASND.INI
+.index SYNTHGM.SBK
+.index musicPatchSetPath
+.index musicRomBankPath
+.index MusicPatchSet$
+.index MusicRomBank$
+.index softwave
.h1 Music Library
-Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
+Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), WAV and VOC sound files, and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
-Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank is bin/libs/kpunch/music/genmidi.op2.
+Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).
.h2 Playing a File
@@ -71,30 +101,35 @@ if (rc != MUSIC_OK) {
}
.endcode
-musicLoad and musicLoadMem tell the kind by the contents: a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state), and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
+musicLoad and musicLoadMem tell the kind by the contents: a WAV or VOC sound file (MUSIC_KIND_SOUND) or a MIDI file (or a RIFF RMID file around one) by its header, anything else is offered to the module player. A WAV loaded by its path is read from the disk as it plays (only its header is read at the load), so a long recording needs no memory; a VOC, or a WAV given in memory, is decoded into memory (audioDecode). Errors: MUSIC_ERR_OPEN (unreadable file), MUSIC_ERR_FORMAT (not music DVX plays, or a type 2 MIDI file), MUSIC_ERR_UNSUPPORTED (a sound file that is compressed or not 8- or 16-bit; its text is audio.lib's), MUSIC_ERR_CORRUPT, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no sound device could start), MUSIC_ERR_BANK (MIDI with no usable instrument bank and no MPU-401 port). Nothing plays until musicPlay(music, loop), which plays from the current position (the start after a load or a stop; a music that played to its end starts over). musicPause / musicResume, musicStop (back to the start), musicIsPlaying, musicSetVolume (0..MUSIC_VOLUME_MAX, per music; the master volume applies too - on the FM chip and the MPU-401 port as it was when the music started or its volume was last set), musicGetPosition / musicSetPosition (milliseconds), musicGetInfo (kind, where it plays, a description of the device, title, length, position, state; the title is a module's own, a MIDI file's sequence name (the first track's name, control characters as spaces), and none for a sound), and musicFree complete the set. musicSetEndFn(music, fn, userData) asks for a call when a music that does not loop plays to its end; it comes in the shell's idle time as the program that owns the music, never from an interrupt.
-Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time.
+musicLoadMacro(macro, &music) makes a MIDI music from a music macro string (the language PLAY takes, below): its notes play on the General MIDI instruments its I commands name, the piano for notes before any I or after I0, and it is then played, paused, and freed like a MIDI file (MUSIC_ERR_MACRO for a mistake in the string). musicSetBank(music, bank) plays one MIDI music on another instrument bank than DVX.INI's choice: bank names it as midiBank and soundFont do ("" the standard OP2 bank, NULL DVX.INI's choices again). A SoundFont (.SF2 or .SBK) or an UltraSound's ULTRASND.INI is for wavetable music (the FM bank stays DVX.INI's), any other bank for the FM synthesizers. The bank is read at once, so a missing or damaged one is MUSIC_ERR_BANK there and then; it must come before the music's first musicPlay (MUSIC_ERR_ARG for a module, live MIDI, or a music that has played). Control Panel's Instruments Test is musicLoadMacro and musicSetBank.
+
+Setting the position of a MIDI file sends the instruments and controllers set before that point (no notes). A module goes to the start of the pattern that holds the time; a sound goes to the frame.
.h2 Where Music Plays
-Modules are rendered by libxmp-lite into a stream of the sound device (16-bit stereo at the device's rate, 400 ms of buffer, linear interpolation), so they play on any device that plays sampled sound.
+A module plays on the wavetable synthesizer's own voices (a Gravis UltraSound's, an AWE32's or AWE64's) when there is one and no other music plays on it (one music at a time, MIDI or a module; a stopped one gives it up): its samples go into the chip's memory and libxmp's voices are played on the chip's, nothing mixed in software (MUSIC_OUT_WAVE; see Modules). Otherwise libxmp-lite renders it into a stream of the sound device (16-bit stereo at the device's rate, 400 ms of buffer, linear interpolation), so it plays on any device that plays sampled sound (MUSIC_OUT_STREAM): with DVX.INI [audio] modules = software (audioModulesChoices; the Control Panel's Sounds page), when the module has more channels than the chip has voices or a stereo sample, or when its samples do not fit in the chip's memory (the SoundFont kept there for MIDI gives way first).
+
+A sound always plays on a stream (MUSIC_OUT_STREAM) at its own rate and channels, 16-bit, with the same 400 ms of buffer; the mixer converts it to the device's rate. A pause, a seek, or a stop of any music on a stream is heard at once: the stream's voice is held where it is (audioVoicePause) and, for a seek or a stop, what it holds is dropped (audioStreamFlush).
MIDI goes to the best synthesizer there is, decided at the first musicPlay (or musicMidiOpen), with DVX.INI [audio] midi:
.table
midi MIDI plays on
---- -------------
- auto the FM chip (OPL2 / OPL3) when there is one and no program has taken the whole chip; else the software synthesizer
+ auto a wavetable synthesizer (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64) when there is one, no other music is using it, and its SoundFont loads; else the FM chip (OPL2 / OPL3) when there is one and no program has taken the whole chip; else the software synthesizer
mpu401 the MPU-401 port (mpuPort, default 330h) when it was found and no other music is using it; else as auto
- fm as auto
+ fm the FM chip when there is one and it is free; else the software synthesizer
software the software synthesizer
+ softwave the SoundFont software synthesizer (below); else the software synthesizer
.endtable
On the FM chip and the MPU-401 port the song is played by the timer service (500 times a second), so it keeps time however busy the programs are. On the FM chip every note takes a voice from audio.lib's shared pool; when none is free the note plays on the software synthesizer instead, which then runs beside the chip (its stream is always open for that). The software synthesizer runs in the stream's fill function (the shell's idle time), 64 frames between event checks, 150 ms ahead of what is heard; a program that keeps the desktop busy every frame should call audioPump once a frame, as for any stream.
.h2 General MIDI on FM
-Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. Control Panel's Sounds page lists the banks to choose from. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). The bank is read once, at the first MIDI music that needs it, so a new choice takes effect when DVX next starts.
+Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. Control Panel's Sounds page lists the banks to choose from; DVX's own add-ons offer DMXOPL and DMXOPL3 (MIT, by ConSiGno and Sneakernets) through Software. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). DVX.INI's choice is read as each MIDI music starts, so a new choice reaches the next one; a music keeps the bank it started with, and a bank is freed with the last music playing on it.
What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both, or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.
@@ -102,6 +137,18 @@ What is played: note on / off, velocity, program change, channel volume (CC7), e
When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo, and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time, and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.
+.h2 SoundFonts on a Wavetable Synthesizer
+
+A wavetable synthesizer (audio.lib's gus and guswt drivers for a Gravis UltraSound, awe for the EMU8000 of a Sound Blaster AWE32 or AWE64) plays recorded instruments from its own memory. Its instruments come from a SoundFont, DVX.INI [audio] soundFont (MUSIC_INI_SOUND_FONT), named as midiBank names an OP2 bank; not set, the standard one (MUSIC_SOUND_FONT_DEFAULT, KPUNCH\DVXGM.SF2: DVX's General MIDI SoundFont add-on, made from FluidR3_GM by mksf, about 3.4 MB). SoundFont 2 files are read in full; SoundFont 1 (.SBK, the AWE32's own) as far as it is known (it has no public specification). soundFont may also name a Gravis UltraSound's ULTRASND.INI by its path, which plays that folder's .PAT patches as instruments; and where the standard SoundFont is not installed, the patches in the folder ULTRADIR names play (musicPatchSetPath), else the AWE's own bank for its sample ROM (musicRomBankPath: SFBANK\SYNTHGM.SF2 or SYNTHGM.SBK in the folder SOUND names, as Creative's drivers installed it). Control Panel's Sounds page lists all of these.
+
+A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its SoundFont and samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC). Live MIDI loads a program's samples as it is chosen. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.
+
+The AWE runs the SoundFont's envelopes, LFOs, filter, and reverb and chorus sends itself; on the GUS the library runs the envelopes and LFOs (its volume and pitch changed as the note goes on), without a filter or effects.
+
+.h2 The SoundFont Software Synthesizer
+
+With DVX.INI [audio] midi = softwave, MIDI plays from the SoundFont in software on any device that plays sampled sound: the song's samples are read into memory at their own rates and mixed into the music's stream (linear interpolation, the envelopes and LFOs as on the GUS, no filter or effects), with the software FM synthesizer beside it for the notes the bank lacks. It needs the memory for the song's samples (a General MIDI song of ten instruments and a drum kit about 360 KB of DVXGM's) and more CPU time than FM.
+
.h2 The MPU-401 Port
With DVX.INI [audio] midi = mpu401, MIDI goes out of the MPU-401 port (audio.lib's mpu401 driver) to an external synthesizer, one music at a time (a second MIDI music meanwhile plays on the FM chip or the software synthesizer). Bytes are queued and sent by the timer as fast as the port takes them, with a 40 ms pause after every system exclusive message (first-generation MT-32s lose data without it). The music's volume scales every channel volume (CC7) sent. DVX.INI [audio] mpuMode says what is on the port:
@@ -127,11 +174,34 @@ musicMidiNoteOn(keys, 0, 60, 100); // middle C
musicMidiNoteOff(keys, 0, 60);
.endcode
-musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl, and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range.
+musicMidiOpen opens a music with no file that plays at once, on the same synthesizer a MIDI file would use. musicMidiNoteOn, musicMidiNoteOff, musicMidiProgram, musicMidiControl, and musicMidiPitchBend (-8192..8191) send channel messages (channels 0 to 15, data 0 to 127) to it, or to a MIDI file that is playing (for dynamic game scores); MUSIC_ERR_ARG for a module or a value out of range. musicMidiSend(music, status, data1, data2) sends any channel message as MIDI input brings it (audioMidiInRead: status 80h..EFh with the channel in it; a data byte the message does not use is ignored), so what a keyboard plays sounds as it comes (MIDI thru).
+
+.h2 Recording MIDI
+
+.code
+MusicMidiRecordT *take = NULL;
+AudioMidiMsgT msg;
+
+musicMidiRecordNew(&take);
+
+while (audioMidiInRead(audio, &msg)) { // each tick, while recording
+ musicMidiSend(keys, msg.status, msg.data1, msg.data2);
+ musicMidiRecordAdd(take, msg.ms, msg.status, msg.data1, msg.data2);
+}
+
+musicMidiRecordSave(take, "SONG.MID");
+musicMidiRecordFree(take);
+.endcode
+
+A MIDI recording keeps channel messages with their times (musicMidiRecordAdd(rec, ms, status, data1, data2), ms from the recording's start; a time before the last message's counts as the last's; MUSIC_ERR_ARG for anything but a channel message) and musicMidiRecordSave(rec, path) writes them as a Standard MIDI File: type 0, one track of 1,000 ticks a quarter note at 60 quarter notes a minute, so a tick is a millisecond. A note still held at the end is let go there, so the file never leaves a note sounding. MUSIC_ERR_WRITE when the file cannot be written (nothing is left of one not all written). musicMidiRecordCount and musicMidiRecordLength (the last message's time) say what it holds; musicMidiRecordFree frees it, and a program's recordings are freed when it ends. musicLoad plays the file. The music macros (below) are written into MIDI files by the same code.
.h2 Modules
-MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite, unmodified; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so modules play in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. Rough cost on a 486DX2-66 at 22,050 Hz: 5 percent for a 4-channel MOD, 20 to 25 percent for a 16-channel XM.
+MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite with DVX's patches, listed in its DVX-VERSION.md; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so it plays modules in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. On a stream libxmp mixes every channel in software (linear interpolation, three multiplies a channel a frame).
+
+On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.
+
+Measured on an emulated 486DX2-66 at 22,050 Hz, a 4-channel MOD with all four channels sounding takes about 49 percent of the CPU mixed in software and about 14 percent on a Gravis UltraSound's voices, most of that libxmp's own work each tick (about 1.3 ms of the 20).
.h2 Music Macros
@@ -148,11 +218,11 @@ Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds
.h2 Several Programs at Once
-Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music. When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.
+Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music, and so does the wavetable synthesizer (MIDI or a module). When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.
.h2 BASIC
-src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums). A number only works for the program that got it. MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease; see the sound library).
+src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.
The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
@@ -162,6 +232,8 @@ libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hi
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 General MIDI SoundFont add-on (DVXGM.SF2) is made from FluidR3_GM by Frank Wen (MIT licence); its notice is installed beside it (SHARED\BANKS\KPUNCH\DVXGM.TXT).
+
The GENMIDI instrument bank is from Freedoom 0.13.0, Copyright (C) 2001-2024 Contributors to the Freedoom project. All rights reserved. BSD 3-clause licence:
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Freedoom project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/libs/kpunch/music/music.h b/src/libs/kpunch/music/music.h
index 5e0d576c..a8e71bb0 100644
--- a/src/libs/kpunch/music/music.h
+++ b/src/libs/kpunch/music/music.h
@@ -25,19 +25,29 @@
// Music for DVX programs on whatever sound hardware the computer has:
//
// Modules tracker music (MOD, S3M, XM, IT), played by libxmp-lite
-// into a sound stream, so it plays on any card that plays
-// sampled sound.
+// on a wavetable synthesizer's own voices when it is free
+// (DVX.INI [audio] modules), else into a sound stream, so it
+// plays on any card that plays sampled sound.
+// Sounds WAV and VOC files, played into a sound stream at their own
+// rate (a WAV loaded by its path is read from the disk as it
+// plays, so a long recording needs no memory), with
+// everything a module has: pause, seek, position, loop.
// MIDI Standard MIDI Files (type 0 and 1), played by DVX's own
// sequencer on the best synthesizer there is: an external
// MIDI synthesizer on the MPU-401 port when DVX.INI [audio]
// midi = mpu401 (General MIDI, or a Roland MT-32 with
-// mpuMode), else the FM chip (OPL2 / OPL3) with the General
-// MIDI instrument bank (DVX.INI [audio] midiBank, an OP2
-// bank, default the bundled Freedoom GENMIDI bank), else the
-// library's own FM software synthesizer, which sounds like
-// the FM chip and plays on any card that plays sampled
-// sound. When the FM chip runs out of voices, extra notes
-// play on the software synthesizer.
+// mpuMode), else a wavetable synthesizer (Gravis
+// UltraSound, Sound Blaster AWE32 / AWE64) with a General
+// MIDI SoundFont (DVX.INI [audio] soundFont), else the FM
+// chip (OPL2 / OPL3) with the General MIDI instrument bank
+// (DVX.INI [audio] midiBank, an OP2 bank, default the
+// bundled Freedoom GENMIDI bank), else the library's own FM
+// software synthesizer, which sounds like the FM chip and
+// plays on any card that plays sampled sound. A wavetable
+// synthesizer's memory holds only the samples each song
+// plays; notes whose samples do not fit play on the FM chip.
+// When the FM chip runs out of voices, extra notes play on
+// the software synthesizer.
// Live MIDI note on / off, program change, controllers and pitch bend
// sent by the program, on the same synthesizers (a music
// opened with musicMidiOpen, or one playing a file).
@@ -48,13 +58,14 @@
// Every music belongs to the program that loaded it; it is stopped and
// freed when the program ends, however it ends. Any number of programs
// may play music at the same time (they share the FM chip's voices; the
-// MPU-401 port plays one program's MIDI at a time and the others use the
-// FM chip or the software synthesizer).
+// MPU-401 port and the wavetable synthesizer each play one MIDI music at
+// a time, and the others use the FM chip or the software synthesizer).
//
// MIDI played on the synthesizers runs from the timer service, so it
-// keeps time however busy the programs are; modules and the software
-// synthesizer are made in the shell's idle time (audioPump), a quarter of
-// a second or so ahead. Module playback uses floating point (libxmp):
+// keeps time however busy the programs are; modules, sounds, and the
+// software synthesizer are made in the shell's idle time (audioPump), a
+// quarter of a second or so ahead (a pause, a seek, or a stop is heard at
+// once: what the stream holds is held or dropped). Module playback uses floating point (libxmp):
// on a computer without a maths coprocessor it is slow.
//
// Functions returning int32_t return MUSIC_OK (0) on success and a
@@ -78,14 +89,40 @@
#define MUSIC_MIDI_BEND_MIN (-8192)
#define MUSIC_MIDI_BEND_MAX 8191
-// Text sizes in MusicInfoT (with the terminating zero)
// DVX.INI [audio] midiBank: the FM instrument bank, an OP2 file named as
// the banks kind names it (NAME or VENDOR\NAME in SHARED\BANKS or the
// user's BANKS folder; platformKindFind) or by its path. Not set, or not
-// found: the bundled one. Read once, when MIDI first plays.
+// found: the bundled one. Read as each MIDI music starts, so a new
+// choice reaches the next one (the musics playing keep theirs).
#define MUSIC_INI_BANK "midiBank"
#define MUSIC_BANK_DEFAULT "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "music" DVX_PATH_SEP "genmidi.op2"
+// DVX.INI [audio] soundFont: the wavetable synthesizer's General MIDI
+// instruments, a SoundFont 2 file named as midiBank names an OP2 bank.
+// Not set: the standard one, DVX's General MIDI add-on. Missing or
+// damaged: MIDI music plays on the FM chip.
+#define MUSIC_INI_SOUND_FONT "soundFont"
+#define MUSIC_SOUND_FONT_DEFAULT "KPUNCH" DVX_PATH_SEP "DVXGM.SF2"
+
+// soundFont (and musicSetBank) may also name a Gravis UltraSound's
+// ULTRASND.INI by its path: that UltraSound folder's General MIDI patches
+// (MIDI\*.PAT, as the INI maps them) play as a SoundFont's instruments
+// would. Where the standard SoundFont is not installed, the patches in
+// the folder ULTRADIR names play, when it has them.
+#define MUSIC_PATCH_SET_INI "ULTRASND.INI"
+#define MUSIC_PATCH_SET_ENV "ULTRADIR"
+
+// soundFont may also name an AWE32's or AWE64's own General MIDI bank for
+// its sample ROM (SYNTHGM.SF2, or the older SoundFont 1 SYNTHGM.SBK, from
+// Creative's drivers): its instruments play from the card's ROM with
+// nothing loaded into its memory (on other synthesizers their notes go to
+// the FM synthesizer). Creative's drivers keep it in SFBANK in the folder
+// SOUND names; where neither the standard SoundFont nor UltraSound
+// patches are found, that one plays.
+#define MUSIC_ROM_BANK_ENV "SOUND"
+#define MUSIC_ROM_BANK_DIR "SFBANK"
+
+// Text sizes in MusicInfoT (with the terminating zero)
#define MUSIC_TITLE_MAX 64
#define MUSIC_DESC_MAX 64
@@ -99,7 +136,9 @@
#define MUSIC_ERR_NO_DEVICE (-6) // no sound device, or nothing that can play this music
#define MUSIC_ERR_BANK (-7) // the FM instrument bank is missing or damaged
#define MUSIC_ERR_MACRO (-8) // a mistake in a music macro string
-#define MUSIC_ERR_COUNT 9 // MUSIC_OK and the eight errors
+#define MUSIC_ERR_UNSUPPORTED (-9) // a WAV or VOC file in a form not played (compressed, or not 8- or 16-bit)
+#define MUSIC_ERR_WRITE (-10) // the file could not be written
+#define MUSIC_ERR_COUNT 11 // MUSIC_OK and the ten errors
// Opaque handle
typedef struct MusicT MusicT;
@@ -109,21 +148,24 @@ typedef struct MusicMmlT MusicMmlT;
typedef enum {
MUSIC_KIND_MODULE = 0, // MOD, S3M, XM, IT
MUSIC_KIND_MIDI = 1, // a MIDI file
- MUSIC_KIND_LIVE = 2 // live MIDI only (musicMidiOpen)
+ MUSIC_KIND_LIVE = 2, // live MIDI only (musicMidiOpen)
+ MUSIC_KIND_SOUND = 3 // a WAV or VOC sound file
} MusicKindE;
// Where it plays
typedef enum {
MUSIC_OUT_NONE = 0, // not decided yet (before the first musicPlay)
- MUSIC_OUT_STREAM = 1, // modules: a sound stream
+ MUSIC_OUT_STREAM = 1, // modules and sounds: a sound stream (mixed in software)
MUSIC_OUT_MPU401 = 2, // MIDI: the MPU-401 port
MUSIC_OUT_FM = 3, // MIDI: the FM chip (extra notes on the software synthesizer)
- MUSIC_OUT_SOFTWARE = 4 // MIDI: the software FM synthesizer
+ MUSIC_OUT_SOFTWARE = 4, // MIDI: the software FM synthesizer
+ MUSIC_OUT_WAVE = 5, // MIDI or a module: the wavetable synthesizer (MIDI notes whose samples do not fit there on the FM chip)
+ MUSIC_OUT_SOFTWAVE = 6 // MIDI: the SoundFont software synthesizer (DVX.INI midi = softwave)
} MusicOutputE;
// A music (musicGetInfo).
typedef struct {
- char title[MUSIC_TITLE_MAX]; // the module's title ("" for MIDI files)
+ char title[MUSIC_TITLE_MAX]; // a module's own title or a MIDI file's first track's name ("" for none, and for sounds)
char output[MUSIC_DESC_MAX]; // for people, e.g. "OPL3 FM synthesizer at 388h"
int32_t kind; // MusicKindE
int32_t where; // MusicOutputE
@@ -142,11 +184,35 @@ typedef void (*MusicEndFnT)(MusicT *music, void *userData);
// Loading and playing
// ============================================================
-// Load a module or MIDI file (by its contents, not its name) from a file
-// or from memory (copied). Nothing plays until musicPlay.
+// Load a module, a MIDI file, or a WAV or VOC sound file (by its
+// contents, not its name) from a file or from memory (copied). Nothing
+// plays until musicPlay. MUSIC_ERR_UNSUPPORTED: a sound file in a form
+// that is not played (compressed, or not 8- or 16-bit).
int32_t musicLoad(const char *path, MusicT **out);
int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out);
+// A MIDI music from a music macro string (the language below): its notes
+// on the General MIDI instruments its I commands name, the piano for
+// notes before any I or after I0. MUSIC_ERR_MACRO when it has a mistake.
+int32_t musicLoadMacro(const char *macro, MusicT **out);
+
+// Play a MIDI music on another instrument bank than DVX.INI's choice:
+// bank names one as DVX.INI's midiBank and soundFont do ("" = the
+// standard OP2 bank, NULL = DVX.INI's choices again). A SoundFont is for
+// the wavetable synthesizer, an OP2 bank for the FM ones. It is read now
+// (MUSIC_ERR_BANK when it is missing or damaged). Before the music's
+// first musicPlay; MUSIC_ERR_ARG for a module, live MIDI, or a music that
+// has played.
+int32_t musicSetBank(MusicT *music, const char *bank);
+
+// The UltraSound's own patches: out = ULTRASND.INI in the folder ULTRADIR
+// names, when it is there; false otherwise.
+bool musicPatchSetPath(char *out, int32_t outSize);
+
+// The AWE's own ROM bank: out = SFBANK\SYNTHGM.SF2 (else SYNTHGM.SBK) in
+// the folder SOUND names, when it is there; false otherwise.
+bool musicRomBankPath(char *out, int32_t outSize);
+
// Stop music and free it. NULL is ignored.
void musicFree(MusicT *music);
@@ -171,7 +237,8 @@ int32_t musicSetVolume(MusicT *music, int32_t volume);
// Position and length in milliseconds. Setting the position of a MIDI
// file replays its settings (instruments, controllers) up to that point;
-// a module goes to the start of the pattern that holds that time.
+// a module goes to the start of the pattern that holds that time; a
+// sound goes to that very frame.
uint32_t musicGetPosition(const MusicT *music);
int32_t musicSetPosition(MusicT *music, uint32_t ms);
@@ -206,6 +273,41 @@ int32_t musicMidiNoteOn(MusicT *music, int32_t channel, int32_t note, int32_t ve
int32_t musicMidiPitchBend(MusicT *music, int32_t channel, int32_t bend);
int32_t musicMidiProgram(MusicT *music, int32_t channel, int32_t program);
+// Any channel message, as MIDI input brings it (audioMidiInRead): status
+// 80h..EFh (the kind and the channel 0..15) and its data bytes (one a
+// message does not use is ignored); MUSIC_ERR_ARG for anything else.
+// Plays what a keyboard plays (MIDI thru).
+int32_t musicMidiSend(MusicT *music, uint8_t status, uint8_t data1, uint8_t data2);
+
+// ============================================================
+// Recording MIDI
+// ============================================================
+//
+// A MIDI recording: channel messages with their times, written as a
+// Standard MIDI File (type 0, a tick a millisecond) that musicLoad plays.
+// It belongs to the program that made it and goes when that program
+// ends.
+
+typedef struct MusicMidiRecordS MusicMidiRecordT;
+
+int32_t musicMidiRecordNew(MusicMidiRecordT **out);
+
+// A channel message (as musicMidiSend takes) at ms milliseconds from the
+// recording's start; a time before the last message's counts as the
+// last's. MUSIC_ERR_ARG for anything else.
+int32_t musicMidiRecordAdd(MusicMidiRecordT *rec, uint32_t ms, uint8_t status, uint8_t data1, uint8_t data2);
+
+// The messages kept, and the time of the last (0 when none).
+uint32_t musicMidiRecordCount(const MusicMidiRecordT *rec);
+uint32_t musicMidiRecordLength(const MusicMidiRecordT *rec);
+
+// Write it to path (MUSIC_ERR_WRITE when it cannot be written; nothing is
+// left of a file not all written). Notes still held at the end are let
+// go there.
+int32_t musicMidiRecordSave(const MusicMidiRecordT *rec, const char *path);
+
+void musicMidiRecordFree(MusicMidiRecordT *rec);
+
// ============================================================
// Music macros
// ============================================================
@@ -286,7 +388,15 @@ int32_t musicBasSound(double hz, double ticks);
// ============================================================
// Each BASIC program's musics are numbered from 1 (0 = MUSIC_FAILED;
-// MusicError$ says why). Volumes are percent (0..100).
+// MusicError$ says why), and its MIDI recordings the same way. Volumes
+// are percent (0..100). A MIDI message is packed as MidiInMessage gives
+// it (AUDIO_MIDI_PACKED_*).
+void MidiRecordAdd(int32_t handle, int32_t milliseconds, int32_t midiMsg);
+int32_t MidiRecordCount(int32_t handle);
+void MidiRecordFree(int32_t handle);
+int32_t MidiRecordLength(int32_t handle);
+int32_t MidiRecordNew(void);
+int32_t MidiRecordSave(int32_t handle, const char *fileName);
void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value);
const char *MusicError(void);
void MusicFree(int32_t handle);
@@ -294,9 +404,12 @@ int32_t MusicGetVolume(int32_t handle);
const char *MusicInfo(int32_t handle, const char *field);
int32_t MusicLength(int32_t handle);
int32_t MusicLoad(const char *fileName);
+int32_t MusicLoadMacro(const char *macro);
int32_t MusicMidiOpen(void);
+void MusicMidiSend(int32_t handle, int32_t midiMsg);
void MusicNoteOff(int32_t handle, int32_t channel, int32_t note);
void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity);
+const char *MusicPatchSet(void);
void MusicPause(int32_t handle);
void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend);
void MusicPlay(int32_t handle, int32_t loopIt);
@@ -304,7 +417,9 @@ int32_t MusicPlaying(int32_t handle);
int32_t MusicPosition(int32_t handle);
void MusicProgram(int32_t handle, int32_t channel, int32_t program);
void MusicResume(int32_t handle);
+const char *MusicRomBank(void);
void MusicSeek(int32_t handle, int32_t milliseconds);
+void MusicSetBank(int32_t handle, const char *bank);
void MusicStop(int32_t handle);
void MusicVolume(int32_t handle, int32_t volume);
diff --git a/src/libs/kpunch/music/musicBas.c b/src/libs/kpunch/music/musicBas.c
index 16d95467..91d02f89 100644
--- a/src/libs/kpunch/music/musicBas.c
+++ b/src/libs/kpunch/music/musicBas.c
@@ -26,9 +26,10 @@
// signatures use only BASIC-marshalable types. A BASIC program refers
// to its musics by number (1..BAS_SLOTS, 0 = failed; MusicError$ says
// why); a number only works for the program that got it. MIDI channels
-// are 1..16 in BASIC (10 is percussion), volumes percent. The library
-// frees a program's musics when it ends (musicAppEnded, which also
-// forgets their numbers here). The musicBas... functions are BASIC's own
+// are 1..16 in BASIC (10 is percussion), volumes percent. MIDI
+// recordings are numbered the same way, in a table of their own. The
+// library frees a program's musics and recordings when it ends
+// (musicAppEnded, which also forgets their numbers here). The musicBas... functions are BASIC's own
// BEEP, SOUND and PLAY statements, called by the BASIC runtime, on the
// program's macro queue.
@@ -40,6 +41,8 @@
#include
#define BAS_SLOTS 64
+#define BAS_RECORD_SLOTS 8
+#define BYTE_MASK 0xFF
#define BAS_FIRST_CHANNEL 1
#define PERCENT_MAX 100
#define BAS_INFO_MAX (MUSIC_TITLE_MAX + MUSIC_DESC_MAX)
@@ -55,6 +58,11 @@ typedef struct {
int32_t appId;
} BasSlotT;
+typedef struct {
+ MusicMidiRecordT *rec;
+ int32_t appId;
+} BasRecordSlotT;
+
// MusicInfo$ fields
typedef enum {
InfoTitleE,
@@ -69,47 +77,62 @@ typedef enum {
// MusicInfo$ field names, and the words for MusicKindE and MusicOutputE
// in their enum order
static const char *const sInfoNames[InfoCountE] = { "title", "output", "kind", "where", "paused", "looping" };
-static const char *const sKindWords[] = { "module", "midi", "live" };
-static const char *const sWhereWords[] = { "", "stream", "mpu401", "fm", "software" };
+static const char *const sKindWords[] = { "module", "midi", "live", "sound" };
+static const char *const sWhereWords[] = { "", "stream", "mpu401", "fm", "software", "wave", "softwave" };
-static BasSlotT sSlots[BAS_SLOTS];
-static int32_t sLastError = MUSIC_OK;
-static char sInfo[BAS_INFO_MAX];
+static BasSlotT sSlots[BAS_SLOTS];
+static BasRecordSlotT sRecords[BAS_RECORD_SLOTS];
+static int32_t sLastError = MUSIC_OK;
+static char sInfo[BAS_INFO_MAX];
// ============================================================
// Prototypes
// ============================================================
-static int32_t basAdd(MusicT *music, int32_t rc);
-static MusicT *basGet(int32_t handle);
-static int32_t basResult(int32_t rc);
-static const char *basWord(const char *const *words, int32_t count, int32_t value);
-int32_t musicBasBeep(void);
-bool musicBasBusy(void);
-void musicBasForget(int32_t appId);
-int32_t musicBasPlay(const char *macro);
-int32_t musicBasPlayCount(void);
-int32_t musicBasSound(double hz, double ticks);
-void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value);
-const char *MusicError(void);
-void MusicFree(int32_t handle);
-int32_t MusicGetVolume(int32_t handle);
-const char *MusicInfo(int32_t handle, const char *field);
-int32_t MusicLength(int32_t handle);
-int32_t MusicLoad(const char *fileName);
-int32_t MusicMidiOpen(void);
-void MusicNoteOff(int32_t handle, int32_t channel, int32_t note);
-void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity);
-void MusicPause(int32_t handle);
-void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend);
-void MusicPlay(int32_t handle, int32_t loopIt);
-int32_t MusicPlaying(int32_t handle);
-int32_t MusicPosition(int32_t handle);
-void MusicProgram(int32_t handle, int32_t channel, int32_t program);
-void MusicResume(int32_t handle);
-void MusicSeek(int32_t handle, int32_t milliseconds);
-void MusicStop(int32_t handle);
-void MusicVolume(int32_t handle, int32_t volume);
+static int32_t basAdd(MusicT *music, int32_t rc);
+static int32_t basAppId(void);
+static MusicT *basGet(int32_t handle);
+static uint8_t basPacked(int32_t midiMsg, int32_t shift);
+static MusicMidiRecordT *basRecord(int32_t handle);
+static int32_t basResult(int32_t rc);
+static const char *basWord(const char *const *words, int32_t count, int32_t value);
+void MidiRecordAdd(int32_t handle, int32_t milliseconds, int32_t midiMsg);
+int32_t MidiRecordCount(int32_t handle);
+void MidiRecordFree(int32_t handle);
+int32_t MidiRecordLength(int32_t handle);
+int32_t MidiRecordNew(void);
+int32_t MidiRecordSave(int32_t handle, const char *fileName);
+int32_t musicBasBeep(void);
+bool musicBasBusy(void);
+void musicBasForget(int32_t appId);
+int32_t musicBasPlay(const char *macro);
+int32_t musicBasPlayCount(void);
+int32_t musicBasSound(double hz, double ticks);
+void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value);
+const char *MusicError(void);
+void MusicFree(int32_t handle);
+int32_t MusicGetVolume(int32_t handle);
+const char *MusicInfo(int32_t handle, const char *field);
+int32_t MusicLength(int32_t handle);
+int32_t MusicLoad(const char *fileName);
+int32_t MusicLoadMacro(const char *macro);
+int32_t MusicMidiOpen(void);
+void MusicMidiSend(int32_t handle, int32_t midiMsg);
+void MusicNoteOff(int32_t handle, int32_t channel, int32_t note);
+void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity);
+const char *MusicPatchSet(void);
+void MusicPause(int32_t handle);
+void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend);
+void MusicPlay(int32_t handle, int32_t loopIt);
+int32_t MusicPlaying(int32_t handle);
+int32_t MusicPosition(int32_t handle);
+void MusicProgram(int32_t handle, int32_t channel, int32_t program);
+void MusicResume(int32_t handle);
+const char *MusicRomBank(void);
+void MusicSeek(int32_t handle, int32_t milliseconds);
+void MusicSetBank(int32_t handle, const char *bank);
+void MusicStop(int32_t handle);
+void MusicVolume(int32_t handle, int32_t volume);
// A number for a newly loaded music (rc its load result), or 0.
@@ -118,7 +141,7 @@ static int32_t basAdd(MusicT *music, int32_t rc) {
return basResult(rc);
}
- int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
+ int32_t appId = basAppId();
for (int32_t i = 0; i < BAS_SLOTS; i++) {
if (!sSlots[i].music) {
@@ -134,12 +157,16 @@ static int32_t basAdd(MusicT *music, int32_t rc) {
}
+// The program calling.
+static int32_t basAppId(void) {
+ return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
+}
+
+
// The calling program's music with this number, else NULL (and the error
// is noted).
static MusicT *basGet(int32_t handle) {
- int32_t appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
-
- if (handle < 1 || handle > BAS_SLOTS || !sSlots[handle - 1].music || sSlots[handle - 1].appId != appId) {
+ if (handle < 1 || handle > BAS_SLOTS || !sSlots[handle - 1].music || sSlots[handle - 1].appId != basAppId()) {
sLastError = MUSIC_ERR_ARG;
return NULL;
}
@@ -148,6 +175,24 @@ static MusicT *basGet(int32_t handle) {
}
+// One byte of a message packed as MidiInMessage gives it.
+static uint8_t basPacked(int32_t midiMsg, int32_t shift) {
+ return (uint8_t)((midiMsg >> shift) & BYTE_MASK);
+}
+
+
+// The calling program's MIDI recording with this number, else NULL (and
+// the error is noted).
+static MusicMidiRecordT *basRecord(int32_t handle) {
+ if (handle < 1 || handle > BAS_RECORD_SLOTS || !sRecords[handle - 1].rec || sRecords[handle - 1].appId != basAppId()) {
+ sLastError = MUSIC_ERR_ARG;
+ return NULL;
+ }
+
+ return sRecords[handle - 1].rec;
+}
+
+
// 0 with the error kept for MusicError$.
static int32_t basResult(int32_t rc) {
sLastError = rc;
@@ -161,6 +206,61 @@ static const char *basWord(const char *const *words, int32_t count, int32_t valu
}
+// A message packed as MidiInMessage gives it, at milliseconds from the
+// recording's start (MidiInTime).
+void MidiRecordAdd(int32_t handle, int32_t milliseconds, int32_t midiMsg) {
+ uint32_t ms = milliseconds < 0 ? 0 : (uint32_t)milliseconds;
+
+ sLastError = musicMidiRecordAdd(basRecord(handle), ms, basPacked(midiMsg, 0), basPacked(midiMsg, AUDIO_MIDI_PACKED_DATA1_SHIFT), basPacked(midiMsg, AUDIO_MIDI_PACKED_DATA2_SHIFT));
+}
+
+
+int32_t MidiRecordCount(int32_t handle) {
+ return (int32_t)musicMidiRecordCount(basRecord(handle));
+}
+
+
+void MidiRecordFree(int32_t handle) {
+ MusicMidiRecordT *rec = basRecord(handle);
+
+ if (rec) {
+ musicMidiRecordFree(rec);
+ sRecords[handle - 1].rec = NULL;
+ }
+}
+
+
+int32_t MidiRecordLength(int32_t handle) {
+ return (int32_t)musicMidiRecordLength(basRecord(handle));
+}
+
+
+// A number for a new recording, or 0.
+int32_t MidiRecordNew(void) {
+ for (int32_t i = 0; i < BAS_RECORD_SLOTS; i++) {
+ if (!sRecords[i].rec) {
+ int32_t rc = musicMidiRecordNew(&sRecords[i].rec);
+
+ if (rc != MUSIC_OK) {
+ return basResult(rc);
+ }
+
+ sRecords[i].appId = basAppId();
+ sLastError = MUSIC_OK;
+ return i + 1;
+ }
+ }
+
+ return basResult(MUSIC_ERR_NO_MEMORY);
+}
+
+
+int32_t MidiRecordSave(int32_t handle, const char *fileName) {
+ sLastError = musicMidiRecordSave(basRecord(handle), fileName);
+ return sLastError == MUSIC_OK ? BAS_TRUE : BAS_FALSE;
+}
+
+
int32_t musicBasBeep(void) {
return musicMmlTone(musicMmlBasic(true), QB_BEEP_MILLIHZ, QB_BEEP_US);
}
@@ -171,13 +271,20 @@ bool musicBasBusy(void) {
}
-// The program ended (its musics are being freed): forget their numbers.
+// The program ended (its musics and recordings are being freed): forget
+// their numbers.
void musicBasForget(int32_t appId) {
for (int32_t i = 0; i < BAS_SLOTS; i++) {
if (sSlots[i].appId == appId) {
sSlots[i].music = NULL;
}
}
+
+ for (int32_t i = 0; i < BAS_RECORD_SLOTS; i++) {
+ if (sRecords[i].appId == appId) {
+ sRecords[i].rec = NULL;
+ }
+ }
}
@@ -236,9 +343,9 @@ int32_t MusicGetVolume(int32_t handle) {
// A fact about the music by name (any case): "title" (a module's own
// title), "output" (where it plays, for people), "kind" ("module",
-// "midi", "live"), "where" ("stream", "mpu401", "fm", "software"; ""
-// before it first plays), "paused" and "looping" (True / False as text);
-// "" for another field or no such music.
+// "midi", "live", "sound"), "where" ("stream", "mpu401", "fm", "software",
+// "wave", "softwave"; "" before it first plays), "paused" and "looping"
+// (True / False as text); "" for another field or no such music.
const char *MusicInfo(int32_t handle, const char *field) {
MusicT *m = basGet(handle);
int32_t idx = -1;
@@ -301,6 +408,14 @@ int32_t MusicLoad(const char *fileName) {
}
+int32_t MusicLoadMacro(const char *macro) {
+ MusicT *m = NULL;
+ int32_t rc = musicLoadMacro(macro, &m);
+
+ return basAdd(m, rc);
+}
+
+
int32_t MusicMidiOpen(void) {
MusicT *m = NULL;
int32_t rc = musicMidiOpen(&m);
@@ -309,6 +424,12 @@ int32_t MusicMidiOpen(void) {
}
+// A message packed as MidiInMessage gives it (MIDI thru).
+void MusicMidiSend(int32_t handle, int32_t midiMsg) {
+ sLastError = musicMidiSend(basGet(handle), basPacked(midiMsg, 0), basPacked(midiMsg, AUDIO_MIDI_PACKED_DATA1_SHIFT), basPacked(midiMsg, AUDIO_MIDI_PACKED_DATA2_SHIFT));
+}
+
+
void MusicNoteOff(int32_t handle, int32_t channel, int32_t note) {
sLastError = musicMidiNoteOff(basGet(handle), channel - BAS_FIRST_CHANNEL, note);
}
@@ -319,6 +440,18 @@ void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity
}
+// The UltraSound's own patches' ULTRASND.INI (musicPatchSetPath), or "".
+const char *MusicPatchSet(void) {
+ static char path[DVX_MAX_PATH];
+
+ if (!musicPatchSetPath(path, sizeof(path))) {
+ path[0] = '\0';
+ }
+
+ return path;
+}
+
+
void MusicPause(int32_t handle) {
sLastError = musicPause(basGet(handle));
}
@@ -354,11 +487,29 @@ void MusicResume(int32_t handle) {
}
+// The AWE's own ROM bank (musicRomBankPath), or "".
+const char *MusicRomBank(void) {
+ static char path[DVX_MAX_PATH];
+
+ if (!musicRomBankPath(path, sizeof(path))) {
+ path[0] = '\0';
+ }
+
+ return path;
+}
+
+
void MusicSeek(int32_t handle, int32_t milliseconds) {
sLastError = milliseconds < 0 ? MUSIC_ERR_ARG : musicSetPosition(basGet(handle), (uint32_t)milliseconds);
}
+// "" is the standard bank.
+void MusicSetBank(int32_t handle, const char *bank) {
+ sLastError = musicSetBank(basGet(handle), bank);
+}
+
+
void MusicStop(int32_t handle) {
sLastError = musicStop(basGet(handle));
}
diff --git a/src/libs/kpunch/music/musicCents.c b/src/libs/kpunch/music/musicCents.c
new file mode 100644
index 00000000..d6518616
--- /dev/null
+++ b/src/libs/kpunch/music/musicCents.c
@@ -0,0 +1,143 @@
+// 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.
+
+// musicCents.c -- cents and powers of two in fixed point, and loudness
+// as attenuation (see musicInt.h, "Cents in fixed point")
+//
+// 2^(cents / 1200) from a table of 2^(i / 120) in 16.16, ten cents a
+// step, with straight lines between its entries, and the other way: a
+// ratio's cents from the same table. An amplitude's attenuation from a
+// table of 128 steps. No floating point and no library
+// calls in waveOctaves and wavePow2, which the synthesizers use at
+// interrupt time (an interrupt-time file, pinned by musicLock);
+// waveRatioCents divides in 64 bits and is for program time only.
+
+#include "musicInt.h"
+
+#define CENTS_STEP 10
+#define CENTS_STEPS (WAVE_CENTS_OCTAVE / CENTS_STEP)
+#define LOCK_ITEMS 4
+
+// 65536 x 2^(i / 120)
+static const uint32_t sPow2[CENTS_STEPS + 1] = {
+ 65536, 65916, 66297, 66682, 67068, 67456, 67847, 68240,
+ 68635, 69033, 69433, 69835, 70240, 70647, 71056, 71468,
+ 71882, 72298, 72717, 73138, 73562, 73988, 74416, 74848,
+ 75281, 75717, 76156, 76597, 77041, 77487, 77936, 78387,
+ 78841, 79298, 79758, 80220, 80684, 81152, 81622, 82095,
+ 82570, 83049, 83530, 84013, 84500, 84990, 85482, 85977,
+ 86475, 86976, 87480, 87987, 88497, 89009, 89525, 90043,
+ 90565, 91090, 91617, 92148, 92682, 93219, 93759, 94302,
+ 94848, 95398, 95950, 96506, 97065, 97628, 98193, 98762,
+ 99334, 99909, 100488, 101070, 101656, 102245, 102837, 103433,
+ 104032, 104635, 105241, 105850, 106464, 107080, 107701, 108324,
+ 108952, 109583, 110218, 110856, 111499, 112145, 112794, 113448,
+ 114105, 114766, 115431, 116099, 116772, 117448, 118129, 118813,
+ 119501, 120194, 120890, 121590, 122295, 123003, 123715, 124432,
+ 125153, 125878, 126607, 127341, 128078, 128820, 129567, 130317,
+ 131072
+};
+
+// 200 log10(128 / i): the attenuation of an amplitude of i / 128
+const int16_t waveCbForStep[WAVE_STEP_MAX + 1] = {
+ 1440, 421, 361, 326, 301, 282, 266, 252, 241, 231, 221, 213, 206, 199, 192, 186,
+ 181, 175, 170, 166, 161, 157, 153, 149, 145, 142, 138, 135, 132, 129, 126, 123,
+ 120, 118, 115, 113, 110, 108, 105, 103, 101, 99, 97, 95, 93, 91, 89, 87,
+ 85, 83, 82, 80, 78, 77, 75, 73, 72, 70, 69, 67, 66, 64, 63, 62,
+ 60, 59, 58, 56, 55, 54, 52, 51, 50, 49, 48, 46, 45, 44, 43, 42,
+ 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26,
+ 25, 24, 23, 22, 21, 21, 20, 19, 18, 17, 16, 16, 15, 14, 13, 12,
+ 12, 11, 10, 9, 9, 8, 7, 6, 6, 5, 4, 3, 3, 2, 1, 1,
+ 0
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+int32_t waveCentsLockItems(AudioLockItemT *items);
+int32_t waveOctaves(int32_t cents, uint32_t *frac);
+uint32_t wavePow2(int32_t cents);
+int32_t waveRatioCents(uint32_t num, uint32_t den);
+
+
+int32_t waveCentsLockItems(AudioLockItemT *items) {
+ items[0].addr = (const void *)waveOctaves;
+ items[0].size = 0;
+ items[1].addr = (const void *)wavePow2;
+ items[1].size = 0;
+ items[2].addr = sPow2;
+ items[2].size = sizeof(sPow2);
+ items[3].addr = waveCbForStep;
+ items[3].size = sizeof(waveCbForStep);
+ return LOCK_ITEMS;
+}
+
+
+// The whole octaves below cents (rounded down), and the table's entry
+// below the rest with a straight line to the next.
+int32_t waveOctaves(int32_t cents, uint32_t *frac) {
+ int32_t oct = cents >= 0 ? cents / WAVE_CENTS_OCTAVE : -((WAVE_CENTS_OCTAVE - 1 - cents) / WAVE_CENTS_OCTAVE);
+ int32_t rest = cents - oct * WAVE_CENTS_OCTAVE;
+ int32_t i = rest / CENTS_STEP;
+
+ *frac = sPow2[i] + (sPow2[i + 1] - sPow2[i]) * (uint32_t)(rest % CENTS_STEP) / CENTS_STEP;
+ return oct;
+}
+
+
+uint32_t wavePow2(int32_t cents) {
+ uint32_t frac;
+ int32_t oct = waveOctaves(cents, &frac);
+
+ return oct >= 0 ? frac << oct : frac >> -oct;
+}
+
+
+// The ratio as 16.16 between 1 and 2 and the octaves taken out, then the
+// table's entry below it and a straight line to the next.
+int32_t waveRatioCents(uint32_t num, uint32_t den) {
+ if (!num || !den) {
+ return 0;
+ }
+
+ uint64_t r = ((uint64_t)num << WAVE_FIX_SHIFT) / den;
+ int32_t oct = 0;
+
+ while (r >= ((uint64_t)WAVE_FIX_ONE << 1)) {
+ r >>= 1;
+ oct++;
+ }
+
+ while (r < WAVE_FIX_ONE) {
+ r <<= 1;
+ oct--;
+ }
+
+ int32_t i = 0;
+
+ while (i < CENTS_STEPS - 1 && sPow2[i + 1] <= r) {
+ i++;
+ }
+
+ return oct * WAVE_CENTS_OCTAVE + i * CENTS_STEP + (int32_t)((r - sPow2[i]) * CENTS_STEP / (sPow2[i + 1] - sPow2[i]));
+}
diff --git a/src/libs/kpunch/music/musicFound.c b/src/libs/kpunch/music/musicFound.c
new file mode 100644
index 00000000..d8b25fd7
--- /dev/null
+++ b/src/libs/kpunch/music/musicFound.c
@@ -0,0 +1,74 @@
+// 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.
+
+// musicFound.c -- the instrument banks a sound card's own software
+// installed (see music.h): a Gravis UltraSound's patches (ULTRASND.INI in
+// the folder ULTRADIR names) and an AWE's bank for its sample ROM
+// (SFBANK\SYNTHGM.SF2 or SYNTHGM.SBK in the folder SOUND names, where
+// Creative's drivers put it).
+
+#include "musicInt.h"
+
+#include
+#include
+
+static const char *sRomBanks[] = { "SYNTHGM.SF2", "SYNTHGM.SBK" };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool musicFileThere(const char *path);
+bool musicPatchSetPath(char *out, int32_t outSize);
+bool musicRomBankPath(char *out, int32_t outSize);
+
+
+bool musicFileThere(const char *path) {
+ struct stat st;
+
+ return stat(path, &st) == 0;
+}
+
+
+bool musicPatchSetPath(char *out, int32_t outSize) {
+ const char *dir = getenv(MUSIC_PATCH_SET_ENV);
+
+ return dir && dir[0] && platformPathJoin(out, outSize, dir, MUSIC_PATCH_SET_INI) && musicFileThere(out);
+}
+
+
+bool musicRomBankPath(char *out, int32_t outSize) {
+ const char *sound = getenv(MUSIC_ROM_BANK_ENV);
+ char dir[DVX_MAX_PATH];
+
+ if (!sound || !sound[0] || !platformPathJoin(dir, sizeof(dir), sound, MUSIC_ROM_BANK_DIR)) {
+ return false;
+ }
+
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sRomBanks); i++) {
+ if (platformPathJoin(out, outSize, dir, sRomBanks[i]) && musicFileThere(out)) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/libs/kpunch/music/musicInt.h b/src/libs/kpunch/music/musicInt.h
index d8c0d5a3..90c3c056 100644
--- a/src/libs/kpunch/music/musicInt.h
+++ b/src/libs/kpunch/music/musicInt.h
@@ -24,30 +24,51 @@
//
// The parts of the library and how they connect:
//
-// musicSmf.c Standard MIDI File -> one list of events, times in
-// microseconds (MidiFileT)
-// musicSeq.c the sequencer (SeqT: sends the events that are due to a
-// MidiSinkT), the event ring that carries MIDI from the
-// timer interrupt to the software synthesizer, and the
-// timer callback that plays MIDI on the synthesizers
-// musicFm.c General MIDI on two-operator FM voices (FmSynthT): the
-// channels' state, notes, the OP2 bank's patches, pitch
-// and loudness, on the FM chip or the software synthesizer
-// musicMpu.c MIDI out of the MPU-401 port (MpuSinkT): General MIDI
-// as it is, or remapped for a Roland MT-32, paced
-// musicSynth.c the software FM synthesizer (SynthChipT): an integer
-// two-operator FM chip rendering into a sound stream
-// musicOp2.c the OP2 instrument bank
-// musicXmp.c tracker modules through libxmp-lite
-// music.c musics, programs, streams, the public functions
+// musicSmf.c Standard MIDI File -> one list of events, times in
+// microseconds (MidiFileT)
+// musicSeq.c the sequencer (SeqT: sends the events that are due
+// to a MidiSinkT), the event ring that carries MIDI
+// from the timer interrupt to the software
+// synthesizer, and the music timer
+// musicFm.c General MIDI on two-operator FM voices (FmSynthT):
+// the channels' state, notes, the OP2 bank's patches,
+// pitch and loudness, on the FM chip or the software
+// synthesizer
+// musicSynth.c the software FM synthesizer (SynthChipT): an
+// integer two-operator FM chip rendering into a stream
+// musicOp2.c the OP2 instrument bank
+// musicMpu.c MIDI out of the MPU-401 port (MpuSinkT): General MIDI
+// as it is, or remapped for a Roland MT-32, paced
+// musicSf2.c SoundFont banks (WaveBankT)
+// musicPat.c a Gravis UltraSound patch set as a SoundFont bank
+// musicFound.c the banks a sound card's own software installed
+// musicWave.c General MIDI on wavetable voices (WaveSynthT), on
+// the wavetable synthesizer or in software
+// musicWaveLoad.c a SoundFont's samples in the synthesizer's memory
+// musicWaveSoft.c the SoundFont software synthesizer (WaveSoftT)
+// musicCents.c cents, powers of two, and attenuation in fixed point
+// musicXmp.c tracker modules through libxmp-lite
+// musicSound.c WAV and VOC files (SoundSrcT): a WAV read from the
+// disk as it plays, a VOC decoded into memory
+// musicModHw.c a module on a wavetable synthesizer's voices: its
+// samples there, libxmp's voices made into frames
+// ahead (ModHwT)
+// musicModTick.c ... the frames played by the music timer
+// musicMml.c QBasic's music macro language
+// musicMmlPlay.c music macro queues
+// musicBas.c DVX BASIC's entry points
+// musicShell.c the library's hooks into the shell
+// music.c musics, programs, streams, the public functions
//
-// musicSeq.c, musicFm.c and musicMpu.c also run at interrupt time (the
-// timer service plays MIDI on the FM chip and the MPU-401 port), so they
-// keep to the interrupt rules (audioDrv.h): integer arithmetic, no calls
-// out of those files but audio.lib's interrupt-safe FM and MIDI
-// functions, everything pinned (the Makefile links them last, so one
-// span from their first function to the end of the library's data covers
-// them; musicLock).
+// musicSeq.c, musicFm.c, musicMpu.c, musicWave.c, musicCents.c and
+// musicModTick.c also run at interrupt time (the music timer plays MIDI
+// on the FM chip, the wavetable synthesizer and the MPU-401 port, and
+// modules on the wavetable synthesizer), so they keep to the interrupt
+// rules (audioDrv.h): integer arithmetic, no calls out of those files
+// but audio.lib's interrupt-safe FM, MIDI and wavetable functions,
+// everything pinned (the Makefile links them last, so one span from
+// their first function to the end of the library's data covers them;
+// musicLock).
#ifndef MUSIC_INT_H
#define MUSIC_INT_H
@@ -77,6 +98,10 @@
#define MIDI_DATA_BITS 7
#define MIDI_CENTER 64
+// A channel message of this type (status AND MIDI_TYPE_MASK) carries one
+// data byte (program change, channel pressure); the others carry two.
+#define MIDI_ONE_DATA_BYTE(type) ((type) == MIDI_PROGRAM || (type) == MIDI_CHAN_PRESSURE)
+
// Controllers
#define MIDI_CC_BANK 0
#define MIDI_CC_MODULATION 1
@@ -126,6 +151,7 @@ typedef struct {
uint32_t count;
uint32_t sysexBytes;
uint32_t length; // microseconds: the last event or end of track
+ char title[MUSIC_TITLE_MAX]; // the first track's name (meta event 03), "" when it has none
} MidiFileT;
// Most events and system exclusive bytes a file may have
@@ -140,6 +166,18 @@ void midiFileFree(MidiFileT *file);
bool midiIsFile(const uint8_t *data, uint32_t size);
int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out);
+// Writing one (musicSmf.c): a track's events go into an stb_ds byte
+// array -- a number in bytes bytes (most significant first), a delta time
+// or other variable-length number, a tempo event at the start, the end of
+// the track delta ticks after the last event. midiFileBuild makes a type
+// 0 file of the track (malloc'd into *out): MUSIC_ERR_NO_MEMORY or
+// MUSIC_OK.
+int32_t midiFileBuild(const uint8_t *trk, uint32_t trkLen, uint32_t division, uint8_t **out, uint32_t *size);
+void midiPutBig(uint8_t **buf, uint32_t value, int32_t bytes);
+void midiPutEnd(uint8_t **trk, uint32_t delta);
+void midiPutTempo(uint8_t **trk, uint32_t usPerQuarter);
+void midiPutVlq(uint8_t **buf, uint32_t value);
+
// ============================================================
// Where MIDI goes (musicSeq.c, musicFm.c, musicMpu.c)
// ============================================================
@@ -239,6 +277,187 @@ typedef struct {
// MUSIC_OK or MUSIC_ERR_BANK (not an OP2 bank, too short).
int32_t op2Parse(const uint8_t *data, uint32_t size, Op2BankT *bank);
+// A loaded bank, shared: every MIDI music that plays on it holds a
+// reference, and so does the library while it is DVX.INI's choice.
+// Pinned (the timer reads it); freed with its last reference.
+typedef struct {
+ Op2BankT op2;
+ int32_t refs;
+ char name[DVX_MAX_PATH]; // as DVX.INI's midiBank names it ("" = the standard one)
+} MusicBankT;
+
+// ============================================================
+// SoundFont banks (musicSf2.c)
+// ============================================================
+
+// The SoundFont 2 generators (the specification's numbers and order)
+#define SF_GEN_START_OFFSET 0
+#define SF_GEN_END_OFFSET 1
+#define SF_GEN_LOOP_START_OFFSET 2
+#define SF_GEN_LOOP_END_OFFSET 3
+#define SF_GEN_START_COARSE 4
+#define SF_GEN_MOD_LFO_TO_PITCH 5
+#define SF_GEN_VIB_LFO_TO_PITCH 6
+#define SF_GEN_MOD_ENV_TO_PITCH 7
+#define SF_GEN_FILTER_FC 8
+#define SF_GEN_FILTER_Q 9
+#define SF_GEN_MOD_LFO_TO_FILTER 10
+#define SF_GEN_MOD_ENV_TO_FILTER 11
+#define SF_GEN_END_COARSE 12
+#define SF_GEN_MOD_LFO_TO_VOLUME 13
+#define SF_GEN_CHORUS_SEND 15
+#define SF_GEN_REVERB_SEND 16
+#define SF_GEN_PAN 17
+#define SF_GEN_MOD_LFO_DELAY 21
+#define SF_GEN_MOD_LFO_FREQ 22
+#define SF_GEN_VIB_LFO_DELAY 23
+#define SF_GEN_VIB_LFO_FREQ 24
+#define SF_GEN_MOD_ENV_DELAY 25
+#define SF_GEN_MOD_ENV_ATTACK 26
+#define SF_GEN_MOD_ENV_HOLD 27
+#define SF_GEN_MOD_ENV_DECAY 28
+#define SF_GEN_MOD_ENV_SUSTAIN 29
+#define SF_GEN_MOD_ENV_RELEASE 30
+#define SF_GEN_KEY_TO_MOD_ENV_HOLD 31
+#define SF_GEN_KEY_TO_MOD_ENV_DECAY 32
+#define SF_GEN_VOL_ENV_DELAY 33
+#define SF_GEN_VOL_ENV_ATTACK 34
+#define SF_GEN_VOL_ENV_HOLD 35
+#define SF_GEN_VOL_ENV_DECAY 36
+#define SF_GEN_VOL_ENV_SUSTAIN 37
+#define SF_GEN_VOL_ENV_RELEASE 38
+#define SF_GEN_KEY_TO_VOL_ENV_HOLD 39
+#define SF_GEN_KEY_TO_VOL_ENV_DECAY 40
+#define SF_GEN_INSTRUMENT 41
+#define SF_GEN_KEY_RANGE 43
+#define SF_GEN_VEL_RANGE 44
+#define SF_GEN_LOOP_START_COARSE 45
+#define SF_GEN_KEYNUM 46
+#define SF_GEN_VELOCITY 47
+#define SF_GEN_ATTENUATION 48
+#define SF_GEN_LOOP_END_COARSE 50
+#define SF_GEN_COARSE_TUNE 51
+#define SF_GEN_FINE_TUNE 52
+#define SF_GEN_SAMPLE_ID 53
+#define SF_GEN_SAMPLE_MODES 54
+#define SF_GEN_SCALE_TUNING 56
+#define SF_GEN_EXCLUSIVE_CLASS 57
+#define SF_GEN_ROOT_KEY 58
+#define SF_GEN_COUNT 61
+
+// sampleModes: how a sample loops
+#define SF_LOOP_NONE 0
+#define SF_LOOP_ON 1 // loops for as long as it sounds
+#define SF_LOOP_RELEASE 3 // loops while the key is held, then plays out
+
+// Sample types (a ROM sample's bit set: in the synthesizer's own memory)
+#define SF_SAMPLE_MONO 1
+#define SF_SAMPLE_RIGHT 2
+#define SF_SAMPLE_LEFT 4
+#define SF_SAMPLE_ROM 0x8000
+
+#define SF_NAME_LEN 20
+#define SF_DRUM_BANK 128 // General MIDI percussion, on channel 10
+#define SF_ROOT_NONE (-1) // overridingRootKey / keynum / velocity not set
+
+// A sample, in frames of the bank's 16-bit sample data: start .. end - 1;
+// the loop from loopStart to loopEnd - 1.
+typedef struct {
+ uint32_t start;
+ uint32_t end;
+ uint32_t loopStart;
+ uint32_t loopEnd;
+ uint32_t rate; // Hz
+ uint16_t type; // SF_SAMPLE_*
+ uint8_t rootKey; // the MIDI note it sounds as recorded
+ int8_t correction; // cents to add
+} WaveSampleT;
+
+// What one note plays: a sample with every generator resolved (the
+// instrument's, its global zone's, and the preset's added), for the keys
+// and velocities in its ranges.
+typedef struct {
+ int16_t gen[SF_GEN_COUNT];
+ uint16_t sample;
+ uint8_t keyLo;
+ uint8_t keyHi;
+ uint8_t velLo;
+ uint8_t velHi;
+} WaveRegionT;
+
+// A preset: its regions in WaveBankT.regions
+typedef struct {
+ uint32_t first;
+ uint32_t count;
+ uint16_t bank;
+ uint16_t program;
+ char name[SF_NAME_LEN + 1];
+} WavePresetT;
+
+// Where a bank's frames are when they are not one SoundFont's data (a
+// Gravis patch set: each wave in its own .PAT file): a run of the bank's
+// frames read from a file's bytes, 8- or 16-bit, signed or not. Frames
+// no source covers read as silence.
+#define WAVE_SRC_16BIT 0x01
+#define WAVE_SRC_UNSIGNED 0x02
+
+typedef struct {
+ uint32_t first; // bank frame
+ uint32_t frames;
+ uint32_t offset; // bytes into the file
+ uint16_t file; // in WaveBankT.files
+ uint8_t format; // WAVE_SRC_*
+} WaveSourceT;
+
+// A SoundFont: the presets, regions, and samples, in malloc'd arrays; the
+// sample data stays where it is (in the file, or the memory it was read
+// from; or, with sources, in the files they name) until a song needs it
+// (waveBankRead).
+typedef struct {
+ WavePresetT *presets;
+ WaveRegionT *regions;
+ WaveSampleT *samples;
+ WaveSourceT *sources; // NULL: the data is path's (or memory's)
+ char (*files)[DVX_MAX_PATH]; // the sources' files
+ uint32_t presetCount;
+ uint32_t regionCount;
+ uint32_t sampleCount;
+ uint32_t sourceCount;
+ uint32_t fileCount;
+ uint32_t dataOffset; // of the sample data, bytes from the start
+ uint32_t dataFrames;
+ const uint8_t *memory; // the whole file in memory, or NULL: path
+ char path[DVX_MAX_PATH];
+} WaveBankT;
+
+// sf2Load reads a SoundFont 2 file's presets, instruments, and sample
+// headers; sf2Parse the same from a whole file in memory (which must
+// last as long as the bank). MUSIC_OK, MUSIC_ERR_OPEN, _BANK (not a
+// SoundFont, or damaged), _NO_MEMORY.
+int32_t sf2Load(const char *path, WaveBankT *bank);
+int32_t sf2Parse(const uint8_t *data, uint32_t size, WaveBankT *bank);
+
+// A generator's value where no zone sets it (the specification's
+// default; 0 for one out of range).
+int16_t sf2GenDefault(int32_t gen);
+
+// A Gravis UltraSound patch set as a bank (musicPat.c): an UltraSound
+// folder's ULTRASND.INI (iniPath) and the .PAT files it names; bank 0's
+// 128 programs and the standard kit, the sample data left in the files.
+// MUSIC_OK, MUSIC_ERR_OPEN (no such INI), _BANK (no patch), _NO_MEMORY.
+int32_t patLoadSet(const char *iniPath, WaveBankT *bank);
+
+// A file or folder is at path (musicFound.c).
+bool musicFileThere(const char *path);
+
+// The bank's memory given back (the bank cleared).
+void waveBankFree(WaveBankT *bank);
+
+
+// frames frames of sample data from frame first into out (16-bit);
+// false when they cannot be read.
+bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out);
+
// ============================================================
// General MIDI on FM voices (musicFm.c)
// ============================================================
@@ -310,6 +529,237 @@ void fmSynthSetGain(FmSynthT *synth, int32_t gain);
#define FM_PITCH_STEPS 32
void fmPitchToFreq(int32_t pitch, uint32_t *fnum, uint32_t *block);
+// ============================================================
+// Cents in fixed point (musicCents.c)
+// ============================================================
+
+#define WAVE_CENTS_OCTAVE 1200
+#define WAVE_FIX_SHIFT 16 // 16.16
+#define WAVE_FIX_ONE 65536U
+
+// cents as whole octaves (returned, rounded down) and 2^(the rest / 1200)
+// in 16.16 (*frac). Interrupt-safe.
+int32_t waveOctaves(int32_t cents, uint32_t *frac);
+
+// 2^(cents / 1200) in 16.16 (cents from -16 to +15 octaves).
+// Interrupt-safe.
+uint32_t wavePow2(int32_t cents);
+
+// 1200 log2(num / den): the cents between two rates (program time).
+int32_t waveRatioCents(uint32_t num, uint32_t den);
+
+// 200 log10(WAVE_STEP_MAX / i): the attenuation in centibels of an
+// amplitude of i / WAVE_STEP_MAX (0: AUDIO_WT_ATTEN_MAX); a volume or
+// gain of 0..AUDIO_VOLUME_MAX indexes it by halves.
+#define WAVE_STEP_MAX 128
+#define WAVE_GAIN_SHIFT 1
+extern const int16_t waveCbForStep[WAVE_STEP_MAX + 1];
+
+// This file's interrupt-time code and tables for musicLock (how many).
+int32_t waveCentsLockItems(AudioLockItemT *items);
+
+// ============================================================
+// General MIDI on wavetable voices (musicWave.c)
+// ============================================================
+
+#define WAVE_VOICES_MAX AUDIO_WT_VOICES_MAX
+#define WAVE_NOT_LOADED 0xFFFFFFFFU
+#define WAVE_GUARD_FRAMES 8 // frames stored after a sample's end (a chip's interpolation reads them)
+
+// Voices come from a backend: the wavetable synthesizer (audio.lib's
+// shared voices; waveHwBackend) or the software one (musicWaveSoft.c).
+// The calls are audio.lib's audioWt* ones; flags says what the backend
+// does itself (AUDIO_WT_*), and the synthesizer does the rest by updates.
+typedef struct {
+ int32_t (*alloc)(void *owner);
+ void (*free)(void *owner, int32_t voice);
+ void (*start)(void *owner, int32_t voice, const AudioWtVoiceT *v);
+ void (*update)(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what);
+ void (*release)(void *owner, int32_t voice);
+ bool (*active)(void *owner, int32_t voice);
+ void *owner;
+ uint32_t flags;
+} WaveBackendT;
+
+// A sample of the bank in the synthesizer's memory (musicWaveLoad.c)
+typedef struct {
+ uint32_t addr; // WAVE_NOT_LOADED: not there
+ uint32_t frames; // stored (from the sample's start, guard frames included)
+ int32_t rateCents; // 1200 log2(stored rate / 44,100)
+ uint8_t bits; // 8 or 16
+ uint8_t shift; // the frames were halved this many times to fit
+} WaveLoadedT;
+
+typedef struct {
+ int16_t bend; // -8192..8191
+ uint8_t program;
+ uint8_t bankMsb;
+ uint8_t volume;
+ uint8_t expression;
+ uint8_t pan;
+ uint8_t bendRange; // semitones
+ uint8_t modWheel;
+ uint8_t pressure;
+ uint8_t reverb;
+ uint8_t chorus;
+ uint8_t rpnMsb;
+ uint8_t rpnLsb;
+ bool sustain;
+} WaveChannelT;
+
+// Voice states
+#define WAVE_V_FREE 0
+#define WAVE_V_ON 1
+#define WAVE_V_HELD 2 // the key is up, the sustain pedal down
+#define WAVE_V_RELEASED 3
+
+// A sounding voice: the note as last sent to the backend, and (for a
+// backend without them) the envelopes and LFOs run here
+typedef struct {
+ AudioWtVoiceT note;
+ const WaveRegionT *region;
+ int32_t id; // backend voice (0: free slot)
+ uint32_t age;
+ int32_t basePitch; // cents: key, tuning, the sample's rate
+ int32_t baseAtten; // centibels: the region and the velocity
+ int32_t envAtten; // the volume envelope now (centibels)
+ int32_t relFrom; // ... where its release began
+ int32_t modEnv; // the modulation envelope now (0..WAVE_PERMILLE)
+ int32_t modFrom; // ... where its release began
+ uint32_t stageUs; // time in the volume envelope's stage
+ uint32_t modStageUs; // ... in the modulation envelope's
+ uint32_t sinceOn; // microseconds since the note began
+ uint32_t modPhase; // LFO phases (65536 a cycle)
+ uint32_t vibPhase;
+ int32_t sentAtten; // last attenuation and pitch sent
+ int32_t sentPitch;
+ uint8_t stage; // AUDIO_WT_ENV_*, or WAVE_ENV_DONE
+ uint8_t modStage;
+ uint8_t state; // WAVE_V_*
+ uint8_t channel;
+ uint8_t key;
+ uint8_t exclusive;
+} WaveVoiceT;
+
+#define WAVE_ENV_DONE AUDIO_WT_ENV_STAGES
+#define WAVE_PERMILLE 1000
+
+// overflow: where notes go that the bank cannot play here (no region, or
+// a sample not in the synthesizer's memory; NULL: they are not played);
+// it gets every other message too, so its channels are in step. gain:
+// the music's volume x the master volume (0..AUDIO_VOLUME_MAX).
+typedef struct {
+ MidiSinkT sink;
+ WaveBackendT backend;
+ const WaveBankT *bank;
+ const WaveLoadedT *loaded; // per sample of bank
+ MidiSinkT *overflow;
+ WaveChannelT channels[MUSIC_MIDI_CHANNELS];
+ WaveVoiceT voices[WAVE_VOICES_MAX];
+ uint32_t clock;
+ int32_t gain;
+} WaveSynthT;
+
+// The preset for a bank and program: that one; for drums (bank 128) the
+// standard kit when the kit asked for is missing; else the same program
+// in bank 0 (a variation the bank lacks); NULL when there is none.
+// Interrupt time.
+const WavePresetT *waveBankPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program);
+
+// The wavetable synthesizer as a backend (audio.lib's shared voices, for
+// ctx).
+void waveHwBackend(AudioCtxT *ctx, WaveBackendT *out);
+int32_t waveLockItems(AudioLockItemT *items);
+void waveSynthInit(WaveSynthT *synth, const WaveBackendT *backend, const WaveBankT *bank, const WaveLoadedT *loaded, MidiSinkT *overflow, int32_t gain);
+void waveSynthSetGain(WaveSynthT *synth, int32_t gain);
+
+// Move the notes on by us microseconds: the envelopes and LFOs the
+// backend does not run, voices that have fallen silent given back.
+void waveSynthTick(WaveSynthT *synth, uint32_t us);
+
+
+// ============================================================
+// SoundFont MIDI in software (musicWaveSoft.c)
+// ============================================================
+
+// The software wavetable synthesizer: a WaveBackendT whose voices are
+// mixed here (linear interpolation, the volume and pan as gains), from a
+// song's samples held in memory, 16-bit at their own rates. The
+// envelopes and LFOs are musicWave.c's (flags 0). It runs at program
+// time, in the music's stream fill.
+typedef struct {
+ uint32_t pos; // the frame in the arena
+ uint32_t frac; // ... and the 16-bit fraction past it
+ uint32_t step; // frames per output frame, 16.16
+ uint32_t loopStart; // frames in the arena
+ uint32_t loopEnd;
+ uint32_t end;
+ int32_t gainL; // 0..WAVE_SOFT_GAIN_ONE
+ int32_t gainR;
+ uint8_t loop; // AUDIO_WT_LOOP_*
+ bool used; // allocated
+ bool playing;
+} WaveSoftVoiceT;
+
+#define WAVE_SOFT_GAIN_SHIFT 15
+#define WAVE_SOFT_GAIN_ONE (1 << WAVE_SOFT_GAIN_SHIFT)
+
+typedef struct {
+ WaveSoftVoiceT voices[WAVE_VOICES_MAX];
+ int16_t *arena; // the samples loaded (malloc'd)
+ uint32_t frames; // in the arena
+ int32_t rateCents; // 1200 log2(44,100 / the output rate)
+} WaveSoftT;
+
+// The synthesizer's backend; its samples (need[i] set) read into its
+// arena (those already there kept), loaded[] saying where; and freed.
+// MUSIC_OK, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_OPEN.
+void waveSoftBackend(WaveSoftT *soft, uint32_t rate, WaveBackendT *out);
+void waveSoftFree(WaveSoftT *soft);
+int32_t waveSoftLoad(WaveSoftT *soft, const WaveBankT *bank, WaveLoadedT *loaded, const uint8_t *need);
+
+// frames of every playing voice added into out (16-bit, channels 1 or 2,
+// clipped).
+void waveSoftRender(WaveSoftT *soft, int16_t *out, uint32_t frames, uint32_t channels);
+
+// ============================================================
+// SoundFont samples in the synthesizer's memory (musicWaveLoad.c)
+// ============================================================
+
+// Seconds the library keeps the SoundFont's samples in the synthesizer
+// after the last music goes (music.c), for the next one.
+#define MUSIC_FONT_KEEP_SEC 60
+
+// The bank's samples in the synthesizer's memory, kept from song to song
+// while they fit; loaded (read at interrupt time) is pinned.
+typedef struct {
+ WaveLoadedT *loaded; // per bank sample
+ uint32_t *lastUse; // the song count when it was last needed
+ uint32_t count;
+ uint32_t songs;
+} WaveCacheT;
+
+// waveCacheInit: a cache for bank with nothing loaded; MUSIC_ERR_NO_MEMORY.
+// waveCacheFree gives its synthesizer memory back (ctx's).
+void waveCacheFree(AudioCtxT *ctx, WaveCacheT *cache);
+int32_t waveCacheInit(WaveCacheT *cache, const WaveBankT *bank);
+
+// The samples (need[sample] set) a file plays, from its bank selects,
+// programs and notes; those of one preset, at every key and velocity
+// (live MIDI, whose notes are not known), added to need: how many were
+// not needed before.
+void waveNeedsFile(const WaveBankT *bank, const MidiFileT *file, uint8_t *need);
+uint32_t waveNeedsPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program, uint8_t *need);
+
+// Every needed sample in ctx's synthesizer memory, at the best quality
+// that fits: 16-bit, 8-bit (where the chip plays it), then at half and a
+// quarter of the rate; samples not needed are given up first, those used
+// longest ago first. When they do not all fit even at the least, as many
+// as fit, and *missing says how many did not (their notes go to the
+// overflow). MUSIC_OK; MUSIC_ERR_NO_DEVICE when none fits (or the bank
+// cannot be read); MUSIC_ERR_NO_MEMORY.
+int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags, uint32_t *missing);
+
// ============================================================
// The MPU-401 port (musicMpu.c)
// ============================================================
@@ -419,6 +869,44 @@ void synthWrite(SynthChipT *chip, int32_t voice, uint8_t reg, uint8_t value);
typedef struct XmpPlayerT XmpPlayerT;
+// A sample of the module (xmpSample): frames of 8- or 16-bit signed data
+// (libxmp's own, which lasts as long as the module), its loop frames
+typedef struct {
+ const void *data;
+ uint32_t frames;
+ uint32_t loopStart;
+ uint32_t loopEnd;
+ bool bits16;
+ bool loops;
+ bool bidi; // a ping-pong loop
+ bool stereo; // two channels a frame (no synthesizer voice plays it)
+} XmpSampleT;
+
+// A voice libxmp reports as a frame starts when it plays on a
+// synthesizer's voices (xmpHwOn; libxmp's struct xmp_hw_voice)
+typedef struct {
+ double pos; // sample frames
+ double rate; // sample frames a second
+ uint32_t voice; // its voice, 0 .. the voices xmpHwOn gave
+ uint32_t sample; // xmpSample's number
+ uint32_t trigger; // changes when libxmp starts or moves the voice
+ uint32_t loopStart; // the loop it is in: its first frame
+ uint32_t loopEnd; // ... one past its last (no loop: the end)
+ int32_t vol; // 0 .. XMP_HW_VOL_MAX
+ int32_t pan; // -XMP_HW_PAN_SPAN (left) .. XMP_HW_PAN_SPAN - 1
+ uint8_t loop; // XMP_VOICE_LOOP_*
+ bool surround;
+ bool reverse; // playing backwards (a ping-pong loop's way back)
+} XmpHwVoiceT;
+
+#define XMP_HW_VOL_MAX 1024
+#define XMP_HW_PAN_SPAN 128
+#define XMP_VOICE_LOOP_NONE 0 // plays to loopEnd and stops
+#define XMP_VOICE_LOOP_FORWARD 1
+#define XMP_VOICE_LOOP_PINGPONG 2 // there and back (xmpBidiAdjust)
+
+typedef void (*XmpHwFnT)(void *user, const XmpHwVoiceT *voice);
+
// xmpOpen: MUSIC_OK, _FORMAT (not a module), _CORRUPT, _NO_MEMORY; the
// module is ready to render at rate (Hz) in channels (1 or 2) 16-bit
// frames. xmpRender renders frames frames; false once a song that does
@@ -432,6 +920,165 @@ void xmpRestart(XmpPlayerT *player);
void xmpSeek(XmpPlayerT *player, uint32_t ms);
const char *xmpTitle(XmpPlayerT *player);
+// The module's tracks and samples (false: no sample there), and the
+// frames its ping-pong loops are shortened by on the way back (Impulse
+// Tracker's 1: a loop of n frames takes 2n - 1 to play there and back).
+uint32_t xmpBidiAdjust(XmpPlayerT *player);
+uint32_t xmpChannels(XmpPlayerT *player);
+bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out);
+uint32_t xmpSampleCount(XmpPlayerT *player);
+
+// The module on a synthesizer's voices: libxmp started again with voices
+// voices (each of its voices one of the synthesizer's), mixing nothing,
+// fn told every sounding voice as each frame starts (false: it could not
+// start); xmpHwOff back to rendering. xmpHwFrame plays one frame (a
+// tracker tick), its length in *us; false once a song that does not loop
+// has ended (fn not called for that frame).
+bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us);
+void xmpHwOff(XmpPlayerT *player);
+bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user);
+
+// ============================================================
+// Sound files (musicSound.c)
+// ============================================================
+
+typedef struct SoundSrcT SoundSrcT;
+
+// A WAV or VOC file as the frames of a sound music. soundOpenFile reads
+// a WAV named by its path as it plays (only its start now); soundOpenMem
+// decodes a WAV or VOC file's bytes into memory. MUSIC_OK,
+// MUSIC_ERR_FORMAT (not a file the call takes: soundOpenFile takes only
+// WAV), _OPEN, _CORRUPT, _NO_MEMORY. soundRead gives up to frames 16-bit
+// frames from the place it is at (fewer at the end), soundSeek moves it.
+// Program time only.
+uint32_t soundChannels(const SoundSrcT *s);
+void soundClose(SoundSrcT *s);
+uint32_t soundFrames(const SoundSrcT *s);
+int32_t soundOpenFile(const char *path, SoundSrcT **out);
+int32_t soundOpenMem(const uint8_t *data, uint32_t size, SoundSrcT **out);
+uint32_t soundRate(const SoundSrcT *s);
+uint32_t soundRead(SoundSrcT *s, int16_t *out, uint32_t frames);
+void soundSeek(SoundSrcT *s, uint32_t frame);
+uint32_t soundTell(const SoundSrcT *s);
+
+// ============================================================
+// Tracker modules on a wavetable synthesizer (musicModHw.c: the samples
+// and the frames; musicModTick.c: the frames played at interrupt time)
+// ============================================================
+
+// A module's sound made ahead of what is heard: its stream's buffer, or
+// its frames queued for the timer.
+#define MUSIC_MODULE_AHEAD_MS 400
+
+#define MODHW_FRAMES 64 // frames queued (a power of two)
+#define MODHW_ENTRIES 1024 // voices queued, a frame's each (a power of two)
+
+// A module sample in the synthesizer's memory: from addr, frames frames
+// (a ping-pong loop unrolled forward: its way back stored after its end,
+// the frames past that dropped)
+typedef struct {
+ uint32_t addr; // WAVE_NOT_LOADED: not there
+ uint32_t frames;
+ uint32_t loopStart; // the module's loop (for a ping-pong one)
+ uint32_t loopEnd;
+ uint32_t backEnd; // a ping-pong loop unrolled: where its way back ends (0: not)
+ uint8_t bits;
+} ModHwSampleT;
+
+// A voice as a frame plays it, in the synthesizer's terms
+typedef struct {
+ uint32_t trigger; // libxmp's count of its starts and moves
+ uint32_t offset; // the frame it starts from (when it starts)
+ uint32_t loopStart;
+ uint32_t loopEnd;
+ uint32_t end;
+ int32_t pitch; // cents
+ int16_t atten; // centibels (the music's gain not in it)
+ int16_t pan;
+ uint16_t sample;
+ uint8_t voice; // libxmp's voice
+ uint8_t loop; // AUDIO_WT_LOOP_*
+} ModHwEntryT;
+
+// A frame: its voices (entries first .. first + count - 1), or the
+// song's end
+typedef struct {
+ uint32_t us; // its length
+ uint32_t ms; // the song's time at its start
+ uint32_t first;
+ uint32_t count;
+ bool end;
+} ModHwFrameT;
+
+// One of libxmp's voices on the synthesizer
+typedef struct {
+ AudioWtVoiceT note; // as last sent
+ int32_t id; // the synthesizer's voice (0: none)
+ uint32_t trigger;
+ uint16_t sample;
+} ModHwSlotT;
+
+// The frames are a queue: the program side adds at the heads, the timer
+// takes from the tails; the frame at frameTail plays once applied, its
+// entries kept until the next one plays (a pause starts its voices
+// again). Each side writes only its own counts. Pinned: the timer
+// reads it all.
+typedef struct {
+ ModHwEntryT entries[MODHW_ENTRIES];
+ ModHwFrameT frames[MODHW_FRAMES];
+ ModHwSlotT slots[AUDIO_WT_VOICES_MAX];
+ AudioCtxT *ctx;
+ ModHwSampleT *samples; // per module sample (pinned)
+ uint32_t sampleCount;
+ uint32_t voices; // libxmp's
+ uint32_t bidiAdjust;
+ int32_t level; // centibels every voice is lowered by: libxmp's level and the chip's
+ uint32_t staged; // program side: entries the frame being made has (0 between frames)
+ volatile uint32_t entryHead; // program side
+ volatile uint32_t frameHead;
+ volatile uint32_t madeUs; // the frames' lengths added
+ volatile uint32_t entryTail; // timer
+ volatile uint32_t frameTail;
+ volatile uint32_t playedUs; // ... the lengths of those played
+ volatile uint32_t usInto; // ... time into the frame playing
+ volatile uint32_t ms; // ... the song's time at its start
+ volatile int32_t gainAtten; // centibels for the music's gain (the next frame sends it)
+ volatile bool applied; // the frame at frameTail plays
+ bool endMade; // program side: the end is queued
+} ModHwT;
+
+// modHwOpen: the module (libxmp's player) on ctx's wavetable
+// synthesizer at gain (the music's volume x the master volume), its
+// samples in the synthesizer's memory, libxmp switched to reporting its
+// voices; MUSIC_OK, MUSIC_ERR_NO_MEMORY, MUSIC_ERR_NO_DEVICE (no
+// synthesizer, too few voices for the module's channels, a stereo
+// sample), MODHW_ERR_ROOM (the samples do not fit in its memory).
+// modHwClose undoes it (libxmp back to rendering, where the song was).
+#define MODHW_ERR_ROOM (-MUSIC_ERR_COUNT) // modHwOpen only; never leaves the library
+void modHwClose(ModHwT *mod, XmpPlayerT *xmp);
+int32_t modHwOpen(AudioCtxT *ctx, XmpPlayerT *xmp, int32_t gain, ModHwT **out);
+
+// Frames made until MUSIC_MODULE_AHEAD_MS are queued (or the song's
+// end: a song that does not loop ends after one play).
+void modHwFill(ModHwT *mod, XmpPlayerT *xmp, bool loop);
+
+// The queue emptied and the voices stopped (before libxmp is moved: a
+// seek, a restart); the song's time is ms until the next frame plays.
+void modHwFlush(ModHwT *mod, uint32_t ms);
+
+// The song's time heard; the gain changed; the voices stopped (a pause:
+// the frame playing starts them again at the next tick that plays).
+uint32_t modHwPosition(const ModHwT *mod);
+void modHwSetGain(ModHwT *mod, int32_t gain);
+void modHwSilence(ModHwT *mod);
+
+// Interrupt time: the frames on by us microseconds; false once the
+// song's end plays (the voices stopped). modHwStop stops every voice
+// (with interrupts off at program time).
+int32_t modHwLockItems(AudioLockItemT *items);
+void modHwStop(ModHwT *mod);
+bool modHwTick(ModHwT *mod, uint32_t us);
+
// ============================================================
// Players at interrupt time (musicSeq.c) and the library (music.c)
// ============================================================
@@ -441,12 +1088,15 @@ const char *xmpTitle(XmpPlayerT *player);
#define PLAY_PLAYING 1
#define PLAY_PAUSED 2
-// The part of a MIDI music the timer interrupt plays (on the FM chip or
-// the MPU-401 port).
+// The part of a music the timer interrupt plays: MIDI (on the FM chip,
+// the wavetable chip, or the MPU-401 port) or a module (on the wavetable
+// chip).
typedef struct {
SeqT seq;
MidiSinkT *sink;
MpuSinkT *mpu; // drained every tick (NULL: not the MPU-401)
+ WaveSynthT *wave; // ticked every tick (NULL: not the wavetable chip)
+ ModHwT *mod; // a module on the wavetable chip (NULL: MIDI)
volatile int32_t state;
volatile bool ended; // played to its end: the end call is due
} IsrPlayerT;
@@ -481,6 +1131,7 @@ bool musicLock(bool lock);
// Instruments (I): 0 plays square-wave tones, 1..MML_PROGRAMS General MIDI
// instrument n (program n - 1)
#define MML_PROGRAM_TONE 0
+#define MML_PROGRAM_PIANO 1 // the acoustic grand piano, General MIDI's first
#define MML_PROGRAMS 128
// The settings a macro changes, which last from one string to the next.
@@ -521,15 +1172,19 @@ void musicMmlIdle(void);
void musicMmlShutdown(void);
// music.c: the program running now, the library's end of a program's
-// life, the shell's idle time and its own unloading (musicShell.c hooks
-// them up), making appId the current program (returns the one before),
-// the MIDI bank in use (loaded on first need; NULL when it cannot be).
+// life, the shell's idle time, a DOS program run with DVX stepped aside
+// (musicSuspend, a platform suspend hook) and its own unloading
+// (musicShell.c hooks them up), making appId the current program
+// (returns the one before).
int32_t musicAppCurrent(void);
void musicAppEnded(int32_t appId);
int32_t musicAppEnter(int32_t appId);
-const Op2BankT *musicBank(int32_t *err);
void musicBasForget(int32_t appId);
void musicIdle(void *unused);
void musicShutdown(void);
+void musicSuspend(bool suspending);
+
+// musicMidiRec.c: a program's MIDI recordings go when it ends.
+void musicMidiRecordAppEnded(int32_t appId);
#endif // MUSIC_INT_H
diff --git a/src/libs/kpunch/music/musicMidiRec.c b/src/libs/kpunch/music/musicMidiRec.c
new file mode 100644
index 00000000..951f5ad8
--- /dev/null
+++ b/src/libs/kpunch/music/musicMidiRec.c
@@ -0,0 +1,237 @@
+// 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.
+
+// musicMidiRec.c -- recording MIDI into a Standard MIDI File
+//
+// A recording keeps the channel messages it is given with their times
+// and writes them as a type 0 file: one track of 1,000 ticks a quarter
+// note at 60 quarter notes a minute, so a tick is a millisecond. Every
+// message is written with its status byte (no running status). A note
+// still held at the end is let go at the last message's time, so the
+// file never leaves a note sounding. A recording is the calling
+// program's own and goes when it ends (musicMidiRecordAppEnded).
+
+#include "musicInt.h"
+#include "stb_ds.h"
+
+#include
+#include
+#include
+
+#define SMF_TICKS_PER_QUARTER 1000 // with SMF_US_PER_QUARTER, a tick is a millisecond
+#define SMF_US_PER_QUARTER 1000000
+#define HELD_WORD_BITS 32
+#define HELD_WORDS ((MUSIC_MIDI_DATA_MAX + 1) / HELD_WORD_BITS)
+
+typedef struct {
+ uint32_t ms;
+ uint8_t status;
+ uint8_t data1;
+ uint8_t data2;
+} RecMsgT;
+
+struct MusicMidiRecordS {
+ RecMsgT *msgs; // stb_ds, in time order
+ MusicMidiRecordT *next; // every recording (sRecords)
+ int32_t appId;
+};
+
+static MusicMidiRecordT *sRecords = NULL;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+int32_t musicMidiRecordAdd(MusicMidiRecordT *rec, uint32_t ms, uint8_t status, uint8_t data1, uint8_t data2);
+void musicMidiRecordAppEnded(int32_t appId);
+uint32_t musicMidiRecordCount(const MusicMidiRecordT *rec);
+void musicMidiRecordFree(MusicMidiRecordT *rec);
+uint32_t musicMidiRecordLength(const MusicMidiRecordT *rec);
+int32_t musicMidiRecordNew(MusicMidiRecordT **out);
+int32_t musicMidiRecordSave(const MusicMidiRecordT *rec, const char *path);
+static void putMsg(uint8_t **trk, uint32_t delta, uint8_t status, uint8_t data1, uint8_t data2);
+static uint8_t *trackBytes(const MusicMidiRecordT *rec);
+
+
+int32_t musicMidiRecordAdd(MusicMidiRecordT *rec, uint32_t ms, uint8_t status, uint8_t data1, uint8_t data2) {
+ if (!rec || (status & MIDI_STATUS_BIT) == 0 || status >= MIDI_SYSEX || data1 > MUSIC_MIDI_DATA_MAX || data2 > MUSIC_MIDI_DATA_MAX) {
+ return MUSIC_ERR_ARG;
+ }
+
+ ptrdiff_t count = arrlen(rec->msgs);
+ RecMsgT msg = { ms, status, data1, MIDI_ONE_DATA_BYTE(status & MIDI_TYPE_MASK) ? 0 : data2 };
+
+ if (count > 0 && msg.ms < rec->msgs[count - 1].ms) {
+ msg.ms = rec->msgs[count - 1].ms;
+ }
+
+ arrput(rec->msgs, msg);
+ return MUSIC_OK;
+}
+
+
+void musicMidiRecordAppEnded(int32_t appId) {
+ MusicMidiRecordT *rec = sRecords;
+
+ while (rec) {
+ MusicMidiRecordT *next = rec->next;
+
+ if (rec->appId == appId) {
+ musicMidiRecordFree(rec);
+ }
+
+ rec = next;
+ }
+}
+
+
+uint32_t musicMidiRecordCount(const MusicMidiRecordT *rec) {
+ return rec ? (uint32_t)arrlen(rec->msgs) : 0;
+}
+
+
+void musicMidiRecordFree(MusicMidiRecordT *rec) {
+ if (!rec) {
+ return;
+ }
+
+ MusicMidiRecordT **link = &sRecords;
+
+ while (*link && *link != rec) {
+ link = &(*link)->next;
+ }
+
+ if (*link) {
+ *link = rec->next;
+ }
+
+ arrfree(rec->msgs);
+ free(rec);
+}
+
+
+uint32_t musicMidiRecordLength(const MusicMidiRecordT *rec) {
+ ptrdiff_t count = rec ? arrlen(rec->msgs) : 0;
+
+ return count > 0 ? rec->msgs[count - 1].ms : 0;
+}
+
+
+int32_t musicMidiRecordNew(MusicMidiRecordT **out) {
+ *out = NULL;
+
+ MusicMidiRecordT *rec = (MusicMidiRecordT *)calloc(1, sizeof(MusicMidiRecordT));
+
+ if (!rec) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ rec->appId = musicAppCurrent();
+ rec->next = sRecords;
+ sRecords = rec;
+ *out = rec;
+ return MUSIC_OK;
+}
+
+
+// A file that is not all written is removed.
+int32_t musicMidiRecordSave(const MusicMidiRecordT *rec, const char *path) {
+ if (!rec || !path || !path[0]) {
+ return MUSIC_ERR_ARG;
+ }
+
+ uint8_t *trk = trackBytes(rec);
+ uint8_t *file = NULL;
+ uint32_t size = 0;
+ int32_t rc = midiFileBuild(trk, (uint32_t)arrlen(trk), SMF_TICKS_PER_QUARTER, &file, &size);
+
+ arrfree(trk);
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+
+ FILE *f = fopen(path, "wb");
+ bool ok = f != NULL;
+
+ if (f) {
+ ok = fwrite(file, 1, size, f) == size;
+ ok = (fclose(f) == 0) && ok;
+
+ if (!ok) {
+ remove(path);
+ }
+ }
+
+ free(file);
+ return ok ? MUSIC_OK : MUSIC_ERR_WRITE;
+}
+
+
+static void putMsg(uint8_t **trk, uint32_t delta, uint8_t status, uint8_t data1, uint8_t data2) {
+ midiPutVlq(trk, delta);
+ arrput(*trk, status);
+ arrput(*trk, data1);
+
+ if (!MIDI_ONE_DATA_BYTE(status & MIDI_TYPE_MASK)) {
+ arrput(*trk, data2);
+ }
+}
+
+
+// The track's events: the tempo, the messages, a note off for every note
+// still held, and the end of the track.
+static uint8_t *trackBytes(const MusicMidiRecordT *rec) {
+ uint32_t held[MUSIC_MIDI_CHANNELS][HELD_WORDS];
+ uint8_t *trk = NULL;
+ uint32_t last = 0;
+
+ memset(held, 0, sizeof(held));
+ midiPutTempo(&trk, SMF_US_PER_QUARTER);
+
+ for (ptrdiff_t i = 0; i < arrlen(rec->msgs); i++) {
+ const RecMsgT *m = &rec->msgs[i];
+ uint8_t kind = m->status & MIDI_TYPE_MASK;
+ uint32_t *channel = held[m->status & MIDI_CHANNEL_MASK];
+ uint32_t bit = 1U << (m->data1 % HELD_WORD_BITS);
+
+ if (kind == MIDI_NOTE_ON && m->data2 > 0) {
+ channel[m->data1 / HELD_WORD_BITS] |= bit;
+ } else if (kind == MIDI_NOTE_ON || kind == MIDI_NOTE_OFF) {
+ channel[m->data1 / HELD_WORD_BITS] &= ~bit;
+ }
+
+ putMsg(&trk, m->ms - last, m->status, m->data1, m->data2);
+ last = m->ms;
+ }
+
+ for (int32_t ch = 0; ch < MUSIC_MIDI_CHANNELS; ch++) {
+ for (int32_t note = 0; note <= MUSIC_MIDI_DATA_MAX; note++) {
+ if (held[ch][note / HELD_WORD_BITS] & (1U << (note % HELD_WORD_BITS))) {
+ putMsg(&trk, 0, (uint8_t)(MIDI_NOTE_OFF | ch), (uint8_t)note, 0);
+ }
+ }
+ }
+
+ midiPutEnd(&trk, 0);
+ return trk;
+}
diff --git a/src/libs/kpunch/music/musicMml.c b/src/libs/kpunch/music/musicMml.c
index faaac9fe..4eea5993 100644
--- a/src/libs/kpunch/music/musicMml.c
+++ b/src/libs/kpunch/music/musicMml.c
@@ -33,8 +33,6 @@
#include "stb_ds.h"
#include
-#include
-#include
// Ranges and defaults (QBasic's)
#define MML_OCTAVE_MAX 6
@@ -71,19 +69,7 @@ static const int32_t sLetterSemitone[] = { 9, 11, 0, 2, 4, 5, 7 };
#define SMF_DIVISION 10000
#define SMF_TEMPO_US 1000000
#define SMF_US_PER_TICK (SMF_TEMPO_US / SMF_DIVISION)
-#define SMF_HEADER_LEN 6
-#define SMF_FORMAT_0 0
-#define SMF_TRACKS 1
-#define SMF_TAG_BYTES 4
-#define SMF_LEN_BYTES 4
-#define SMF_META_TEMPO 0x51
-#define SMF_META_END 0x2F
-#define SMF_TEMPO_BYTES 3
#define SMF_VELOCITY 100
-#define SMF_VLQ_MAX_BYTES 5
-#define SMF_WORD_BYTES 2
-#define BITS_PER_BYTE 8
-#define BYTE_MASK 0xFF
// ============================================================
// Prototypes
@@ -97,8 +83,6 @@ void mmlStateInit(MmlStateT *st);
static uint32_t pitchMilliHz(int32_t pitch);
static bool readNumber(const char **p, int32_t *out);
static bool readRanged(const char **p, int32_t lo, int32_t hi, int32_t *out);
-static void smfBig(uint8_t **buf, uint32_t value, int32_t bytes);
-static void smfVlq(uint8_t **buf, uint32_t value);
// A note of pitch (semitones from octave 0's C; -1 a rest) and length
@@ -279,11 +263,7 @@ int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_
uint32_t lastTick = 0;
int32_t program = MML_PROGRAM_TONE;
- smfVlq(&trk, 0);
- arrput(trk, MIDI_META);
- arrput(trk, SMF_META_TEMPO);
- arrput(trk, SMF_TEMPO_BYTES);
- smfBig(&trk, SMF_TEMPO_US, SMF_TEMPO_BYTES);
+ midiPutTempo(&trk, SMF_TEMPO_US);
for (int32_t i = 0; i < count; i++) {
const MmlNoteT *n = ¬es[i];
@@ -292,18 +272,18 @@ int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_
if (n->program != program && n->program != MML_PROGRAM_TONE) {
program = n->program;
- smfVlq(&trk, onAt - lastTick);
+ midiPutVlq(&trk, onAt - lastTick);
arrput(trk, MIDI_PROGRAM);
arrput(trk, (uint8_t)(program - 1));
lastTick = onAt;
}
if (n->milliHz) {
- smfVlq(&trk, onAt - lastTick);
+ midiPutVlq(&trk, onAt - lastTick);
arrput(trk, MIDI_NOTE_ON);
arrput(trk, (uint8_t)n->midiNote);
arrput(trk, SMF_VELOCITY);
- smfVlq(&trk, offAt - onAt);
+ midiPutVlq(&trk, offAt - onAt);
arrput(trk, MIDI_NOTE_OFF);
arrput(trk, (uint8_t)n->midiNote);
arrput(trk, 0);
@@ -313,41 +293,12 @@ int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_
startUs += n->us;
}
- smfVlq(&trk, (uint32_t)(startUs / SMF_US_PER_TICK) - lastTick);
- arrput(trk, MIDI_META);
- arrput(trk, SMF_META_END);
- arrput(trk, 0);
+ midiPutEnd(&trk, (uint32_t)(startUs / SMF_US_PER_TICK) - lastTick);
- uint32_t trkLen = (uint32_t)arrlen(trk);
- uint32_t total = SMF_TAG_BYTES + SMF_LEN_BYTES + SMF_HEADER_LEN + SMF_TAG_BYTES + SMF_LEN_BYTES + trkLen;
- uint8_t *file = (uint8_t *)malloc(total);
- uint8_t *hdr = NULL;
+ int32_t rc = midiFileBuild(trk, (uint32_t)arrlen(trk), SMF_DIVISION, out, size);
- if (!file) {
- arrfree(trk);
- return MUSIC_ERR_NO_MEMORY;
- }
-
- arrput(hdr, 'M');
- arrput(hdr, 'T');
- arrput(hdr, 'h');
- arrput(hdr, 'd');
- smfBig(&hdr, SMF_HEADER_LEN, SMF_LEN_BYTES);
- smfBig(&hdr, SMF_FORMAT_0, SMF_WORD_BYTES);
- smfBig(&hdr, SMF_TRACKS, SMF_WORD_BYTES);
- smfBig(&hdr, SMF_DIVISION, SMF_WORD_BYTES);
- arrput(hdr, 'M');
- arrput(hdr, 'T');
- arrput(hdr, 'r');
- arrput(hdr, 'k');
- smfBig(&hdr, trkLen, SMF_LEN_BYTES);
- memcpy(file, hdr, arrlen(hdr));
- memcpy(file + arrlen(hdr), trk, trkLen);
- arrfree(hdr);
arrfree(trk);
- *out = file;
- *size = total;
- return MUSIC_OK;
+ return rc;
}
@@ -396,29 +347,3 @@ static bool readRanged(const char **p, int32_t lo, int32_t hi, int32_t *out) {
*out = v;
return true;
}
-
-
-// value in bytes bytes, most significant first.
-static void smfBig(uint8_t **buf, uint32_t value, int32_t bytes) {
- for (int32_t i = bytes - 1; i >= 0; i--) {
- arrput(*buf, (uint8_t)((value >> (i * BITS_PER_BYTE)) & BYTE_MASK));
- }
-}
-
-
-// A variable-length quantity: 7 bits a byte, most significant first, the
-// top bit set on all but the last.
-static void smfVlq(uint8_t **buf, uint32_t value) {
- uint8_t tmp[SMF_VLQ_MAX_BYTES];
- int32_t n = 0;
-
- do {
- tmp[n++] = (uint8_t)(value & MIDI_DATA_MASK);
- value >>= MIDI_DATA_BITS;
- } while (value);
-
- while (n > 0) {
- n--;
- arrput(*buf, (uint8_t)(tmp[n] | (n > 0 ? MIDI_STATUS_BIT : 0)));
- }
-}
diff --git a/src/libs/kpunch/music/musicModHw.c b/src/libs/kpunch/music/musicModHw.c
new file mode 100644
index 00000000..a2691957
--- /dev/null
+++ b/src/libs/kpunch/music/musicModHw.c
@@ -0,0 +1,384 @@
+// 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.
+
+// musicModHw.c -- tracker modules on a wavetable synthesizer's voices
+// (see musicInt.h, "Tracker modules on a wavetable synthesizer")
+//
+// libxmp plays the module with DVX's hardware voice patch (xmpHwOn): it
+// mixes nothing and reports its voices as each frame (a tracker tick)
+// starts. Here, at program time (libxmp uses floating point), frames
+// are made ahead, up to MUSIC_MODULE_AHEAD_MS, into a queue of voices in
+// the synthesizer's terms (the sample, the frame to start from, the
+// loop, pitch in cents, attenuation, pan); the music timer plays them at
+// their times (musicModTick.c). The music's gain is added there, so a
+// volume change is heard at the next frame.
+//
+// The samples go into the synthesizer's memory when the module opens: as
+// they are (an 8-bit one stays 8-bit where the chip plays those), else
+// all 8-bit. The chips loop only forward, so a ping-pong loop is
+// unrolled: its way back stored after its end (the frames past the loop
+// dropped, as they are never heard), each frame where libxmp reads it
+// (Impulse Tracker's way back a frame shorter: xmpBidiAdjust). What the
+// chips cannot do plays near enough: a ping-pong sustain loop goes
+// forward, a sample played backwards (no loop) goes forward, Impulse
+// Tracker's filters are not heard, and a voice that libxmp has no
+// synthesizer voice for (a program holds the rest) is not heard.
+
+#include "musicInt.h"
+
+#include
+#include
+
+#define BITS_8 8
+#define BITS_16 16
+#define BYTE_BITS 8
+#define SAMPLE_8_SHIFT 8 // an 8-bit frame as 16-bit
+#define RATE_FIX 16 // a rate's sixteenths count in its pitch
+#define VOL_SHIFT 3 // XMP_HW_VOL_MAX >> VOL_SHIFT = WAVE_STEP_MAX
+#define LEVEL_CB 80 // a voice at full volume 8 dB down: as loud as libxmp mixes it (measured on a GUS alone)
+#define US_PER_MS 1000
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int16_t frameAt(const XmpSampleT *xs, uint32_t i);
+void modHwClose(ModHwT *mod, XmpPlayerT *xmp);
+void modHwFill(ModHwT *mod, XmpPlayerT *xmp, bool loop);
+void modHwFlush(ModHwT *mod, uint32_t ms);
+static void modHwFree(ModHwT *mod);
+int32_t modHwOpen(AudioCtxT *ctx, XmpPlayerT *xmp, int32_t gain, ModHwT **out);
+uint32_t modHwPosition(const ModHwT *mod);
+void modHwSetGain(ModHwT *mod, int32_t gain);
+void modHwSilence(ModHwT *mod);
+static void modHwVoice(void *user, const XmpHwVoiceT *v);
+static int32_t sampleLoad(ModHwT *mod, const XmpSampleT *xs, uint32_t i, uint32_t bits);
+static void samplesFree(ModHwT *mod);
+static int32_t samplesLoad(ModHwT *mod, XmpPlayerT *xmp, uint32_t flags);
+
+
+// Frame i of the sample as 16-bit.
+static int16_t frameAt(const XmpSampleT *xs, uint32_t i) {
+ if (xs->bits16) {
+ return ((const int16_t *)xs->data)[i];
+ }
+
+ return (int16_t)(((const int8_t *)xs->data)[i] * (1 << SAMPLE_8_SHIFT));
+}
+
+
+void modHwClose(ModHwT *mod, XmpPlayerT *xmp) {
+ if (!mod) {
+ return;
+ }
+
+ uint32_t flags = platformIrqSave();
+
+ modHwStop(mod);
+ platformIrqRestore(flags);
+ xmpHwOff(xmp);
+ modHwFree(mod);
+}
+
+
+// A frame is made while there is room for every voice it may have. The
+// song's end is a frame of its own.
+void modHwFill(ModHwT *mod, XmpPlayerT *xmp, bool loop) {
+ while (!mod->endMade && mod->madeUs - mod->playedUs < MUSIC_MODULE_AHEAD_MS * US_PER_MS && mod->frameHead - mod->frameTail < MODHW_FRAMES && MODHW_ENTRIES - (mod->entryHead - mod->entryTail) >= mod->voices) {
+ ModHwFrameT *f = &mod->frames[mod->frameHead & (MODHW_FRAMES - 1)];
+ uint32_t us = 0;
+
+ f->ms = xmpPosition(xmp);
+ f->end = !xmpHwFrame(xmp, loop, &us);
+ f->us = f->end ? 0 : us;
+ f->first = mod->entryHead;
+ f->count = f->end ? 0 : mod->staged;
+ mod->staged = 0;
+ mod->endMade = f->end;
+ mod->madeUs += f->us;
+ mod->entryHead += f->count;
+ __atomic_signal_fence(__ATOMIC_RELEASE);
+ mod->frameHead++;
+ }
+}
+
+
+void modHwFlush(ModHwT *mod, uint32_t ms) {
+ uint32_t flags = platformIrqSave();
+
+ modHwStop(mod);
+ mod->frameTail = mod->frameHead;
+ mod->entryTail = mod->entryHead;
+ mod->playedUs = mod->madeUs;
+ mod->usInto = 0;
+ mod->applied = false;
+ mod->ms = ms;
+ platformIrqRestore(flags);
+ mod->endMade = false;
+}
+
+
+// Its samples out of the synthesizer's memory, unpinned and freed.
+static void modHwFree(ModHwT *mod) {
+ if (mod->samples) {
+ samplesFree(mod);
+ platformUnlockRegion(mod->samples, mod->sampleCount * sizeof(ModHwSampleT), false);
+ }
+
+ free(mod->samples);
+ platformUnlockRegion(mod, sizeof(*mod), false);
+ free(mod);
+}
+
+
+int32_t modHwOpen(AudioCtxT *ctx, XmpPlayerT *xmp, int32_t gain, ModHwT **out) {
+ AudioWtInfoT info;
+ XmpSampleT xs;
+ uint32_t count = xmpSampleCount(xmp);
+ bool stereo = false;
+
+ *out = NULL;
+ audioWtGetInfo(&info);
+
+ for (uint32_t i = 0; i < count; i++) {
+ stereo = stereo || (xmpSample(xmp, i, &xs) && xs.stereo);
+ }
+
+ uint32_t voices = info.voices < AUDIO_WT_VOICES_MAX ? info.voices : AUDIO_WT_VOICES_MAX;
+
+ if (!info.present || voices < xmpChannels(xmp) || stereo) {
+ return MUSIC_ERR_NO_DEVICE;
+ }
+
+ ModHwT *mod = (ModHwT *)calloc(1, sizeof(ModHwT));
+
+ if (!mod) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ if (platformLockRegion(mod, sizeof(*mod), false) != 0) {
+ free(mod);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ mod->ctx = ctx;
+ mod->voices = voices;
+ mod->bidiAdjust = xmpBidiAdjust(xmp);
+ mod->level = LEVEL_CB + info.level;
+ mod->sampleCount = count;
+ mod->samples = (ModHwSampleT *)calloc(count ? count : 1, sizeof(ModHwSampleT));
+
+ if (!mod->samples || platformLockRegion(mod->samples, count * sizeof(ModHwSampleT), false) != 0) {
+ free(mod->samples);
+ mod->samples = NULL;
+ modHwFree(mod);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ mod->samples[i].addr = WAVE_NOT_LOADED;
+ }
+
+ for (uint32_t v = 0; v < AUDIO_WT_VOICES_MAX; v++) {
+ audioWtNoteInit(&mod->slots[v].note);
+ }
+
+ int32_t rc = samplesLoad(mod, xmp, info.flags);
+
+ if (rc == MUSIC_OK && !xmpHwOn(xmp, voices, modHwVoice, mod)) {
+ rc = MUSIC_ERR_NO_MEMORY;
+ }
+
+ if (rc != MUSIC_OK) {
+ modHwFree(mod);
+ return rc;
+ }
+
+ modHwSetGain(mod, gain);
+ mod->ms = xmpPosition(xmp);
+ *out = mod;
+ return MUSIC_OK;
+}
+
+
+uint32_t modHwPosition(const ModHwT *mod) {
+ return mod->ms + mod->usInto / US_PER_MS;
+}
+
+
+void modHwSetGain(ModHwT *mod, int32_t gain) {
+ int32_t g = gain < 0 ? 0 : (gain > AUDIO_VOLUME_MAX ? AUDIO_VOLUME_MAX : gain);
+
+ mod->gainAtten = waveCbForStep[g >> WAVE_GAIN_SHIFT];
+}
+
+
+void modHwSilence(ModHwT *mod) {
+ uint32_t flags = platformIrqSave();
+
+ modHwStop(mod);
+ mod->applied = false;
+ platformIrqRestore(flags);
+}
+
+
+// libxmp's voice as the frame being made has it (xmpHwOn's function): a
+// voice on a sample not in the synthesizer's memory, or at its end, is
+// left out (and so is any beyond the room modHwFill made: libxmp has no
+// more voices than that).
+static void modHwVoice(void *user, const XmpHwVoiceT *v) {
+ ModHwT *mod = (ModHwT *)user;
+
+ if (mod->staged >= mod->voices || v->sample >= mod->sampleCount || v->voice >= mod->voices || mod->samples[v->sample].addr == WAVE_NOT_LOADED) {
+ return;
+ }
+
+ const ModHwSampleT *s = &mod->samples[v->sample];
+ ModHwEntryT *e = &mod->entries[(mod->entryHead + mod->staged) & (MODHW_ENTRIES - 1)];
+ uint32_t pos = (uint32_t)v->pos;
+ double rate = v->rate * RATE_FIX;
+ int32_t vol = v->vol < 0 ? 0 : (v->vol > XMP_HW_VOL_MAX ? XMP_HW_VOL_MAX : v->vol);
+
+ if (v->loop == XMP_VOICE_LOOP_PINGPONG && s->backEnd && v->loopStart == s->loopStart && v->loopEnd == s->loopEnd) {
+ // On the way back: where libxmp reads, in the stored way back
+ e->loop = AUDIO_WT_LOOP_ON;
+ e->loopStart = s->loopStart;
+ e->loopEnd = s->backEnd;
+ e->end = s->backEnd;
+ e->offset = v->reverse ? 2 * s->loopEnd - mod->bidiAdjust - pos : pos;
+ e->offset = e->offset < s->backEnd ? e->offset : s->loopStart;
+ } else {
+ e->loop = v->loop == XMP_VOICE_LOOP_NONE ? AUDIO_WT_LOOP_NONE : AUDIO_WT_LOOP_ON;
+ e->loopStart = v->loopStart < s->frames ? v->loopStart : s->frames;
+ e->loopEnd = v->loopEnd < s->frames ? v->loopEnd : s->frames;
+ e->end = e->loopEnd;
+ e->offset = pos;
+ }
+
+ if (e->offset >= e->end || (e->loop != AUDIO_WT_LOOP_NONE && e->loopEnd <= e->loopStart)) {
+ return;
+ }
+
+ e->trigger = v->trigger;
+ e->pitch = waveRatioCents(rate < (double)UINT32_MAX ? (uint32_t)rate : UINT32_MAX, AUDIO_WT_PITCH_RATE * RATE_FIX);
+ e->atten = (int16_t)(waveCbForStep[vol >> VOL_SHIFT] + mod->level);
+ e->pan = (int16_t)(v->surround ? 0 : v->pan * AUDIO_WT_PAN_MAX / XMP_HW_PAN_SPAN);
+ e->sample = (uint16_t)v->sample;
+ e->voice = (uint8_t)v->voice;
+ mod->staged++;
+}
+
+
+// Sample i into the synthesizer's memory at bits: its frames (a ping-pong
+// loop unrolled), then WAVE_GUARD_FRAMES the chip's interpolation reads
+// past the end (the loop again, where the stored frames end with it;
+// else silence). MODHW_ERR_ROOM when it does not fit.
+static int32_t sampleLoad(ModHwT *mod, const XmpSampleT *xs, uint32_t i, uint32_t bits) {
+ ModHwSampleT *s = &mod->samples[i];
+ uint32_t span = xs->loopEnd - xs->loopStart;
+ uint32_t back = xs->bidi && span > mod->bidiAdjust ? span - mod->bidiAdjust : 0;
+ uint32_t body = back ? xs->loopEnd : xs->frames;
+ uint32_t frames = body + back;
+ uint32_t repeat = xs->loops && (back || xs->loopEnd == xs->frames) ? frames - xs->loopStart : 0;
+ int16_t *pcm = (int16_t *)malloc((frames + WAVE_GUARD_FRAMES) * sizeof(int16_t));
+ uint32_t addr = 0;
+
+ if (!pcm) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ for (uint32_t k = 0; k < body; k++) {
+ pcm[k] = frameAt(xs, k);
+ }
+
+ // The way back: frame k of it is libxmp's loopEnd - adjust - k (its
+ // first, at the end, reads the last)
+ for (uint32_t k = 0; k < back; k++) {
+ uint32_t from = xs->loopEnd - mod->bidiAdjust - k;
+
+ pcm[body + k] = frameAt(xs, from < xs->loopEnd ? from : xs->loopEnd - 1);
+ }
+
+ for (uint32_t g = 0; g < WAVE_GUARD_FRAMES; g++) {
+ pcm[frames + g] = repeat ? pcm[xs->loopStart + g % repeat] : 0;
+ }
+
+ int32_t rc = audioWtMemAlloc(mod->ctx, (frames + WAVE_GUARD_FRAMES) * (bits / BYTE_BITS), bits, &addr);
+
+ if (rc == AUDIO_OK && audioWtUpload(mod->ctx, addr, pcm, frames + WAVE_GUARD_FRAMES, bits) != AUDIO_OK) {
+ audioWtMemFree(mod->ctx, addr);
+ rc = AUDIO_ERR_ARG;
+ }
+
+ free(pcm);
+
+ if (rc != AUDIO_OK) {
+ return rc == AUDIO_ERR_NO_ROOM ? MODHW_ERR_ROOM : MUSIC_ERR_NO_DEVICE;
+ }
+
+ s->addr = addr;
+ s->frames = frames;
+ s->loopStart = xs->loopStart;
+ s->loopEnd = xs->loopEnd;
+ s->backEnd = back ? frames : 0;
+ s->bits = (uint8_t)bits;
+ return MUSIC_OK;
+}
+
+
+static void samplesFree(ModHwT *mod) {
+ for (uint32_t i = 0; i < mod->sampleCount; i++) {
+ if (mod->samples[i].addr != WAVE_NOT_LOADED) {
+ audioWtMemFree(mod->ctx, mod->samples[i].addr);
+ }
+
+ mod->samples[i].addr = WAVE_NOT_LOADED;
+ }
+}
+
+
+// Every sample as it is (8-bit ones 8-bit where the chip plays those),
+// else (some do not fit) all 8-bit where it does; a sample libxmp has no
+// data for is left out.
+static int32_t samplesLoad(ModHwT *mod, XmpPlayerT *xmp, uint32_t flags) {
+ bool chip8 = (flags & AUDIO_WT_8BIT) != 0;
+ int32_t rc = MODHW_ERR_ROOM;
+
+ for (uint32_t pass = 0; pass < (chip8 ? 2U : 1U) && rc == MODHW_ERR_ROOM; pass++) {
+ XmpSampleT xs;
+
+ rc = MUSIC_OK;
+ samplesFree(mod);
+
+ for (uint32_t i = 0; i < mod->sampleCount && rc == MUSIC_OK; i++) {
+ if (xmpSample(xmp, i, &xs)) {
+ rc = sampleLoad(mod, &xs, i, chip8 && (pass > 0 || !xs.bits16) ? BITS_8 : BITS_16);
+ }
+ }
+ }
+
+ if (rc != MUSIC_OK) {
+ samplesFree(mod);
+ }
+
+ return rc;
+}
diff --git a/src/libs/kpunch/music/musicModTick.c b/src/libs/kpunch/music/musicModTick.c
new file mode 100644
index 00000000..c64ea150
--- /dev/null
+++ b/src/libs/kpunch/music/musicModTick.c
@@ -0,0 +1,215 @@
+// 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.
+
+// musicModTick.c -- a module's frames played on the wavetable
+// synthesizer by the music timer (see musicInt.h, "Tracker modules on a
+// wavetable synthesizer")
+//
+// Runs at interrupt time (see musicInt.h). A frame is applied as it
+// starts: each of its voices started on the synthesizer when it is new
+// there, when libxmp started or moved it, or when its sample or loop
+// changed; else its pitch, attenuation (the music's gain added), and pan
+// sent where they changed. A voice of libxmp's that the frame does not
+// have is stopped and its synthesizer voice given back. When the next
+// frame is not made yet the one playing goes on, its voices sounding.
+
+#include "musicInt.h"
+
+#define LOCK_ITEMS 5
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool frameApply(ModHwT *mod);
+int32_t modHwLockItems(AudioLockItemT *items);
+void modHwStop(ModHwT *mod);
+bool modHwTick(ModHwT *mod, uint32_t us);
+static void slotApply(ModHwT *mod, const ModHwEntryT *e);
+static void slotStop(const ModHwT *mod, ModHwSlotT *slot);
+
+
+// The frame at frameTail onto the voices; false: it is the song's end
+// (every voice stopped).
+static bool frameApply(ModHwT *mod) {
+ const ModHwFrameT *f = &mod->frames[mod->frameTail & (MODHW_FRAMES - 1)];
+ uint32_t seen = 0;
+
+ if (f->end) {
+ modHwStop(mod);
+ return false;
+ }
+
+ for (uint32_t i = 0; i < f->count; i++) {
+ const ModHwEntryT *e = &mod->entries[(f->first + i) & (MODHW_ENTRIES - 1)];
+
+ seen |= 1U << e->voice;
+ slotApply(mod, e);
+ }
+
+ for (uint32_t v = 0; v < mod->voices; v++) {
+ if (!(seen & (1U << v))) {
+ slotStop(mod, &mod->slots[v]);
+ }
+ }
+
+ mod->ms = f->ms;
+ return true;
+}
+
+
+int32_t modHwLockItems(AudioLockItemT *items) {
+ const void *fns[LOCK_ITEMS] = {
+ (const void *)frameApply,
+ (const void *)modHwStop,
+ (const void *)modHwTick,
+ (const void *)slotApply,
+ (const void *)slotStop
+ };
+
+ for (int32_t i = 0; i < LOCK_ITEMS; i++) {
+ items[i].addr = fns[i];
+ items[i].size = 0;
+ }
+
+ return LOCK_ITEMS;
+}
+
+
+void modHwStop(ModHwT *mod) {
+ for (uint32_t v = 0; v < mod->voices; v++) {
+ slotStop(mod, &mod->slots[v]);
+ }
+}
+
+
+// The frames on by us: each that has played its length gives way to the
+// next one made (none made: it goes on). A frame not applied yet (the
+// first, or the one playing after a pause) is applied first.
+bool modHwTick(ModHwT *mod, uint32_t us) {
+ if (!mod->applied) {
+ if (mod->frameTail == mod->frameHead) {
+ return true;
+ }
+
+ __atomic_signal_fence(__ATOMIC_ACQUIRE);
+ mod->applied = true;
+
+ if (!frameApply(mod)) {
+ return false;
+ }
+ }
+
+ mod->usInto += us;
+
+ for (;;) {
+ const ModHwFrameT *f = &mod->frames[mod->frameTail & (MODHW_FRAMES - 1)];
+
+ if (mod->usInto < f->us) {
+ break;
+ }
+
+ if (mod->frameTail + 1 == mod->frameHead) {
+ mod->usInto = f->us;
+ break;
+ }
+
+ __atomic_signal_fence(__ATOMIC_ACQUIRE);
+ mod->usInto -= f->us;
+ mod->playedUs += f->us;
+ mod->frameTail++;
+ mod->entryTail = mod->frames[mod->frameTail & (MODHW_FRAMES - 1)].first;
+
+ if (!frameApply(mod)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+// One voice of the frame: started (a voice taken when it has none; none
+// free: it is not heard) or changed.
+static void slotApply(ModHwT *mod, const ModHwEntryT *e) {
+ ModHwSlotT *slot = &mod->slots[e->voice];
+ AudioWtVoiceT *n = &slot->note;
+ const ModHwSampleT *smp = &mod->samples[e->sample];
+ int32_t atten = e->atten + mod->gainAtten;
+ uint32_t what = 0;
+
+ atten = atten < AUDIO_WT_ATTEN_MAX ? atten : AUDIO_WT_ATTEN_MAX;
+
+ if (!slot->id || slot->trigger != e->trigger || slot->sample != e->sample || n->loop != e->loop || n->loopStart != e->loopStart || n->loopEnd != e->loopEnd || n->end != e->end) {
+ if (!slot->id) {
+ int32_t id = audioWtAlloc(mod->ctx);
+
+ if (id <= 0) {
+ return;
+ }
+
+ slot->id = id;
+ }
+
+ slot->trigger = e->trigger;
+ slot->sample = e->sample;
+ n->addr = smp->addr;
+ n->bits = smp->bits;
+ n->offset = e->offset;
+ n->loopStart = e->loopStart;
+ n->loopEnd = e->loopEnd;
+ n->end = e->end;
+ n->loop = e->loop;
+ n->pitch = e->pitch;
+ n->attenuation = atten;
+ n->pan = e->pan;
+ audioWtStart(mod->ctx, slot->id, n);
+ return;
+ }
+
+ if (n->pitch != e->pitch) {
+ n->pitch = e->pitch;
+ what |= AUDIO_WT_SET_PITCH;
+ }
+
+ if (n->attenuation != atten) {
+ n->attenuation = atten;
+ what |= AUDIO_WT_SET_VOLUME;
+ }
+
+ if (n->pan != e->pan) {
+ n->pan = e->pan;
+ what |= AUDIO_WT_SET_PAN;
+ }
+
+ if (what) {
+ audioWtUpdate(mod->ctx, slot->id, n, what);
+ }
+}
+
+
+static void slotStop(const ModHwT *mod, ModHwSlotT *slot) {
+ if (slot->id) {
+ audioWtFree(mod->ctx, slot->id);
+ slot->id = 0;
+ }
+}
diff --git a/src/libs/kpunch/music/musicMpu.c b/src/libs/kpunch/music/musicMpu.c
index 54925321..d3eecccb 100644
--- a/src/libs/kpunch/music/musicMpu.c
+++ b/src/libs/kpunch/music/musicMpu.c
@@ -253,7 +253,7 @@ static void mpuQueueMsg(MpuSinkT *m, uint8_t status, uint8_t data1, uint8_t data
uint8_t type = status & MIDI_TYPE_MASK;
const uint8_t msg[MPU_MSG_BYTES_MAX] = { status, data1, data2 };
- mpuQueue(m, msg, (type == MIDI_PROGRAM || type == MIDI_CHAN_PRESSURE) ? MPU_MSG_BYTES_MAX - 1 : MPU_MSG_BYTES_MAX);
+ mpuQueue(m, msg, MIDI_ONE_DATA_BYTE(type) ? MPU_MSG_BYTES_MAX - 1 : MPU_MSG_BYTES_MAX);
}
diff --git a/src/libs/kpunch/music/musicPat.c b/src/libs/kpunch/music/musicPat.c
new file mode 100644
index 00000000..49a5c959
--- /dev/null
+++ b/src/libs/kpunch/music/musicPat.c
@@ -0,0 +1,547 @@
+// 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.
+
+// musicPat.c -- a Gravis UltraSound patch set as a SoundFont bank (see
+// musicInt.h, patLoadSet)
+//
+// ULTRASND.INI in an UltraSound folder (ULTRADIR) maps General MIDI
+// programs ([Melodic Bank 0]: 0..127) and drum keys ([Drum Bank 0]: the
+// MIDI note) to patch names (BLANK or nothing: none; the older
+// [Melodic Patches] / [Drum Patches] where the banks are missing); each
+// is NAME.PAT in its PatchDir, else ULTRADIR\MIDI. Only the headers are
+// read here: a .PAT is a 129-byte header, a 63-byte instrument and a
+// 47-byte layer header (the file's sizes there are not to be trusted),
+// then its waves, each a 96-byte header and its data. Each wave becomes
+// a sample (its frames read from the file when a song needs them:
+// WaveBankT sources) and a region:
+//
+// keys from the wave's lowest and highest frequency (one wave:
+// every key); kept apart where they overlap (the GUS plays
+// the later wave) and stretched over the gaps
+// pitch the patch's pivot key (scale_frequency) is the root, the
+// keys scale by scale_factor / 1024 semitones (0: one pitch
+// for every key, as drums do), the root frequency's distance
+// from the pivot's goes in the tuning (coarse and fine)
+// loop looping or not (backwards and bidirectional loops play
+// forwards)
+// envelope the six GF1 volume points: the first is the attack (its
+// level the region's attenuation), the third (with sustain)
+// or the last the sustain level, the second's rate the
+// decay, the fourth's the release; GF1 rates as times at 24
+// voices (a frame of 38.873 us)
+// LFOs tremolo (to volume, up to 12 dB) and vibrato (to pitch, up
+// to an octave) at 0.05 + rate x 0.0233 Hz; the vibrato's
+// sweep as its delay
+// pan balance 0..15
+
+#include "musicInt.h"
+#include "stb_ds.h"
+
+#include
+#include
+#include
+#include
+
+#define PAT_MIDI_DIR "MIDI"
+#define PAT_EXT ".PAT"
+#define PAT_BLANK "BLANK"
+#define PAT_SECTION_DIR "Ultrasound"
+#define PAT_SECTION_MELODIC "Melodic Bank 0"
+#define PAT_SECTION_DRUMS "Drum Bank 0"
+#define PAT_LEGACY_MELODIC "Melodic Patches"
+#define PAT_LEGACY_DRUMS "Drum Patches"
+#define PAT_KEY_DIR "PatchDir"
+#define PAT_KIT_NAME "Standard"
+#define PAT_KEY_LEN 8
+
+// File layout
+#define PAT_MAGIC "GF1PATCH1" // GF1PATCH100 or GF1PATCH110
+#define PAT_MAGIC_LEN 9
+#define PAT_HEADER_BYTES 129
+#define PAT_INSTR_BYTES 63
+#define PAT_LAYER_BYTES 47
+#define PAT_WAVE_BYTES 96
+#define PAT_WAVES_AT 85 // the header's wave count
+#define W_NAME_BYTES 7
+#define W_SIZE 8
+#define W_LOOP_START 12
+#define W_LOOP_END 16
+#define W_RATE 20
+#define W_LOW 22
+#define W_HIGH 26
+#define W_ROOT 30
+#define W_BALANCE 36
+#define W_ENV_RATE 37
+#define W_ENV_OFFSET 43
+#define W_TREM_RATE 50
+#define W_TREM_DEPTH 51
+#define W_VIB_SWEEP 52
+#define W_VIB_RATE 53
+#define W_VIB_DEPTH 54
+#define W_MODES 55
+#define W_SCALE_FREQ 56
+#define W_SCALE_FACTOR 58
+#define MODE_16BIT 0x01
+#define MODE_UNSIGNED 0x02
+#define MODE_LOOP 0x04
+#define MODE_SUSTAIN 0x20
+#define MODE_ENVELOPE 0x40
+#define ENV_POINTS 6
+#define ENV_SUSTAIN_POINT 2
+#define ENV_RELEASE_POINT 3
+
+// Units
+#define A4_MILLIHZ 440000
+#define A4_KEY_CENTS 6900
+#define CENTS_SEMI 100
+#define KEY_TOP 127
+#define SCALE_UNITY 1024 // scale_factor: a semitone a key
+#define BALANCE_MAX 15
+#define PAN_SPAN 1000
+#define PAN_HALF 500
+#define VOL_SHIFT 4 // envelope offsets: 12-bit volume >> 4
+#define VOL_FULL 4095
+#define VOL_STEP_CB_NUM 2352 // a 12-bit volume step: 0.2352 cB
+#define VOL_STEP_CB_DEN 10000
+#define ATTEN_GEN_NUM 10 // the region's attenuation counts 0.4 dB a unit
+#define ATTEN_GEN_DEN 4
+#define RATE_INC_MASK 0x3F
+#define RATE_RANGE_SHIFT 6
+#define RATE_RANGE_STEP 8 // each range step: 8 times slower
+#define FRAME_NS 38873 // a GF1 frame at 24 voices
+#define NS_PER_US 1000
+#define US_PER_SEC 1000000
+#define LFO_BASE_MILLIHZ 8176 // 0 absolute cents
+#define LFO_MILLIHZ_BASE 50 // 0.05 Hz
+#define LFO_MILLIHZ_NUM 2333 // + 0.02333 Hz a step
+#define LFO_MILLIHZ_DEN 100
+#define TREM_CB_FULL 120 // 12 dB at depth 255
+#define VIB_CENTS_FULL 1200
+#define DEPTH_STEPS 256
+#define SWEEP_SECS 45 // the vibrato's depth fades in over sweep / 45 s
+#define BITS_8_BYTES 1
+#define BITS_16_BYTES 2
+
+// A wave of a patch, as its header gives it
+typedef struct {
+ uint32_t dataAt; // bytes into the file
+ uint32_t size; // bytes
+ uint32_t loopStart; // bytes from the wave's start
+ uint32_t loopEnd;
+ uint32_t low; // milli-Hz
+ uint32_t high;
+ uint32_t root;
+ uint16_t rate; // Hz
+ uint16_t scaleKey; // the pivot key
+ uint16_t scaleFactor;
+ uint8_t balance;
+ uint8_t envRate[ENV_POINTS];
+ uint8_t envLevel[ENV_POINTS];
+ uint8_t tremRate;
+ uint8_t tremDepth;
+ uint8_t vibSweep;
+ uint8_t vibRate;
+ uint8_t vibDepth;
+ uint8_t modes;
+} PatWaveT;
+
+// The bank as it is built (stb_ds arrays)
+typedef struct {
+ WavePresetT *presets;
+ WaveRegionT *regions;
+ WaveSampleT *samples;
+ WaveSourceT *sources;
+ char (*files)[DVX_MAX_PATH];
+ uint32_t frames; // the bank's frames so far
+} PatBuildT;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t keyCents(uint32_t milliHz);
+static uint32_t le16(const uint8_t *p);
+static uint32_t le32(const uint8_t *p);
+static int32_t lfoCents(uint8_t rate);
+static void patAddWave(PatBuildT *b, const PatWaveT *w, uint16_t file, uint8_t keyLo, uint8_t keyHi);
+static bool patFind(PrefsHandleT *ini, const char *section, const char *dir, int32_t key, char *path);
+int32_t patLoadSet(const char *iniPath, WaveBankT *bank);
+static int32_t patRead(const char *path, PatWaveT **waves);
+static int16_t rampTc(uint32_t units, uint8_t rate);
+
+
+// 100 x the MIDI key (fractional) of a frequency.
+static int32_t keyCents(uint32_t milliHz) {
+ return A4_KEY_CENTS + waveRatioCents(milliHz ? milliHz : 1, A4_MILLIHZ);
+}
+
+
+static uint32_t le16(const uint8_t *p) {
+ return (uint32_t)p[0] | (uint32_t)p[1] << 8;
+}
+
+
+static uint32_t le32(const uint8_t *p) {
+ return le16(p) | le16(p + 2) << 16;
+}
+
+
+// An LFO rate byte (0.05 Hz + 0.0233 Hz a step) in absolute cents.
+static int32_t lfoCents(uint8_t rate) {
+ return waveRatioCents(LFO_MILLIHZ_BASE + rate * LFO_MILLIHZ_NUM / LFO_MILLIHZ_DEN, LFO_BASE_MILLIHZ);
+}
+
+
+// A wave as a sample (its frames from the file, silence after them) and
+// a region over keyLo..keyHi (see the top).
+static void patAddWave(PatBuildT *b, const PatWaveT *w, uint16_t file, uint8_t keyLo, uint8_t keyHi) {
+ uint32_t bytes = w->modes & MODE_16BIT ? BITS_16_BYTES : BITS_8_BYTES;
+ int32_t tune;
+ WaveSourceT src;
+ WaveSampleT s;
+ WaveRegionT r;
+
+ memset(&src, 0, sizeof(src));
+ src.first = b->frames;
+ src.frames = w->size / bytes;
+ src.offset = w->dataAt;
+ src.file = file;
+ src.format = (uint8_t)((w->modes & MODE_16BIT ? WAVE_SRC_16BIT : 0) | (w->modes & MODE_UNSIGNED ? WAVE_SRC_UNSIGNED : 0));
+ arrput(b->sources, src);
+
+ memset(&s, 0, sizeof(s));
+ s.start = src.first;
+ s.end = src.first + src.frames;
+ s.loopStart = src.first + w->loopStart / bytes;
+ s.loopEnd = src.first + w->loopEnd / bytes;
+ s.rate = w->rate;
+ s.type = SF_SAMPLE_MONO;
+ s.rootKey = (uint8_t)(w->scaleKey > KEY_TOP ? KEY_TOP : w->scaleKey);
+ s.loopEnd = s.loopEnd > s.end ? s.end : s.loopEnd;
+ s.loopStart = s.loopStart > s.loopEnd ? s.loopEnd : s.loopStart;
+ tune = s.rootKey * CENTS_SEMI - keyCents(w->root);
+ b->frames += src.frames + WAVE_GUARD_FRAMES;
+ arrput(b->samples, s);
+
+ memset(&r, 0, sizeof(r));
+
+ for (int32_t g = 0; g < SF_GEN_COUNT; g++) {
+ r.gen[g] = sf2GenDefault(g);
+ }
+
+ r.sample = (uint16_t)(arrlen(b->samples) - 1);
+ r.keyLo = keyLo;
+ r.keyHi = keyHi;
+ r.velHi = KEY_TOP;
+ r.gen[SF_GEN_SAMPLE_MODES] = w->modes & MODE_LOOP ? SF_LOOP_ON : SF_LOOP_NONE;
+ r.gen[SF_GEN_SCALE_TUNING] = (int16_t)(w->scaleFactor * CENTS_SEMI / SCALE_UNITY);
+ r.gen[SF_GEN_COARSE_TUNE] = (int16_t)(tune / CENTS_SEMI);
+ r.gen[SF_GEN_FINE_TUNE] = (int16_t)(tune % CENTS_SEMI);
+ r.gen[SF_GEN_PAN] = (int16_t)(w->balance * PAN_SPAN / BALANCE_MAX - PAN_HALF);
+
+ if (w->modes & MODE_ENVELOPE) {
+ uint32_t peak = (uint32_t)w->envLevel[0] << VOL_SHIFT;
+ uint32_t held = (uint32_t)w->envLevel[w->modes & MODE_SUSTAIN ? ENV_SUSTAIN_POINT : ENV_POINTS - 1] << VOL_SHIFT;
+
+ r.gen[SF_GEN_ATTENUATION] = (int16_t)((VOL_FULL - peak) * VOL_STEP_CB_NUM / VOL_STEP_CB_DEN * ATTEN_GEN_NUM / ATTEN_GEN_DEN);
+ r.gen[SF_GEN_VOL_ENV_ATTACK] = rampTc(peak, w->envRate[0]);
+ r.gen[SF_GEN_VOL_ENV_DECAY] = rampTc(VOL_FULL, w->envRate[1]);
+ r.gen[SF_GEN_VOL_ENV_SUSTAIN] = (int16_t)(held < peak ? (peak - held) * VOL_STEP_CB_NUM / VOL_STEP_CB_DEN : 0);
+ r.gen[SF_GEN_VOL_ENV_RELEASE] = rampTc(VOL_FULL, w->envRate[ENV_RELEASE_POINT]);
+ }
+
+ if (w->tremDepth) {
+ r.gen[SF_GEN_MOD_LFO_FREQ] = (int16_t)lfoCents(w->tremRate);
+ r.gen[SF_GEN_MOD_LFO_TO_VOLUME] = (int16_t)((w->tremDepth + 1) * TREM_CB_FULL / DEPTH_STEPS);
+ }
+
+ if (w->vibDepth) {
+ r.gen[SF_GEN_VIB_LFO_FREQ] = (int16_t)lfoCents(w->vibRate);
+ r.gen[SF_GEN_VIB_LFO_TO_PITCH] = (int16_t)((w->vibDepth + 1) * VIB_CENTS_FULL / DEPTH_STEPS);
+
+ if (w->vibSweep) {
+ r.gen[SF_GEN_VIB_LFO_DELAY] = (int16_t)waveRatioCents(w->vibSweep, SWEEP_SECS);
+ }
+ }
+
+ arrput(b->regions, r);
+}
+
+
+// The patch for key (a program, or a drum's note) in section: in its
+// PatchDir, else the folder's MIDI folder. False for none or BLANK.
+static bool patFind(PrefsHandleT *ini, const char *section, const char *dir, int32_t key, char *path) {
+ char keyName[PAT_KEY_LEN];
+ char file[DVX_MAX_PATH];
+ char midi[DVX_MAX_PATH];
+ const char *name;
+ const char *patchDir;
+
+ snprintf(keyName, sizeof(keyName), "%d", (int)key);
+ name = prefsGetString(ini, section, keyName, "");
+
+ if (!name[0] || strcasecmp(name, PAT_BLANK) == 0) {
+ return false;
+ }
+
+ snprintf(file, sizeof(file), "%s" PAT_EXT, name);
+ patchDir = prefsGetString(ini, section, PAT_KEY_DIR, prefsGetString(ini, PAT_SECTION_DIR, PAT_KEY_DIR, ""));
+
+ if (patchDir[0] && platformPathJoin(path, DVX_MAX_PATH, patchDir, file) && musicFileThere(path)) {
+ return true;
+ }
+
+ return platformPathJoin(midi, sizeof(midi), dir, PAT_MIDI_DIR) && platformPathJoin(path, DVX_MAX_PATH, midi, file) && musicFileThere(path);
+}
+
+
+// Each program's patch, then the drum kit (a region a key), from the
+// banks' sections (the legacy ones where the banks are missing).
+int32_t patLoadSet(const char *iniPath, WaveBankT *bank) {
+ char dir[DVX_MAX_PATH];
+ char path[DVX_MAX_PATH];
+ PatBuildT b;
+ PrefsHandleT *ini;
+ const char *melodic;
+ const char *drums;
+ int32_t rc = MUSIC_OK;
+
+ memset(bank, 0, sizeof(*bank));
+ memset(&b, 0, sizeof(b));
+
+ if (!platformPathDirName(iniPath, dir, sizeof(dir)) || !musicFileThere(iniPath)) {
+ return MUSIC_ERR_OPEN;
+ }
+
+ ini = prefsLoad(iniPath);
+
+ if (!ini) {
+ return MUSIC_ERR_OPEN;
+ }
+
+ melodic = prefsKeyName(ini, PAT_SECTION_MELODIC, 0) ? PAT_SECTION_MELODIC : PAT_LEGACY_MELODIC;
+ drums = prefsKeyName(ini, PAT_SECTION_DRUMS, 0) ? PAT_SECTION_DRUMS : PAT_LEGACY_DRUMS;
+
+ for (int32_t pass = 0; pass < 2 && rc == MUSIC_OK; pass++) {
+ bool kit = pass == 1;
+ WavePresetT p;
+
+ memset(&p, 0, sizeof(p));
+ p.first = (uint32_t)arrlen(b.regions);
+
+ for (int32_t key = 0; key <= KEY_TOP && rc == MUSIC_OK; key++) {
+ PatWaveT *waves = NULL;
+
+ if (!patFind(ini, kit ? drums : melodic, dir, key, path)) {
+ continue;
+ }
+
+ rc = patRead(path, &waves);
+
+ if (rc != MUSIC_OK) {
+ dvxLog("Music: the patch %s is damaged; left out", path);
+ rc = MUSIC_OK;
+ arrfree(waves);
+ continue;
+ }
+
+ uint16_t file = (uint16_t)arrlen(b.files);
+
+ arrsetlen(b.files, file + 1);
+ snprintf(b.files[file], DVX_MAX_PATH, "%s", path);
+
+ if (!kit) {
+ p.first = (uint32_t)arrlen(b.regions);
+ }
+
+ // Drums: the wave for the key's own pitch, at that key only;
+ // instruments: each wave over its keys, kept apart, stretched
+ // over the gaps
+ int32_t next = 0;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(waves); i++) {
+ const PatWaveT *w = &waves[i];
+ bool last = i == (int32_t)arrlen(waves) - 1;
+ int32_t lo = (keyCents(w->low) + CENTS_SEMI - 1) / CENTS_SEMI;
+ int32_t hi = keyCents(w->high) / CENTS_SEMI;
+
+ if (kit) {
+ if (last || (key >= lo && key <= hi)) {
+ patAddWave(&b, w, file, (uint8_t)key, (uint8_t)key);
+ break;
+ }
+
+ continue;
+ }
+
+ lo = next;
+ hi = last || hi > KEY_TOP ? KEY_TOP : hi;
+
+ if (hi >= lo) {
+ patAddWave(&b, w, file, (uint8_t)lo, (uint8_t)hi);
+ next = hi + 1;
+ }
+ }
+
+ arrfree(waves);
+
+ if (!kit) {
+ p.count = (uint32_t)arrlen(b.regions) - p.first;
+ p.bank = 0;
+ p.program = (uint16_t)key;
+ snprintf(p.name, sizeof(p.name), "%s", platformPathBaseName(path));
+ arrput(b.presets, p);
+ }
+ }
+
+ if (kit && arrlen(b.regions) > p.first) {
+ p.count = (uint32_t)arrlen(b.regions) - p.first;
+ p.bank = SF_DRUM_BANK;
+ p.program = 0;
+ snprintf(p.name, sizeof(p.name), "%s", PAT_KIT_NAME);
+ arrput(b.presets, p);
+ }
+ }
+
+ prefsClose(ini);
+
+ if (arrlen(b.presets) == 0) {
+ rc = rc == MUSIC_OK ? MUSIC_ERR_BANK : rc;
+ }
+
+ // The arrays into the bank (malloc'd copies, as sf2Load leaves them)
+ if (rc == MUSIC_OK) {
+ bank->presetCount = (uint32_t)arrlen(b.presets);
+ bank->regionCount = (uint32_t)arrlen(b.regions);
+ bank->sampleCount = (uint32_t)arrlen(b.samples);
+ bank->sourceCount = (uint32_t)arrlen(b.sources);
+ bank->fileCount = (uint32_t)arrlen(b.files);
+ bank->dataFrames = b.frames;
+ bank->presets = (WavePresetT *)malloc(bank->presetCount * sizeof(WavePresetT));
+ bank->regions = (WaveRegionT *)malloc(bank->regionCount * sizeof(WaveRegionT) + 1);
+ bank->samples = (WaveSampleT *)malloc(bank->sampleCount * sizeof(WaveSampleT) + 1);
+ bank->sources = (WaveSourceT *)malloc(bank->sourceCount * sizeof(WaveSourceT) + 1);
+ bank->files = (char (*)[DVX_MAX_PATH])malloc(bank->fileCount * DVX_MAX_PATH + 1);
+
+ if (!bank->presets || !bank->regions || !bank->samples || !bank->sources || !bank->files) {
+ waveBankFree(bank);
+ rc = MUSIC_ERR_NO_MEMORY;
+ } else {
+ memcpy(bank->presets, b.presets, bank->presetCount * sizeof(WavePresetT));
+ memcpy(bank->regions, b.regions, bank->regionCount * sizeof(WaveRegionT));
+ memcpy(bank->samples, b.samples, bank->sampleCount * sizeof(WaveSampleT));
+ memcpy(bank->sources, b.sources, bank->sourceCount * sizeof(WaveSourceT));
+ memcpy(bank->files, b.files, bank->fileCount * DVX_MAX_PATH);
+ snprintf(bank->path, sizeof(bank->path), "%s", iniPath);
+ }
+ }
+
+ arrfree(b.presets);
+ arrfree(b.regions);
+ arrfree(b.samples);
+ arrfree(b.sources);
+ arrfree(b.files);
+ return rc;
+}
+
+
+// A patch's waves (headers only): the patch header's mark, then each
+// wave's header, its data skipped by its own size.
+static int32_t patRead(const char *path, PatWaveT **waves) {
+ uint8_t head[PAT_HEADER_BYTES];
+ uint8_t wh[PAT_WAVE_BYTES];
+ FILE *f = fopen(path, "rb");
+ int32_t rc = MUSIC_OK;
+
+ if (!f) {
+ return MUSIC_ERR_OPEN;
+ }
+
+ if (fread(head, 1, sizeof(head), f) != sizeof(head) || memcmp(head, PAT_MAGIC, PAT_MAGIC_LEN) != 0 || fseek(f, PAT_INSTR_BYTES + PAT_LAYER_BYTES, SEEK_CUR) != 0) {
+ fclose(f);
+ return MUSIC_ERR_BANK;
+ }
+
+ uint32_t count = le16(head + PAT_WAVES_AT);
+ long at = PAT_HEADER_BYTES + PAT_INSTR_BYTES + PAT_LAYER_BYTES;
+
+ for (uint32_t i = 0; i < count && rc == MUSIC_OK; i++) {
+ PatWaveT w;
+
+ if (fread(wh, 1, sizeof(wh), f) != sizeof(wh)) {
+ rc = MUSIC_ERR_BANK;
+ break;
+ }
+
+ memset(&w, 0, sizeof(w));
+ w.dataAt = (uint32_t)(at + PAT_WAVE_BYTES);
+ w.size = le32(wh + W_SIZE);
+ w.loopStart = le32(wh + W_LOOP_START);
+ w.loopEnd = le32(wh + W_LOOP_END);
+ w.rate = (uint16_t)le16(wh + W_RATE);
+ w.low = le32(wh + W_LOW);
+ w.high = le32(wh + W_HIGH);
+ w.root = le32(wh + W_ROOT);
+ w.balance = wh[W_BALANCE] > BALANCE_MAX ? BALANCE_MAX : wh[W_BALANCE];
+ w.tremRate = wh[W_TREM_RATE];
+ w.tremDepth = wh[W_TREM_DEPTH];
+ w.vibSweep = wh[W_VIB_SWEEP];
+ w.vibRate = wh[W_VIB_RATE];
+ w.vibDepth = wh[W_VIB_DEPTH];
+ w.modes = wh[W_MODES];
+ w.scaleKey = (uint16_t)le16(wh + W_SCALE_FREQ);
+ w.scaleFactor = (uint16_t)le16(wh + W_SCALE_FACTOR);
+ memcpy(w.envRate, wh + W_ENV_RATE, ENV_POINTS);
+ memcpy(w.envLevel, wh + W_ENV_OFFSET, ENV_POINTS);
+ at += PAT_WAVE_BYTES + (long)w.size;
+
+ if (w.rate == 0 || w.size == 0 || fseek(f, at, SEEK_SET) != 0) {
+ rc = MUSIC_ERR_BANK;
+ break;
+ }
+
+ arrput(*waves, w);
+ }
+
+ fclose(f);
+ return rc;
+}
+
+
+// A GF1 volume ramp of units 12-bit steps at a rate byte (RRMMMMMM: MMMMMM
+// steps every 8^RR frames) as timecents.
+static int16_t rampTc(uint32_t units, uint8_t rate) {
+ uint32_t inc = rate & RATE_INC_MASK;
+ uint32_t frames = units;
+
+ if (inc == 0 || units == 0) {
+ return sf2GenDefault(SF_GEN_VOL_ENV_ATTACK);
+ }
+
+ for (uint32_t r = rate >> RATE_RANGE_SHIFT; r > 0; r--) {
+ frames *= RATE_RANGE_STEP;
+ }
+
+ uint64_t us = (uint64_t)frames / inc * FRAME_NS / NS_PER_US;
+
+ return (int16_t)waveRatioCents((uint32_t)(us ? us : 1), US_PER_SEC);
+}
diff --git a/src/libs/kpunch/music/musicSeq.c b/src/libs/kpunch/music/musicSeq.c
index 6f5883d7..1f835e2f 100644
--- a/src/libs/kpunch/music/musicSeq.c
+++ b/src/libs/kpunch/music/musicSeq.c
@@ -23,9 +23,10 @@
// musicSeq.c -- the sequencer, the event ring and the music timer
//
// Runs at interrupt time as well as program time (see musicInt.h): the
-// timer callback musicTick plays every MIDI music on the FM chip or the
-// MPU-401 port, MUSIC_TICK_US at a time. The software synthesizer runs
-// the same sequencer from its stream's fill function instead.
+// timer callback musicTick plays every MIDI music on the FM chip, the
+// wavetable chip, or the MPU-401 port, and every module on the wavetable
+// chip, MUSIC_TICK_US at a time. The software synthesizer runs the same
+// sequencer from its stream's fill function instead.
#include "musicInt.h"
@@ -35,7 +36,7 @@
#define RING_DATA1_SHIFT 8
#define RING_DATA2_SHIFT 16
#define RING_BYTE_MASK 0xFF
-#define LOCK_ITEMS_MAX 64
+#define LOCK_ITEMS_MAX 128
IsrPlayerT *musicIsrPlayers[MUSIC_ISR_PLAYERS_MAX];
@@ -82,10 +83,11 @@ void eventRingInit(EventRingT *ring) {
}
-// Every function of the three files that run at interrupt time
-// (musicFm.c, musicMpu.c, this one) and the player table: one span from
-// the lowest of them to the end of the table, which is the library's
-// last data (the Makefile links these files last).
+// Every function of the files that run at interrupt time (musicCents.c,
+// musicFm.c, musicModTick.c, musicMpu.c, musicWave.c, this one) and the
+// player table: one
+// span from the lowest of them to the end of the table, which is the
+// library's last data (the Makefile links these files last).
bool musicLock(bool lock) {
const void *fns[] = {
(const void *)eventRingDrain,
@@ -109,7 +111,9 @@ bool musicLock(bool lock) {
}
count += fmLockItems(&items[count]);
+ count += modHwLockItems(&items[count]);
count += mpuLockItems(&items[count]);
+ count += waveLockItems(&items[count]);
items[count].addr = musicIsrPlayers;
items[count].size = sizeof(musicIsrPlayers);
count++;
@@ -118,8 +122,10 @@ bool musicLock(bool lock) {
// A player that plays to its end is stopped and silenced here; the end
-// call follows in the idle time (musicIdle). The MPU-401 queue drains
-// even while stopped (its closing messages).
+// call follows in the idle time (musicIdle). A module's frames play on
+// the wavetable synthesizer (musicModTick.c). The MPU-401 queue drains
+// even while stopped (its closing messages), and wavetable notes play
+// out their releases.
void musicTick(void *unused) {
(void)unused;
@@ -130,7 +136,12 @@ void musicTick(void *unused) {
continue;
}
- if (p->state == PLAY_PLAYING && !seqAdvance(&p->seq, MUSIC_TICK_US, p->sink)) {
+ if (p->mod) {
+ if (p->state == PLAY_PLAYING && !modHwTick(p->mod, MUSIC_TICK_US)) {
+ p->state = PLAY_STOPPED;
+ p->ended = true;
+ }
+ } else if (p->state == PLAY_PLAYING && !seqAdvance(&p->seq, MUSIC_TICK_US, p->sink)) {
p->state = PLAY_STOPPED;
p->sink->silence(p->sink);
p->ended = true;
@@ -139,6 +150,10 @@ void musicTick(void *unused) {
if (p->mpu) {
mpuSinkDrain(p->mpu, MUSIC_TICK_US);
}
+
+ if (p->wave) {
+ waveSynthTick(p->wave, MUSIC_TICK_US);
+ }
}
}
diff --git a/src/libs/kpunch/music/musicSf2.c b/src/libs/kpunch/music/musicSf2.c
new file mode 100644
index 00000000..90e9def7
--- /dev/null
+++ b/src/libs/kpunch/music/musicSf2.c
@@ -0,0 +1,994 @@
+// 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.
+
+// musicSf2.c -- SoundFont 2 banks (see musicInt.h, "SoundFont banks")
+//
+// A SoundFont is a RIFF "sfbk" file: LIST INFO (names, the version),
+// LIST sdta (smpl: every sample's 16-bit data in one run), and LIST pdta,
+// the "hydra" of nine record arrays: presets (phdr) with their zones
+// (pbag) and each zone's generators (pgen) and modulators (pmod),
+// instruments (inst, ibag, igen, imod) the same, and sample headers
+// (shdr). Each array ends with a terminal record.
+//
+// A zone is a list of generators. An instrument zone ends with the
+// sample it plays (sampleID); one without, if it is the first, is the
+// instrument's global zone, whose values every other zone starts from.
+// A preset zone ends with an instrument; its global zone works the same.
+// Instrument values are absolute (over the specification's defaults);
+// preset values are added to them, except the key and velocity ranges,
+// which intersect, and the generators a preset may not set. Each
+// preset zone x instrument zone pair whose ranges meet is one region,
+// every generator resolved and clamped to its legal range, so a note
+// needs only its regions. Modulators are not read: the renderer applies
+// the specification's default ones (velocity, volume, expression, pan,
+// pitch bend, modulation wheel).
+//
+// The sample data is not read here: a song reads what it uses
+// (waveBankRead), from the file or the memory the bank came from.
+//
+// SoundFont 1 (.SBK, the AWE32's own banks, such as SYNTHGM.SBK for its
+// General MIDI ROM) is the same file but for version 1 in ifil, 16-byte
+// sample headers (start, end, loop start, loop end: no name, rate, or
+// key; 44,100 Hz and middle C), the ROM samples first (their addresses
+// the ROM's) until the first that starts at 0 (the file's own data on),
+// loops kept one and two frames lower, and generators in the chip's own
+// units, which sf1Value makes SoundFont 2's. A preset's value there is
+// not added: it counts only where the instrument zone sets nothing. The
+// specification is not public; this follows what is known of the files.
+
+#include "musicInt.h"
+
+#include
+#include
+#include
+
+#define RIFF_HEADER_BYTES 8 // id, length
+#define RIFF_TYPE_BYTES 4
+#define RIFF_ID_BYTES 4
+#define RIFF_ALIGN_MASK 1u // chunks are padded to even lengths
+
+// Record sizes of the hydra's arrays
+#define PHDR_BYTES 38
+#define BAG_BYTES 4
+#define MOD_BYTES 10
+#define GEN_BYTES 4
+#define INST_BYTES 22
+#define SHDR_BYTES 46
+#define HYDRA_ARRAYS 9
+
+// Field offsets
+#define PHDR_PRESET 20
+#define PHDR_BANK 22
+#define PHDR_BAG 24
+#define INST_BAG 20
+#define BAG_GEN 0
+#define GEN_OPER 0
+#define GEN_AMOUNT 2
+#define SHDR_START 20
+#define SHDR_END 24
+#define SHDR_LOOP_START 28
+#define SHDR_LOOP_END 32
+#define SHDR_RATE 36
+#define SHDR_PITCH 40
+#define SHDR_CORRECTION 41
+#define SHDR_TYPE 44
+#define SHDR1_START 0 // SoundFont 1's sample header
+#define SHDR1_END 4
+#define SHDR1_LOOP_START 8
+#define SHDR1_LOOP_END 12
+
+#define SF_VERSION_1 1
+#define SF_VERSION_2 2
+#define IFIL_BYTES 4
+#define RANGE_FULL 0x7F00 // keyRange / velRange 0..127 (low byte low, high byte high)
+#define RANGE_HIGH_SHIFT 8
+#define BYTE_MASK 0xFF
+#define MIDI_TOP 127
+#define SAMPLE_BYTES 2
+#define BYTE_BITS 8
+#define SIGN_FLIP 0x8000 // unsigned to signed 16-bit
+#define UNPITCHED_ROOT 60 // an unpitched sample (original pitch 255) plays as middle C
+
+// SoundFont 1
+#define SHDR1_BYTES 16
+#define SF1_RATE 44100
+#define SF1_ROOT 60
+#define SF1_LOOP_START_ADD 1 // its loop start is kept one frame lower
+#define SF1_LOOP_END_ADD 2 // and its end two
+#define SF1_GEN_SAMPLE_PITCH 55 // key x 100 + cents the sample sounds at
+#define SF1_CENTS_KEY 100
+#define SF1_FINE_ROUND (-50) // a fine tune this low rounds the root up
+#define SF1_SCALE_HALF 50 // scaleTuning 1: 50 cents a key
+#define SF1_SCALE_FULL 100
+#define SF1_TC_NONE (-12000) // a time of 0 ms
+#define SF1_MS_PER_SEC 1000
+#define SF1_SUSTAIN_FULL 96 // sustain values from here on: no fall
+#define SF1_VOL_SUSTAIN_CB 1000 // sustainVolEnv 0: 100 dB down ...
+#define SF1_VOL_SUSTAIN_NUM 21 // ... and 10.5 cB less a step
+#define SF1_VOL_SUSTAIN_DEN 2
+#define SF1_MOD_SUSTAIN_MAX 1000 // sustainModEnv: tenths of a percent down
+#define SF1_KEY_TIME_NUM 555 // keynumTo*: 5.55 timecents a key a step
+#define SF1_KEY_TIME_DEN 100
+#define SF1_LEVEL_MAX 127 // pan, attenuation, filter cutoff: 0..127
+#define SF1_PAN_SPAN 1000
+#define SF1_PAN_HALF 500
+#define SF1_ATTEN_SILENT 1440
+#define SF1_ATTEN_NUM 10 // 200 log10(x) cB is 1200 log2(x) cents x 10 / 199
+#define SF1_ATTEN_DEN 199
+#define SF1_SEND_MAX 255 // chorus, reverb: 0..255
+#define SF1_SEND_SPAN 1000
+#define SF1_FC_OPEN 14400
+#define SF1_FC_STEP 59 // absolute cents a step ...
+#define SF1_FC_BASE 4366 // ... from here
+#define SF1_Q_NUM 3 // filter Q: 1.5 cB a step
+#define SF1_Q_DEN 2
+#define SF1_LFO_OFFSET 7925 // LFO rates: 1200 log2(v) less this
+#define SF1_MOD_LFO_NONE (-725) // the LFOs' rates at 0 (and when not set)
+#define SF1_VIB_LFO_NONE (-15600)
+#define SF1_DEPTH_STEPS 64 // LFO and envelope depths: the chip's steps
+#define SF1_PITCH_SPAN 1200 // to pitch: (1200 v / 64 + 1) / 2 cents
+#define SF1_PITCH_ROUND 1
+#define SF1_PITCH_DEN 2
+#define SF1_LFO_FILTER_SPAN 3600
+#define SF1_ENV_FILTER_SPAN 7200
+#define SF1_VOLUME_SPAN 120
+
+// A generator's legal range, and whether a preset's value is added
+typedef struct {
+ int16_t lo;
+ int16_t hi;
+ int16_t def;
+ bool preset; // a preset may set it (and its value is added)
+} GenRuleT;
+
+// Where the file is read from
+typedef struct {
+ const uint8_t *data; // the whole file in memory, or NULL
+ uint32_t size;
+ FILE *file;
+} SourceT;
+
+// The hydra's arrays, located in the pdta data
+typedef struct {
+ const uint8_t *at[HYDRA_ARRAYS];
+ uint32_t count[HYDRA_ARRAYS]; // records, the terminal one included
+ bool sf1; // SoundFont 1
+} HydraT;
+
+// A zone's generators as applyGens leaves them: values and which it set
+typedef struct {
+ int16_t gen[SF_GEN_COUNT];
+ uint8_t set[SF_GEN_COUNT];
+} ZoneT;
+
+// The hydra's arrays in their file order
+enum { H_PHDR, H_PBAG, H_PMOD, H_PGEN, H_INST, H_IBAG, H_IMOD, H_IGEN, H_SHDR };
+
+static const char sHydraIds[HYDRA_ARRAYS][RIFF_ID_BYTES + 1] = { "phdr", "pbag", "pmod", "pgen", "inst", "ibag", "imod", "igen", "shdr" };
+static const uint32_t sHydraBytes[HYDRA_ARRAYS] = { PHDR_BYTES, BAG_BYTES, MOD_BYTES, GEN_BYTES, INST_BYTES, BAG_BYTES, MOD_BYTES, GEN_BYTES, SHDR_BYTES };
+
+// Ranges and defaults from the SoundFont 2.01 specification (section 8.1)
+static const GenRuleT sGenRules[SF_GEN_COUNT] = {
+ [SF_GEN_START_OFFSET] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_END_OFFSET] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_LOOP_START_OFFSET] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_LOOP_END_OFFSET] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_START_COARSE] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_MOD_LFO_TO_PITCH] = { -12000, 12000, 0, true },
+ [SF_GEN_VIB_LFO_TO_PITCH] = { -12000, 12000, 0, true },
+ [SF_GEN_MOD_ENV_TO_PITCH] = { -12000, 12000, 0, true },
+ [SF_GEN_FILTER_FC] = { 1500, 13500, 13500, true },
+ [SF_GEN_FILTER_Q] = { 0, 960, 0, true },
+ [SF_GEN_MOD_LFO_TO_FILTER] = { -12000, 12000, 0, true },
+ [SF_GEN_MOD_ENV_TO_FILTER] = { -12000, 12000, 0, true },
+ [SF_GEN_END_COARSE] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_MOD_LFO_TO_VOLUME] = { -960, 960, 0, true },
+ [14] = { 0, 0, 0, false },
+ [SF_GEN_CHORUS_SEND] = { 0, 1000, 0, true },
+ [SF_GEN_REVERB_SEND] = { 0, 1000, 0, true },
+ [SF_GEN_PAN] = { -500, 500, 0, true },
+ [18] = { 0, 0, 0, false },
+ [19] = { 0, 0, 0, false },
+ [20] = { 0, 0, 0, false },
+ [SF_GEN_MOD_LFO_DELAY] = { -12000, 5000, -12000, true },
+ [SF_GEN_MOD_LFO_FREQ] = { -16000, 4500, 0, true },
+ [SF_GEN_VIB_LFO_DELAY] = { -12000, 5000, -12000, true },
+ [SF_GEN_VIB_LFO_FREQ] = { -16000, 4500, 0, true },
+ [SF_GEN_MOD_ENV_DELAY] = { -12000, 5000, -12000, true },
+ [SF_GEN_MOD_ENV_ATTACK] = { -12000, 8000, -12000, true },
+ [SF_GEN_MOD_ENV_HOLD] = { -12000, 5000, -12000, true },
+ [SF_GEN_MOD_ENV_DECAY] = { -12000, 8000, -12000, true },
+ [SF_GEN_MOD_ENV_SUSTAIN] = { 0, 1000, 0, true },
+ [SF_GEN_MOD_ENV_RELEASE] = { -12000, 8000, -12000, true },
+ [SF_GEN_KEY_TO_MOD_ENV_HOLD] = { -1200, 1200, 0, true },
+ [SF_GEN_KEY_TO_MOD_ENV_DECAY] = { -1200, 1200, 0, true },
+ [SF_GEN_VOL_ENV_DELAY] = { -12000, 5000, -12000, true },
+ [SF_GEN_VOL_ENV_ATTACK] = { -12000, 8000, -12000, true },
+ [SF_GEN_VOL_ENV_HOLD] = { -12000, 5000, -12000, true },
+ [SF_GEN_VOL_ENV_DECAY] = { -12000, 8000, -12000, true },
+ [SF_GEN_VOL_ENV_SUSTAIN] = { 0, 1440, 0, true },
+ [SF_GEN_VOL_ENV_RELEASE] = { -12000, 8000, -12000, true },
+ [SF_GEN_KEY_TO_VOL_ENV_HOLD] = { -1200, 1200, 0, true },
+ [SF_GEN_KEY_TO_VOL_ENV_DECAY] = { -1200, 1200, 0, true },
+ [SF_GEN_INSTRUMENT] = { INT16_MIN, INT16_MAX, 0, false },
+ [42] = { 0, 0, 0, false },
+ [SF_GEN_KEY_RANGE] = { INT16_MIN, INT16_MAX, RANGE_FULL, false },
+ [SF_GEN_VEL_RANGE] = { INT16_MIN, INT16_MAX, RANGE_FULL, false },
+ [SF_GEN_LOOP_START_COARSE] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_KEYNUM] = { SF_ROOT_NONE, MIDI_TOP, SF_ROOT_NONE, false },
+ [SF_GEN_VELOCITY] = { SF_ROOT_NONE, MIDI_TOP, SF_ROOT_NONE, false },
+ [SF_GEN_ATTENUATION] = { 0, 1440, 0, true },
+ [49] = { 0, 0, 0, false },
+ [SF_GEN_LOOP_END_COARSE] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_COARSE_TUNE] = { -120, 120, 0, true },
+ [SF_GEN_FINE_TUNE] = { -99, 99, 0, true },
+ [SF_GEN_SAMPLE_ID] = { INT16_MIN, INT16_MAX, 0, false },
+ [SF_GEN_SAMPLE_MODES] = { INT16_MIN, INT16_MAX, SF_LOOP_NONE, false },
+ [55] = { 0, 0, 0, false },
+ [SF_GEN_SCALE_TUNING] = { 0, 1200, 100, true },
+ [SF_GEN_EXCLUSIVE_CLASS] = { 0, MIDI_TOP, 0, false },
+ [SF_GEN_ROOT_KEY] = { SF_ROOT_NONE, MIDI_TOP, SF_ROOT_NONE, false },
+ [59] = { 0, 0, 0, false },
+ [60] = { 0, 0, 0, false }
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void applyGens(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, ZoneT *zone);
+static bool bagRange(const HydraT *h, int32_t bagArray, uint32_t bag, uint32_t *first, uint32_t *last);
+static int32_t buildRegions(const HydraT *h, WaveBankT *bank);
+static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h);
+static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_t inst, const ZoneT *preset, uint32_t *count, WaveRegionT **regions);
+static uint32_t le16(const uint8_t *p);
+static uint32_t le32(const uint8_t *p);
+static int32_t readHydra(SourceT *src, uint32_t offset, uint32_t size, bool sf1, WaveBankT *bank);
+static int32_t readSamples(const HydraT *h, WaveBankT *bank);
+static void regionGens(bool sf1, const ZoneT *inst, const ZoneT *preset, int16_t *out);
+static int32_t sf1Default(int32_t gen);
+static const ZoneT *sf1From(const ZoneT *inst, const ZoneT *preset, int32_t gen);
+static void sf1Tuning(const ZoneT *inst, const ZoneT *preset, int16_t *out);
+static int32_t sf1Value(int32_t gen, int32_t v);
+static int32_t sf2From(SourceT *src, WaveBankT *bank);
+int16_t sf2GenDefault(int32_t gen);
+int32_t sf2Load(const char *path, WaveBankT *bank);
+int32_t sf2Parse(const uint8_t *data, uint32_t size, WaveBankT *bank);
+static bool sourcesRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out);
+static bool srcRead(SourceT *src, uint32_t offset, void *out, uint32_t len);
+void waveBankFree(WaveBankT *bank);
+bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out);
+static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, uint32_t oper);
+
+
+// The generators first .. last - 1 of an array set into zone (ranges and
+// all), as a zone's own values replace what it started from.
+static void applyGens(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, ZoneT *zone) {
+ for (uint32_t g = first; g < last; g++) {
+ const uint8_t *rec = h->at[genArray] + g * GEN_BYTES;
+ uint32_t oper = le16(rec + GEN_OPER);
+
+ if (oper < SF_GEN_COUNT) {
+ zone->gen[oper] = (int16_t)le16(rec + GEN_AMOUNT);
+ zone->set[oper] = 1;
+ }
+ }
+}
+
+
+// The generators of zone bag (bagArray: H_PBAG or H_IBAG) are first ..
+// last - 1; false when they run past the generator array.
+static bool bagRange(const HydraT *h, int32_t bagArray, uint32_t bag, uint32_t *first, uint32_t *last) {
+ int32_t genArray = bagArray == H_PBAG ? H_PGEN : H_IGEN;
+
+ *first = le16(h->at[bagArray] + bag * BAG_BYTES + BAG_GEN);
+ *last = le16(h->at[bagArray] + (bag + 1) * BAG_BYTES + BAG_GEN);
+ return *first <= *last && *last <= h->count[genArray];
+}
+
+
+// Every preset's regions, in preset order.
+static int32_t buildRegions(const HydraT *h, WaveBankT *bank) {
+ uint32_t presets = h->count[H_PHDR] - 1;
+
+ bank->presets = (WavePresetT *)calloc(presets ? presets : 1, sizeof(WavePresetT));
+
+ if (!bank->presets) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ for (uint32_t p = 0; p < presets; p++) {
+ const uint8_t *rec = h->at[H_PHDR] + p * PHDR_BYTES;
+ uint32_t bagFirst = le16(rec + PHDR_BAG);
+ uint32_t bagLast = le16(rec + PHDR_BYTES + PHDR_BAG);
+ WavePresetT *out = &bank->presets[p];
+ ZoneT global;
+
+ memcpy(out->name, rec, SF_NAME_LEN);
+ out->name[SF_NAME_LEN] = '\0';
+ out->program = (uint16_t)le16(rec + PHDR_PRESET);
+ out->bank = (uint16_t)le16(rec + PHDR_BANK);
+ out->first = bank->regionCount;
+
+ if (bagFirst > bagLast || bagLast >= h->count[H_PBAG]) {
+ return MUSIC_ERR_BANK;
+ }
+
+ // A preset's own values start at nothing to add, its ranges full
+ memset(&global, 0, sizeof(global));
+ global.gen[SF_GEN_KEY_RANGE] = RANGE_FULL;
+ global.gen[SF_GEN_VEL_RANGE] = RANGE_FULL;
+
+ for (uint32_t b = bagFirst; b < bagLast; b++) {
+ uint32_t first;
+ uint32_t last;
+ ZoneT zone;
+
+ if (!bagRange(h, H_PBAG, b, &first, &last)) {
+ return MUSIC_ERR_BANK;
+ }
+
+ if (!zoneHas(h, H_PGEN, first, last, SF_GEN_INSTRUMENT)) {
+ if (b == bagFirst) {
+ applyGens(h, H_PGEN, first, last, &global);
+ }
+
+ continue;
+ }
+
+ zone = global;
+ applyGens(h, H_PGEN, first, last, &zone);
+
+ uint32_t inst = (uint16_t)zone.gen[SF_GEN_INSTRUMENT];
+
+ if (inst + 1 >= h->count[H_INST]) {
+ return MUSIC_ERR_BANK;
+ }
+
+ int32_t rc = instrumentRegions(h, bank, inst, &zone, &bank->regionCount, &bank->regions);
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+ }
+
+ out->count = bank->regionCount - out->first;
+ }
+
+ bank->presetCount = presets;
+ return MUSIC_OK;
+}
+
+
+// The pdta list's nine arrays, each a whole number of records with its
+// terminal one (the modulators may be missing: they are not read).
+static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h) {
+ memset(h, 0, sizeof(*h));
+ h->sf1 = sf1;
+
+ for (uint32_t at = 0; at + RIFF_HEADER_BYTES <= size;) {
+ uint32_t len = le32(pdta + at + RIFF_ID_BYTES);
+ uint32_t body = at + RIFF_HEADER_BYTES;
+
+ if (len > size - body) {
+ return false;
+ }
+
+ for (int32_t i = 0; i < HYDRA_ARRAYS; i++) {
+ uint32_t rec = i == H_SHDR && sf1 ? SHDR1_BYTES : sHydraBytes[i];
+
+ if (memcmp(pdta + at, sHydraIds[i], RIFF_ID_BYTES) == 0) {
+ if (len % rec != 0 || len < rec) {
+ return false;
+ }
+
+ h->at[i] = pdta + body;
+ h->count[i] = len / rec;
+ }
+ }
+
+ at = body + len + (len & RIFF_ALIGN_MASK);
+ }
+
+ for (int32_t i = 0; i < HYDRA_ARRAYS; i++) {
+ if (!h->at[i] && i != H_PMOD && i != H_IMOD) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+// The regions of instrument inst under preset zone preset (its values,
+// its ranges), appended to *regions.
+static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_t inst, const ZoneT *preset, uint32_t *count, WaveRegionT **regions) {
+ const uint8_t *rec = h->at[H_INST] + inst * INST_BYTES;
+ uint32_t bagFirst = le16(rec + INST_BAG);
+ uint32_t bagLast = le16(rec + INST_BYTES + INST_BAG);
+ ZoneT global;
+
+ if (bagFirst > bagLast || bagLast >= h->count[H_IBAG]) {
+ return MUSIC_ERR_BANK;
+ }
+
+ memset(&global, 0, sizeof(global));
+
+ for (int32_t g = 0; g < SF_GEN_COUNT; g++) {
+ global.gen[g] = sGenRules[g].def;
+ }
+
+ for (uint32_t b = bagFirst; b < bagLast; b++) {
+ uint32_t first;
+ uint32_t last;
+ ZoneT zone;
+
+ if (!bagRange(h, H_IBAG, b, &first, &last)) {
+ return MUSIC_ERR_BANK;
+ }
+
+ if (!zoneHas(h, H_IGEN, first, last, SF_GEN_SAMPLE_ID)) {
+ if (b == bagFirst) {
+ applyGens(h, H_IGEN, first, last, &global);
+ }
+
+ continue;
+ }
+
+ zone = global;
+ applyGens(h, H_IGEN, first, last, &zone);
+
+ uint32_t sample = (uint16_t)zone.gen[SF_GEN_SAMPLE_ID];
+ uint32_t keyLo = (uint32_t)zone.gen[SF_GEN_KEY_RANGE] & BYTE_MASK;
+ uint32_t keyHi = ((uint32_t)zone.gen[SF_GEN_KEY_RANGE] >> RANGE_HIGH_SHIFT) & BYTE_MASK;
+ uint32_t velLo = (uint32_t)zone.gen[SF_GEN_VEL_RANGE] & BYTE_MASK;
+ uint32_t velHi = ((uint32_t)zone.gen[SF_GEN_VEL_RANGE] >> RANGE_HIGH_SHIFT) & BYTE_MASK;
+ uint32_t pKeyLo = (uint32_t)preset->gen[SF_GEN_KEY_RANGE] & BYTE_MASK;
+ uint32_t pKeyHi = ((uint32_t)preset->gen[SF_GEN_KEY_RANGE] >> RANGE_HIGH_SHIFT) & BYTE_MASK;
+ uint32_t pVelLo = (uint32_t)preset->gen[SF_GEN_VEL_RANGE] & BYTE_MASK;
+ uint32_t pVelHi = ((uint32_t)preset->gen[SF_GEN_VEL_RANGE] >> RANGE_HIGH_SHIFT) & BYTE_MASK;
+
+ if (sample >= bank->sampleCount) {
+ return MUSIC_ERR_BANK;
+ }
+
+ keyLo = keyLo > pKeyLo ? keyLo : pKeyLo;
+ keyHi = keyHi < pKeyHi ? keyHi : pKeyHi;
+ velLo = velLo > pVelLo ? velLo : pVelLo;
+ velHi = velHi < pVelHi ? velHi : pVelHi;
+
+ if (keyLo > keyHi || velLo > velHi || keyHi > MIDI_TOP || velHi > MIDI_TOP) {
+ continue;
+ }
+
+ WaveRegionT *grown = (WaveRegionT *)realloc(*regions, (*count + 1) * sizeof(WaveRegionT));
+
+ if (!grown) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ *regions = grown;
+
+ WaveRegionT *r = &grown[(*count)++];
+
+ regionGens(h->sf1, &zone, preset, r->gen);
+ r->sample = (uint16_t)sample;
+ r->keyLo = (uint8_t)keyLo;
+ r->keyHi = (uint8_t)keyHi;
+ r->velLo = (uint8_t)velLo;
+ r->velHi = (uint8_t)velHi;
+ }
+
+ return MUSIC_OK;
+}
+
+
+static uint32_t le16(const uint8_t *p) {
+ return (uint32_t)p[0] | ((uint32_t)p[1] << 8);
+}
+
+
+static uint32_t le32(const uint8_t *p) {
+ return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
+}
+
+
+// The pdta list read and made into the bank.
+static int32_t readHydra(SourceT *src, uint32_t offset, uint32_t size, bool sf1, WaveBankT *bank) {
+ uint8_t *pdta = (uint8_t *)malloc(size ? size : 1);
+ HydraT h;
+ int32_t rc;
+
+ if (!pdta) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ if (!srcRead(src, offset, pdta, size)) {
+ free(pdta);
+ return MUSIC_ERR_BANK;
+ }
+
+ rc = findHydra(pdta, size, sf1, &h) ? readSamples(&h, bank) : MUSIC_ERR_BANK;
+ rc = rc == MUSIC_OK ? buildRegions(&h, bank) : rc;
+ free(pdta);
+ return rc;
+}
+
+
+// The sample headers, checked against the sample data: a ROM sample is
+// kept as it is (its addresses are the synthesizer's); a loop outside
+// its sample is moved inside it. SoundFont 1's: see the top.
+static int32_t readSamples(const HydraT *h, WaveBankT *bank) {
+ uint32_t count = h->count[H_SHDR] - 1;
+ bool rom = h->sf1;
+
+ bank->samples = (WaveSampleT *)calloc(count ? count : 1, sizeof(WaveSampleT));
+
+ if (!bank->samples) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ WaveSampleT *s = &bank->samples[i];
+
+ if (h->sf1) {
+ const uint8_t *rec = h->at[H_SHDR] + i * SHDR1_BYTES;
+
+ rom = rom && le32(rec + SHDR1_START) != 0;
+ s->start = le32(rec + SHDR1_START);
+ s->end = le32(rec + SHDR1_END);
+ s->loopStart = le32(rec + SHDR1_LOOP_START) + SF1_LOOP_START_ADD;
+ s->loopEnd = le32(rec + SHDR1_LOOP_END) + SF1_LOOP_END_ADD;
+ s->rate = SF1_RATE;
+ s->rootKey = SF1_ROOT;
+ s->correction = 0;
+ s->type = (uint16_t)(rom ? SF_SAMPLE_ROM | SF_SAMPLE_MONO : SF_SAMPLE_MONO);
+ } else {
+ const uint8_t *rec = h->at[H_SHDR] + i * SHDR_BYTES;
+
+ s->start = le32(rec + SHDR_START);
+ s->end = le32(rec + SHDR_END);
+ s->loopStart = le32(rec + SHDR_LOOP_START);
+ s->loopEnd = le32(rec + SHDR_LOOP_END);
+ s->rate = le32(rec + SHDR_RATE);
+ s->rootKey = rec[SHDR_PITCH] > MIDI_TOP ? UNPITCHED_ROOT : rec[SHDR_PITCH];
+ s->correction = (int8_t)rec[SHDR_CORRECTION];
+ s->type = (uint16_t)le16(rec + SHDR_TYPE);
+ }
+
+ if (!(s->type & SF_SAMPLE_ROM) && (s->start > s->end || s->end > bank->dataFrames)) {
+ return MUSIC_ERR_BANK;
+ }
+
+ if (s->loopStart < s->start || s->loopEnd > s->end || s->loopStart >= s->loopEnd) {
+ s->loopStart = s->start;
+ s->loopEnd = s->end;
+ }
+ }
+
+ bank->sampleCount = count;
+ return MUSIC_OK;
+}
+
+
+// A region's generators from its instrument zone and preset zone (see
+// the top), each clamped to its legal range.
+static void regionGens(bool sf1, const ZoneT *inst, const ZoneT *preset, int16_t *out) {
+ for (int32_t g = 0; g < SF_GEN_COUNT; g++) {
+ int32_t v;
+
+ if (sf1) {
+ const ZoneT *from = sf1From(inst, preset, g);
+
+ v = from ? sf1Value(g, from->gen[g]) : sf1Default(g);
+ } else {
+ v = inst->gen[g] + (sGenRules[g].preset ? preset->gen[g] : 0);
+ }
+
+ v = v < sGenRules[g].lo ? sGenRules[g].lo : v;
+ v = v > sGenRules[g].hi ? sGenRules[g].hi : v;
+ out[g] = (int16_t)v;
+ }
+
+ if (sf1) {
+ sf1Tuning(inst, preset, out);
+ }
+}
+
+
+// A SoundFont 1 generator no zone sets: the specification's default, but
+// for the sustains and the LFO rates, which are what 0 is.
+static int32_t sf1Default(int32_t gen) {
+ switch (gen) {
+ case SF_GEN_VOL_ENV_SUSTAIN:
+ case SF_GEN_MOD_ENV_SUSTAIN:
+ case SF_GEN_MOD_LFO_FREQ:
+ case SF_GEN_VIB_LFO_FREQ:
+ return sf1Value(gen, 0);
+
+ default:
+ return sGenRules[gen].def;
+ }
+}
+
+
+// The zone a SoundFont 1 generator's value comes from: the instrument's
+// when it sets it, else the preset's (where a preset may set it); NULL
+// for neither.
+static const ZoneT *sf1From(const ZoneT *inst, const ZoneT *preset, int32_t gen) {
+ if (inst->set[gen]) {
+ return inst;
+ }
+
+ return sGenRules[gen].preset && preset->set[gen] ? preset : NULL;
+}
+
+
+// SoundFont 1's tuning: the coarse tune (keys) and fine tune scaled by the
+// scale tuning (0: 100 cents a key, 1: 50), and the sample's own pitch
+// (its root key, the cents off it added to the fine tune), made SoundFont
+// 2's semitones and cents.
+static void sf1Tuning(const ZoneT *inst, const ZoneT *preset, int16_t *out) {
+ const ZoneT *sc = sf1From(inst, preset, SF_GEN_SCALE_TUNING);
+ const ZoneT *co = sf1From(inst, preset, SF_GEN_COARSE_TUNE);
+ const ZoneT *fi = sf1From(inst, preset, SF_GEN_FINE_TUNE);
+ const ZoneT *sp = sf1From(inst, preset, SF1_GEN_SAMPLE_PITCH);
+ int32_t scale = sc && sc->gen[SF_GEN_SCALE_TUNING] ? SF1_SCALE_HALF : SF1_SCALE_FULL;
+ int32_t tune = (co ? co->gen[SF_GEN_COARSE_TUNE] * scale : 0) + (fi ? fi->gen[SF_GEN_FINE_TUNE] * scale / SF1_CENTS_KEY : 0);
+
+ if (sp) {
+ int32_t root = sp->gen[SF1_GEN_SAMPLE_PITCH] / SF1_CENTS_KEY;
+ int32_t fine = -(sp->gen[SF1_GEN_SAMPLE_PITCH] % SF1_CENTS_KEY);
+
+ if (fine <= SF1_FINE_ROUND) {
+ root++;
+ fine += SF1_CENTS_KEY;
+ }
+
+ tune += fine;
+
+ if (out[SF_GEN_ROOT_KEY] == SF_ROOT_NONE && root >= 0 && root <= MIDI_TOP) {
+ out[SF_GEN_ROOT_KEY] = (int16_t)root;
+ }
+ }
+
+ out[SF_GEN_SCALE_TUNING] = (int16_t)scale;
+ out[SF_GEN_COARSE_TUNE] = (int16_t)(tune / SF1_CENTS_KEY);
+ out[SF_GEN_FINE_TUNE] = (int16_t)(tune % SF1_CENTS_KEY);
+}
+
+
+// A SoundFont 1 generator's value v (the chip's units) in SoundFont 2's.
+static int32_t sf1Value(int32_t gen, int32_t v) {
+ switch (gen) {
+ case SF_GEN_MOD_LFO_DELAY:
+ case SF_GEN_VIB_LFO_DELAY:
+ case SF_GEN_MOD_ENV_DELAY:
+ case SF_GEN_MOD_ENV_ATTACK:
+ case SF_GEN_MOD_ENV_HOLD:
+ case SF_GEN_MOD_ENV_DECAY:
+ case SF_GEN_MOD_ENV_RELEASE:
+ case SF_GEN_VOL_ENV_DELAY:
+ case SF_GEN_VOL_ENV_ATTACK:
+ case SF_GEN_VOL_ENV_HOLD:
+ case SF_GEN_VOL_ENV_DECAY:
+ case SF_GEN_VOL_ENV_RELEASE:
+ // Milliseconds
+ return (uint16_t)v ? waveRatioCents((uint16_t)v, SF1_MS_PER_SEC) : SF1_TC_NONE;
+
+ case SF_GEN_VOL_ENV_SUSTAIN:
+ return v >= SF1_SUSTAIN_FULL ? 0 : SF1_VOL_SUSTAIN_CB - v * SF1_VOL_SUSTAIN_NUM / SF1_VOL_SUSTAIN_DEN;
+
+ case SF_GEN_MOD_ENV_SUSTAIN:
+ return v >= SF1_SUSTAIN_FULL ? 0 : SF1_MOD_SUSTAIN_MAX * (SF1_SUSTAIN_FULL - v) / SF1_SUSTAIN_FULL;
+
+ case SF_GEN_KEY_TO_MOD_ENV_HOLD:
+ case SF_GEN_KEY_TO_MOD_ENV_DECAY:
+ case SF_GEN_KEY_TO_VOL_ENV_HOLD:
+ case SF_GEN_KEY_TO_VOL_ENV_DECAY:
+ return v * SF1_KEY_TIME_NUM / SF1_KEY_TIME_DEN;
+
+ case SF_GEN_PAN:
+ return v * SF1_PAN_SPAN / SF1_LEVEL_MAX - SF1_PAN_HALF;
+
+ case SF_GEN_ATTENUATION:
+ // 0..127 of the full level
+ if (v >= SF1_LEVEL_MAX) {
+ return 0;
+ }
+
+ return v > 0 ? -waveRatioCents((uint32_t)v, SF1_LEVEL_MAX) * SF1_ATTEN_NUM / SF1_ATTEN_DEN : SF1_ATTEN_SILENT;
+
+ case SF_GEN_CHORUS_SEND:
+ case SF_GEN_REVERB_SEND:
+ return v * SF1_SEND_SPAN / SF1_SEND_MAX;
+
+ case SF_GEN_FILTER_FC:
+ return v >= SF1_LEVEL_MAX ? SF1_FC_OPEN : SF1_FC_STEP * v + SF1_FC_BASE;
+
+ case SF_GEN_FILTER_Q:
+ return v * SF1_Q_NUM / SF1_Q_DEN;
+
+ case SF_GEN_MOD_LFO_FREQ:
+ return v > 0 ? waveRatioCents((uint32_t)v, 1) - SF1_LFO_OFFSET : SF1_MOD_LFO_NONE;
+
+ case SF_GEN_VIB_LFO_FREQ:
+ return v > 0 ? waveRatioCents((uint32_t)v, 1) - SF1_LFO_OFFSET : SF1_VIB_LFO_NONE;
+
+ case SF_GEN_MOD_LFO_TO_PITCH:
+ case SF_GEN_VIB_LFO_TO_PITCH:
+ case SF_GEN_MOD_ENV_TO_PITCH:
+ return (SF1_PITCH_SPAN * v / SF1_DEPTH_STEPS + SF1_PITCH_ROUND) / SF1_PITCH_DEN;
+
+ case SF_GEN_MOD_LFO_TO_FILTER:
+ return SF1_LFO_FILTER_SPAN * v / SF1_DEPTH_STEPS;
+
+ case SF_GEN_MOD_ENV_TO_FILTER:
+ return SF1_ENV_FILTER_SPAN * v / SF1_DEPTH_STEPS;
+
+ case SF_GEN_MOD_LFO_TO_VOLUME:
+ return SF1_VOLUME_SPAN * v / SF1_DEPTH_STEPS;
+
+ default:
+ return v;
+ }
+}
+
+
+// The file's top level: "RIFF" length "sfbk", the INFO list (the version
+// must be 1 or 2), sdta (where smpl is), pdta (read).
+static int32_t sf2From(SourceT *src, WaveBankT *bank) {
+ uint8_t head[RIFF_HEADER_BYTES + RIFF_TYPE_BYTES];
+ uint32_t pdtaAt = 0;
+ uint32_t pdtaLen = 0;
+ uint32_t version = 0;
+
+ if (!srcRead(src, 0, head, sizeof(head)) || memcmp(head, "RIFF", RIFF_ID_BYTES) != 0 || memcmp(head + RIFF_HEADER_BYTES, "sfbk", RIFF_TYPE_BYTES) != 0) {
+ return MUSIC_ERR_BANK;
+ }
+
+ uint32_t end = RIFF_HEADER_BYTES + le32(head + RIFF_ID_BYTES);
+
+ end = end < src->size ? end : src->size;
+
+ for (uint32_t at = sizeof(head); at + RIFF_HEADER_BYTES + RIFF_TYPE_BYTES <= end;) {
+ uint8_t list[RIFF_HEADER_BYTES + RIFF_TYPE_BYTES];
+ uint32_t len;
+
+ if (!srcRead(src, at, list, sizeof(list))) {
+ return MUSIC_ERR_BANK;
+ }
+
+ len = le32(list + RIFF_ID_BYTES);
+
+ if (len > end - at - RIFF_HEADER_BYTES) {
+ return MUSIC_ERR_BANK;
+ }
+
+ uint32_t body = at + RIFF_HEADER_BYTES + RIFF_TYPE_BYTES;
+ uint32_t bodyEnd = at + RIFF_HEADER_BYTES + len;
+ bool isList = memcmp(list, "LIST", RIFF_ID_BYTES) == 0;
+
+ // The sub-chunks of INFO and sdta that matter: ifil, smpl
+ for (uint32_t sub = body; isList && sub + RIFF_HEADER_BYTES <= bodyEnd;) {
+ uint8_t ck[RIFF_HEADER_BYTES];
+ uint32_t ckLen;
+
+ if (!srcRead(src, sub, ck, sizeof(ck))) {
+ return MUSIC_ERR_BANK;
+ }
+
+ ckLen = le32(ck + RIFF_ID_BYTES);
+
+ if (ckLen > bodyEnd - sub - RIFF_HEADER_BYTES) {
+ return MUSIC_ERR_BANK;
+ }
+
+ if (memcmp(list + RIFF_HEADER_BYTES, "INFO", RIFF_TYPE_BYTES) == 0 && memcmp(ck, "ifil", RIFF_ID_BYTES) == 0 && ckLen >= IFIL_BYTES) {
+ uint8_t ver[IFIL_BYTES];
+
+ version = srcRead(src, sub + RIFF_HEADER_BYTES, ver, sizeof(ver)) ? le16(ver) : 0;
+ } else if (memcmp(list + RIFF_HEADER_BYTES, "sdta", RIFF_TYPE_BYTES) == 0 && memcmp(ck, "smpl", RIFF_ID_BYTES) == 0) {
+ bank->dataOffset = sub + RIFF_HEADER_BYTES;
+ bank->dataFrames = ckLen / SAMPLE_BYTES;
+ }
+
+ sub += RIFF_HEADER_BYTES + ckLen + (ckLen & RIFF_ALIGN_MASK);
+ }
+
+ if (isList && memcmp(list + RIFF_HEADER_BYTES, "pdta", RIFF_TYPE_BYTES) == 0) {
+ pdtaAt = body;
+ pdtaLen = len - RIFF_TYPE_BYTES;
+ }
+
+ at = bodyEnd + (len & RIFF_ALIGN_MASK);
+ }
+
+ if ((version != SF_VERSION_1 && version != SF_VERSION_2) || !pdtaAt) {
+ return MUSIC_ERR_BANK;
+ }
+
+ return readHydra(src, pdtaAt, pdtaLen, version == SF_VERSION_1, bank);
+}
+
+
+int16_t sf2GenDefault(int32_t gen) {
+ return gen >= 0 && gen < SF_GEN_COUNT ? sGenRules[gen].def : 0;
+}
+
+
+int32_t sf2Load(const char *path, WaveBankT *bank) {
+ SourceT src;
+ int32_t rc;
+
+ memset(bank, 0, sizeof(*bank));
+ memset(&src, 0, sizeof(src));
+ src.file = fopen(path, "rb");
+
+ if (!src.file) {
+ return MUSIC_ERR_OPEN;
+ }
+
+ fseek(src.file, 0, SEEK_END);
+ src.size = (uint32_t)ftell(src.file);
+ snprintf(bank->path, sizeof(bank->path), "%s", path);
+ rc = sf2From(&src, bank);
+ fclose(src.file);
+
+ if (rc != MUSIC_OK) {
+ waveBankFree(bank);
+ }
+
+ return rc;
+}
+
+
+int32_t sf2Parse(const uint8_t *data, uint32_t size, WaveBankT *bank) {
+ SourceT src;
+ int32_t rc;
+
+ memset(bank, 0, sizeof(*bank));
+
+ if (!data) {
+ return MUSIC_ERR_BANK;
+ }
+
+ memset(&src, 0, sizeof(src));
+ src.data = data;
+ src.size = size;
+ bank->memory = data;
+ rc = sf2From(&src, bank);
+
+ if (rc != MUSIC_OK) {
+ waveBankFree(bank);
+ }
+
+ return rc;
+}
+
+
+// Frames from the sources that hold them (silence between), each read
+// from its file and made signed 16-bit.
+static bool sourcesRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out) {
+ memset(out, 0, (size_t)frames * sizeof(int16_t));
+
+ for (uint32_t i = 0; i < bank->sourceCount; i++) {
+ const WaveSourceT *src = &bank->sources[i];
+ uint32_t from = src->first > first ? src->first : first;
+ uint32_t to = src->first + src->frames < first + frames ? src->first + src->frames : first + frames;
+
+ if (from >= to) {
+ continue;
+ }
+
+ uint32_t bytes = src->format & WAVE_SRC_16BIT ? SAMPLE_BYTES : 1;
+ uint32_t count = to - from;
+ uint8_t *raw = (uint8_t *)malloc((size_t)count * bytes);
+
+ if (!raw) {
+ return false;
+ }
+
+ FILE *f = fopen(bank->files[src->file], "rb");
+ long at = (long)src->offset + (long)(from - src->first) * (long)bytes;
+ bool ok = f && fseek(f, at, SEEK_SET) == 0 && fread(raw, bytes, count, f) == count;
+
+ if (f) {
+ fclose(f);
+ }
+
+ for (uint32_t k = 0; ok && k < count; k++) {
+ uint32_t v = bytes == SAMPLE_BYTES ? le16(raw + k * SAMPLE_BYTES) : (uint32_t)raw[k] << BYTE_BITS;
+
+ out[from - first + k] = (int16_t)(src->format & WAVE_SRC_UNSIGNED ? v ^ SIGN_FLIP : v);
+ }
+
+ free(raw);
+
+ if (!ok) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+static bool srcRead(SourceT *src, uint32_t offset, void *out, uint32_t len) {
+ if (offset > src->size || len > src->size - offset) {
+ return false;
+ }
+
+ if (src->data) {
+ memcpy(out, src->data + offset, len);
+ return true;
+ }
+
+ return fseek(src->file, (long)offset, SEEK_SET) == 0 && fread(out, 1, len, src->file) == len;
+}
+
+
+void waveBankFree(WaveBankT *bank) {
+ free(bank->presets);
+ free(bank->regions);
+ free(bank->samples);
+ free(bank->sources);
+ free(bank->files);
+ memset(bank, 0, sizeof(*bank));
+}
+
+
+bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out) {
+ if (first > bank->dataFrames || frames > bank->dataFrames - first) {
+ return false;
+ }
+
+ if (bank->sources) {
+ return sourcesRead(bank, first, frames, out);
+ }
+
+ uint32_t offset = bank->dataOffset + first * SAMPLE_BYTES;
+
+ if (bank->memory) {
+ for (uint32_t i = 0; i < frames; i++) {
+ out[i] = (int16_t)le16(bank->memory + offset + i * SAMPLE_BYTES);
+ }
+
+ return true;
+ }
+
+ FILE *f = fopen(bank->path, "rb");
+ bool ok = f && fseek(f, (long)offset, SEEK_SET) == 0 && fread(out, SAMPLE_BYTES, frames, f) == frames;
+
+ if (f) {
+ fclose(f);
+ }
+
+ return ok;
+}
+
+
+// Whether generators first .. last - 1 hold oper (a zone's instrument or
+// sample: its last generator, by the specification).
+static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, uint32_t oper) {
+ return last > first && le16(h->at[genArray] + (last - 1) * GEN_BYTES + GEN_OPER) == oper;
+}
diff --git a/src/libs/kpunch/music/musicShell.c b/src/libs/kpunch/music/musicShell.c
index d4f6c443..f68bbd1c 100644
--- a/src/libs/kpunch/music/musicShell.c
+++ b/src/libs/kpunch/music/musicShell.c
@@ -23,9 +23,10 @@
// musicShell.c -- music.lib's hooks into the shell
//
// Loaded as a module of the shell: the constructor asks for the shell's
-// idle time (end calls) and to be told when a program ends (its musics
-// are freed then); the destructor frees everything and takes the hooks
-// back. Not in the host tests, which call musicIdle and musicAppEnded
+// idle time (end calls), to be told when a program ends (its musics are
+// freed then), and when DVX steps aside for a DOS program (every music
+// lets go of the synthesizers); the destructor frees everything and takes
+// the hooks back. Not in the host tests, which call musicIdle and musicAppEnded
// directly.
#include "musicInt.h"
@@ -38,11 +39,13 @@ static void musicShellShutdown(void) __attribute__((destructor));
static void musicShellInit(void) {
shellRegisterIdle(musicIdle, NULL);
shellRegisterAppCleanup(musicAppEnded);
+ platformSuspendHookAdd(musicSuspend);
}
static void musicShellShutdown(void) {
musicShutdown();
+ platformSuspendHookRemove(musicSuspend);
shellUnregisterAppCleanup(musicAppEnded);
shellUnregisterIdle(musicIdle, NULL);
}
diff --git a/src/libs/kpunch/music/musicSmf.c b/src/libs/kpunch/music/musicSmf.c
index fd3410c9..8b4ade1d 100644
--- a/src/libs/kpunch/music/musicSmf.c
+++ b/src/libs/kpunch/music/musicSmf.c
@@ -28,8 +28,9 @@
// entries, the entries are sorted into one time line, and tempo changes
// (from any track) turn ticks into microseconds on the way. Channel
// messages (with running status), system exclusive messages (F0, and F7
-// escapes) and the tempo and end-of-track meta events are kept; other
-// meta events are skipped. A note on with velocity 0 becomes a note
+// escapes) and the tempo and end-of-track meta events are kept, and the
+// first track's name becomes the file's title; other meta events are
+// skipped. A note on with velocity 0 becomes a note
// off.
//
// Damage: an unknown chunk is skipped; a track that breaks off (short
@@ -38,6 +39,9 @@
// where it breaks; a chunk longer than the file is read to the end of the
// file. Only a missing header or no track at all makes the file
// unusable. Type 2 files (independent patterns) are not played.
+//
+// Writing: type 0 files (one track) for the music macros and MIDI
+// recordings, the events put into a byte array by midiPut....
#include "musicInt.h"
#include "stb_ds.h"
@@ -59,13 +63,21 @@
#define RIFF_HEADER_BYTES 12
#define RIFF_FORM_AT 8
#define VLQ_BYTES_MAX 4
+#define VLQ_VALUE_MAX 0x0FFFFFFF // the largest four bytes hold
#define VLQ_MORE 0x80
#define META_TEMPO 0x51
+#define META_TRACK_NAME 0x03 // a track's name; the first track's is the file's title
+#define TITLE_SPACE ' ' // what a control character of a title becomes
#define META_END_OF_TRACK 0x2F
#define META_TEMPO_BYTES 3
#define TEMPO_DEFAULT_US 500000 // 120 beats a minute
#define US_PER_SEC 1000000ULL
#define BYTE_BITS 8
+#define BYTE_MASK 0xFF
+#define CHUNK_LEN_BYTES 4
+#define WORD_BYTES 2
+#define SMF_TYPE_ONE_TRACK 0
+#define SMF_ONE_TRACK 1
#define NOTE_OFF_VELOCITY 64
#define SYSTEM_FIRST 0xF0
@@ -91,11 +103,17 @@ static uint32_t be16(const uint8_t *p);
static uint32_t be32(const uint8_t *p);
static bool findSmf(const uint8_t *data, uint32_t size, const uint8_t **smf, uint32_t *smfSize);
static bool idIs(const uint8_t *p, const char *id);
+int32_t midiFileBuild(const uint8_t *trk, uint32_t trkLen, uint32_t division, uint8_t **out, uint32_t *size);
void midiFileFree(MidiFileT *file);
bool midiIsFile(const uint8_t *data, uint32_t size);
int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out);
+void midiPutBig(uint8_t **buf, uint32_t value, int32_t bytes);
+void midiPutEnd(uint8_t **trk, uint32_t delta);
+static void midiPutId(uint8_t **buf, const char *id);
+void midiPutTempo(uint8_t **trk, uint32_t usPerQuarter);
+void midiPutVlq(uint8_t **buf, uint32_t value);
static int rawCompare(const void *a, const void *b);
-static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t **sysex);
+static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t **sysex, char *title);
static bool readVlq(SmfCursorT *c, uint32_t *value);
@@ -151,6 +169,35 @@ static bool idIs(const uint8_t *p, const char *id) {
}
+// The header chunk, then the track chunk around the events.
+int32_t midiFileBuild(const uint8_t *trk, uint32_t trkLen, uint32_t division, uint8_t **out, uint32_t *size) {
+ uint8_t *hdr = NULL;
+
+ midiPutId(&hdr, "MThd");
+ midiPutBig(&hdr, SMF_HEADER_BYTES, CHUNK_LEN_BYTES);
+ midiPutBig(&hdr, SMF_TYPE_ONE_TRACK, WORD_BYTES);
+ midiPutBig(&hdr, SMF_ONE_TRACK, WORD_BYTES);
+ midiPutBig(&hdr, division, WORD_BYTES);
+ midiPutId(&hdr, "MTrk");
+ midiPutBig(&hdr, trkLen, CHUNK_LEN_BYTES);
+
+ uint32_t hdrLen = (uint32_t)arrlen(hdr);
+ uint8_t *file = (uint8_t *)malloc(hdrLen + trkLen);
+
+ if (!file) {
+ arrfree(hdr);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ memcpy(file, hdr, hdrLen);
+ memcpy(file + hdrLen, trk, trkLen);
+ arrfree(hdr);
+ *out = file;
+ *size = hdrLen + trkLen;
+ return MUSIC_OK;
+}
+
+
void midiFileFree(MidiFileT *file) {
free(file->events);
free(file->sysex);
@@ -220,7 +267,7 @@ int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out) {
if (idIs(smf + at, "MTrk")) {
SmfCursorT c = { smf + at + CHUNK_HEADER_BYTES, smf + at + CHUNK_HEADER_BYTES + len };
- uint32_t last = readTrack(c, tracks, &raw, &sysex);
+ uint32_t last = readTrack(c, tracks, &raw, &sysex, out->title);
endTick = last > endTick ? last : endTick;
tracks++;
@@ -309,6 +356,59 @@ int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out) {
// Time, then track, then order within the track.
+void midiPutBig(uint8_t **buf, uint32_t value, int32_t bytes) {
+ for (int32_t i = bytes - 1; i >= 0; i--) {
+ arrput(*buf, (uint8_t)((value >> (i * BYTE_BITS)) & BYTE_MASK));
+ }
+}
+
+
+void midiPutEnd(uint8_t **trk, uint32_t delta) {
+ midiPutVlq(trk, delta);
+ arrput(*trk, MIDI_META);
+ arrput(*trk, META_END_OF_TRACK);
+ arrput(*trk, 0);
+}
+
+
+static void midiPutId(uint8_t **buf, const char *id) {
+ for (int32_t i = 0; i < CHUNK_ID_BYTES; i++) {
+ arrput(*buf, (uint8_t)id[i]);
+ }
+}
+
+
+void midiPutTempo(uint8_t **trk, uint32_t usPerQuarter) {
+ midiPutVlq(trk, 0);
+ arrput(*trk, MIDI_META);
+ arrput(*trk, META_TEMPO);
+ arrput(*trk, META_TEMPO_BYTES);
+ midiPutBig(trk, usPerQuarter, META_TEMPO_BYTES);
+}
+
+
+// Seven bits a byte, most significant first, VLQ_MORE set on all but the
+// last; a value over four bytes' worth is held to the most they carry.
+void midiPutVlq(uint8_t **buf, uint32_t value) {
+ uint8_t tmp[VLQ_BYTES_MAX];
+ int32_t n = 0;
+
+ if (value > VLQ_VALUE_MAX) {
+ value = VLQ_VALUE_MAX;
+ }
+
+ do {
+ tmp[n++] = (uint8_t)(value & MIDI_DATA_MASK);
+ value >>= MIDI_DATA_BITS;
+ } while (value);
+
+ while (n > 0) {
+ n--;
+ arrput(*buf, (uint8_t)(tmp[n] | (n > 0 ? VLQ_MORE : 0)));
+ }
+}
+
+
static int rawCompare(const void *a, const void *b) {
const RawEventT *x = (const RawEventT *)a;
const RawEventT *y = (const RawEventT *)b;
@@ -326,8 +426,10 @@ static int rawCompare(const void *a, const void *b) {
// Append a track's events to raw (system exclusive bytes to sysex);
-// returns the tick where it ends.
-static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t **sysex) {
+// returns the tick where it ends. The first track's first name is the
+// file's title (control characters as spaces, MUSIC_TITLE_MAX - 1 bytes
+// at most).
+static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t **sysex, char *title) {
uint32_t tick = 0;
uint32_t order = 0;
uint8_t running = 0;
@@ -360,7 +462,7 @@ static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t
if (status < SYSTEM_FIRST) {
uint8_t type = status & MIDI_TYPE_MASK;
- uint32_t bytes = (type == MIDI_PROGRAM || type == MIDI_CHAN_PRESSURE) ? 1 : 2;
+ uint32_t bytes = MIDI_ONE_DATA_BYTE(type) ? 1 : 2;
if (c.end - c.p < (ptrdiff_t)bytes || (c.p[0] & MIDI_STATUS_BIT) || (bytes == 2 && (c.p[1] & MIDI_STATUS_BIT))) {
break;
@@ -396,6 +498,16 @@ static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t
break;
}
+ if (meta == META_TRACK_NAME && track == 0 && !title[0]) {
+ uint32_t n = len < MUSIC_TITLE_MAX - 1 ? len : MUSIC_TITLE_MAX - 1;
+
+ for (uint32_t i = 0; i < n; i++) {
+ title[i] = c.p[i] < TITLE_SPACE ? TITLE_SPACE : (char)c.p[i];
+ }
+
+ title[n] = '\0';
+ }
+
if (meta == META_TEMPO && len == META_TEMPO_BYTES) {
e.tempo = ((uint32_t)c.p[0] << (BYTE_BITS * 2)) | ((uint32_t)c.p[1] << BYTE_BITS) | c.p[2];
diff --git a/src/libs/kpunch/music/musicSound.c b/src/libs/kpunch/music/musicSound.c
new file mode 100644
index 00000000..43d15666
--- /dev/null
+++ b/src/libs/kpunch/music/musicSound.c
@@ -0,0 +1,249 @@
+// 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.
+
+// musicSound.c -- WAV and VOC files as music (see musicInt.h, "Sound
+// files")
+//
+// A WAV file named by its path is read as it plays: loading reads only
+// its first SOUND_HEAD_MAX bytes (audioWavParse finds its format and
+// where its sound is), and the stream's fill reads the frames it needs, a
+// buffer at a time, at program time; a long recording needs no memory.
+// A VOC file (its blocks may hold silences and continue one another), or
+// a sound file given as bytes, is decoded into 16-bit frames in memory
+// (audioDecode; not pinned, as nothing reads it at interrupt time).
+
+#include "musicInt.h"
+
+#include
+#include
+#include
+
+#define SOUND_HEAD_MAX 65536 // bytes read to find a WAV's format and data
+#define SOUND_READ_FRAMES 1024 // frames read from the file at a time
+#define BYTE_BITS 8
+
+struct SoundSrcT {
+ FILE *fp; // a WAV read as it plays (NULL: pcm)
+ int16_t *pcm; // decoded frames (NULL: fp)
+ uint8_t *raw; // fp's frames as the file has them, SOUND_READ_FRAMES at a time
+ AudioFormatT fmt; // as the file has it (bits 8 or 16)
+ uint32_t dataAt; // fp: where the frames start
+ uint32_t frames;
+ uint32_t pos; // the next frame read
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+uint32_t soundChannels(const SoundSrcT *s);
+void soundClose(SoundSrcT *s);
+static int32_t soundError(int32_t audioRc);
+static uint32_t soundFrameBytes(const SoundSrcT *s);
+uint32_t soundFrames(const SoundSrcT *s);
+int32_t soundOpenFile(const char *path, SoundSrcT **out);
+int32_t soundOpenMem(const uint8_t *data, uint32_t size, SoundSrcT **out);
+uint32_t soundRate(const SoundSrcT *s);
+uint32_t soundRead(SoundSrcT *s, int16_t *out, uint32_t frames);
+static uint32_t soundReadFile(SoundSrcT *s, int16_t *out, uint32_t frames);
+void soundSeek(SoundSrcT *s, uint32_t frame);
+uint32_t soundTell(const SoundSrcT *s);
+
+
+uint32_t soundChannels(const SoundSrcT *s) {
+ return s->fmt.channels;
+}
+
+
+void soundClose(SoundSrcT *s) {
+ if (!s) {
+ return;
+ }
+
+ if (s->fp) {
+ fclose(s->fp);
+ }
+
+ free(s->raw);
+ free(s->pcm);
+ free(s);
+}
+
+
+// audio.lib's reason as music.lib's.
+static int32_t soundError(int32_t audioRc) {
+ switch (audioRc) {
+ case AUDIO_ERR_NO_MEMORY:
+ return MUSIC_ERR_NO_MEMORY;
+
+ case AUDIO_ERR_CORRUPT:
+ return MUSIC_ERR_CORRUPT;
+
+ case AUDIO_ERR_UNSUPPORTED:
+ return MUSIC_ERR_UNSUPPORTED;
+
+ default:
+ return MUSIC_ERR_FORMAT;
+ }
+}
+
+
+// A frame of the file as the file has it, in bytes.
+static uint32_t soundFrameBytes(const SoundSrcT *s) {
+ return s->fmt.channels * s->fmt.bits / BYTE_BITS;
+}
+
+
+uint32_t soundFrames(const SoundSrcT *s) {
+ return s->frames;
+}
+
+
+// The file's start read and parsed; it stays open for the frames.
+int32_t soundOpenFile(const char *path, SoundSrcT **out) {
+ SoundSrcT *s = (SoundSrcT *)calloc(1, sizeof(SoundSrcT));
+ uint8_t *head;
+ uint32_t dataBytes;
+ int32_t rc;
+
+ *out = NULL;
+
+ if (!s) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ s->fp = fopen(path, "rb");
+
+ long size = s->fp && fseek(s->fp, 0, SEEK_END) == 0 ? ftell(s->fp) : -1;
+
+ if (size <= 0 || fseek(s->fp, 0, SEEK_SET) != 0) {
+ soundClose(s);
+ return MUSIC_ERR_OPEN;
+ }
+
+ uint32_t headBytes = (uint32_t)size < SOUND_HEAD_MAX ? (uint32_t)size : SOUND_HEAD_MAX;
+
+ head = (uint8_t *)malloc(headBytes);
+
+ if (!head) {
+ soundClose(s);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ rc = fread(head, 1, headBytes, s->fp) == headBytes ? audioWavParse(head, headBytes, (uint32_t)size, &s->fmt, &s->dataAt, &dataBytes) : AUDIO_ERR_CORRUPT;
+ free(head);
+
+ if (rc != AUDIO_OK) {
+ soundClose(s);
+ return soundError(rc);
+ }
+
+ s->frames = dataBytes / soundFrameBytes(s);
+ s->raw = (uint8_t *)malloc(SOUND_READ_FRAMES * soundFrameBytes(s));
+
+ if (!s->raw) {
+ soundClose(s);
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ soundSeek(s, 0);
+ *out = s;
+ return MUSIC_OK;
+}
+
+
+int32_t soundOpenMem(const uint8_t *data, uint32_t size, SoundSrcT **out) {
+ SoundSrcT *s = (SoundSrcT *)calloc(1, sizeof(SoundSrcT));
+ int32_t rc;
+
+ *out = NULL;
+
+ if (!s) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ rc = audioDecode(data, size, &s->pcm, &s->frames, &s->fmt);
+
+ if (rc != AUDIO_OK) {
+ soundClose(s);
+ return soundError(rc);
+ }
+
+ *out = s;
+ return MUSIC_OK;
+}
+
+
+uint32_t soundRate(const SoundSrcT *s) {
+ return s->fmt.rate;
+}
+
+
+uint32_t soundRead(SoundSrcT *s, int16_t *out, uint32_t frames) {
+ uint32_t n = frames < s->frames - s->pos ? frames : s->frames - s->pos;
+
+ if (s->fp) {
+ return soundReadFile(s, out, n);
+ }
+
+ memcpy(out, s->pcm + (size_t)s->pos * s->fmt.channels, (size_t)n * s->fmt.channels * sizeof(int16_t));
+ s->pos += n;
+ return n;
+}
+
+
+// A buffer at a time; a file that ends before its header said ends there.
+static uint32_t soundReadFile(SoundSrcT *s, int16_t *out, uint32_t frames) {
+ uint32_t done = 0;
+
+ while (done < frames) {
+ uint32_t want = frames - done < SOUND_READ_FRAMES ? frames - done : SOUND_READ_FRAMES;
+ uint32_t got = (uint32_t)fread(s->raw, soundFrameBytes(s), want, s->fp);
+
+ audioConvert(s->raw, out + (size_t)done * s->fmt.channels, got * s->fmt.channels, s->fmt.bits);
+ done += got;
+ s->pos += got;
+
+ if (got < want) {
+ s->frames = s->pos;
+ break;
+ }
+ }
+
+ return done;
+}
+
+
+void soundSeek(SoundSrcT *s, uint32_t frame) {
+ s->pos = frame < s->frames ? frame : s->frames;
+
+ if (s->fp) {
+ uint32_t at = s->dataAt + s->pos * soundFrameBytes(s);
+
+ fseek(s->fp, (long)at, SEEK_SET);
+ }
+}
+
+
+uint32_t soundTell(const SoundSrcT *s) {
+ return s->pos;
+}
diff --git a/src/libs/kpunch/music/musicWave.c b/src/libs/kpunch/music/musicWave.c
new file mode 100644
index 00000000..5276f3a0
--- /dev/null
+++ b/src/libs/kpunch/music/musicWave.c
@@ -0,0 +1,1064 @@
+// 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.
+
+// musicWave.c -- General MIDI on wavetable voices (SoundFont regions)
+//
+// A WaveSynthT plays MIDI with a SoundFont bank (musicSf2.c) whose
+// samples musicWaveLoad.c put in the synthesizer's memory, on voices from
+// a backend: the wavetable chip (audio.lib's shared voices) or the
+// software one. Runs at interrupt time as well (the timer plays MIDI on
+// the chip), see musicInt.h: integer arithmetic only, no calls out but
+// the backend's, everything pinned.
+//
+// A note plays every region of its preset whose key and velocity ranges
+// hold it (several: layers, stereo pairs), a voice each. The preset is
+// the channel's bank and program; channel 10 plays bank 128 (the drum
+// kits), its program choosing the kit. The note's pitch is the key's
+// distance from the sample's root (scale tuning cents a key), the tuning,
+// the sample's correction, and its rate (cents from 44,100 Hz), with the
+// pitch bend (RPN 0 range) on top. Loudness follows the SoundFont's
+// default modulators as attenuation in centibels: the region's own (0.4
+// dB a unit, as the EMU8000 SoundFonts were made for), the velocity, the
+// channel volume (CC7) and expression (CC11) each 40 log10 (the same
+// curve as General MIDI's), and the music's gain. CC10 pans, CC1 and
+// channel pressure deepen the vibrato (50 cents at full), CC91 and CC93
+// add reverb and chorus (20 percent at full), the velocity closes the
+// filter (2400 cents at the softest). An exclusive class (an open and a
+// closed hi-hat) stops the class's other voices on the channel. The
+// sustain pedal holds released notes. With no voice free, the oldest
+// released voice is taken, else the oldest. A note the bank cannot play
+// here (no preset or region for it, or a sample that did not fit in the
+// synthesizer's memory) goes whole to the overflow sink, which gets every
+// other message as well, so its channels are in step.
+//
+// What the backend does not do itself is done here, a tick at a time
+// (waveSynthTick): the volume envelope (delay, attack linear in
+// amplitude, hold, decay and release at 96 dB per stage time, sustain),
+// the modulation envelope and the two LFOs (triangles) to pitch and
+// volume, sent as pitch and volume updates when they move by a step.
+
+#include "musicInt.h"
+
+#include
+
+#define WAVE_ATTEN_NUM 4 // the region's attenuation counts 0.4 dB a unit
+#define WAVE_ATTEN_DEN 10
+#define WAVE_FULL_SCALE 960 // centibels a decay or release moves in its stage time
+#define WAVE_ATTEN_STEP 4 // centibels the volume moves before it is sent again
+#define WAVE_CENTS_SEMI 100
+#define WAVE_COARSE_FRAMES 32768 // a coarse address offset's unit
+#define WAVE_KEY_CENTER 60 // the key the keynum-to-envelope generators count from
+#define WAVE_VEL_FILTER 2400 // cents the softest note closes the filter by
+#define WAVE_FILTER_MIN 1500
+#define WAVE_FILTER_MAX 13500
+#define WAVE_MOD_DEPTH 50 // cents of vibrato the mod wheel and pressure add at full
+#define WAVE_SEND_DEPTH 200 // tenths of a percent CC91 / CC93 add at full
+#define WAVE_SEND_MAX 1000
+#define WAVE_EXPRESSION_MAX 127
+#define WAVE_BEND_STEPS 8192
+#define WAVE_LOOP_MASK 3
+#define WAVE_LOOP_UNUSED 2 // sampleModes 2 plays like no loop
+#define BYTE_BITS 8
+
+// Time: timecents (2^(tc / 1200) seconds) through waveOctaves
+#define US_MANT_MUL 15625 // 1,000,000 / 65536 = 15625 / 1024
+#define US_MANT_SHIFT 10
+#define LFO_BASE_MILLIHZ 8176 // 0 absolute cents: 8.176 Hz
+#define LFO_CYCLE 65536 // a phase's units in one cycle
+#define LFO_HALF (LFO_CYCLE / 2)
+#define LFO_PHASE_DIV 15259 // 1e9 / 65536: millihertz x microseconds -> phase units
+
+// 400 log10(127 / i): the attenuation of a velocity or controller level
+static const int16_t sCbForLevel[128] = {
+ 1440, 842, 721, 651, 601, 562, 530, 503, 480, 460, 442, 425, 410, 396, 383, 371,
+ 360, 349, 339, 330, 321, 313, 305, 297, 289, 282, 276, 269, 263, 257, 251, 245,
+ 239, 234, 229, 224, 219, 214, 210, 205, 201, 196, 192, 188, 184, 180, 176, 173,
+ 169, 165, 162, 158, 155, 152, 149, 145, 142, 139, 136, 133, 130, 127, 125, 122,
+ 119, 116, 114, 111, 109, 106, 103, 101, 99, 96, 94, 91, 89, 87, 85, 82,
+ 80, 78, 76, 74, 72, 70, 68, 66, 64, 62, 60, 58, 56, 54, 52, 50,
+ 49, 47, 45, 43, 42, 40, 38, 36, 35, 33, 31, 30, 28, 27, 25, 23,
+ 22, 20, 19, 17, 16, 14, 13, 11, 10, 8, 7, 6, 4, 3, 1, 0
+};
+
+static const uint8_t sGmOn[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t bendCents(const WaveChannelT *c);
+static const WavePresetT *channelPreset(const WaveSynthT *s, uint8_t channel);
+static void channelReset(WaveChannelT *c);
+static int32_t clampInt(int32_t v, int32_t lo, int32_t hi);
+static void controller(WaveSynthT *s, uint8_t channel, uint8_t cc, uint8_t value);
+static void envTick(const WaveSynthT *s, WaveVoiceT *v, uint32_t us);
+static int32_t lfoAt(uint32_t *phase, int16_t freq, uint32_t us);
+static uint32_t lfoMilliHz(int32_t cents);
+static void modEnvTick(WaveVoiceT *v, uint32_t us);
+static void noteOff(WaveSynthT *s, uint8_t channel, uint8_t key);
+static void noteOn(WaveSynthT *s, uint8_t channel, uint8_t key, uint8_t velocity);
+static bool notePlayable(const WaveSynthT *s, uint8_t channel, uint8_t key, uint8_t velocity);
+static void noteSetup(const WaveSynthT *s, WaveVoiceT *v, const WaveRegionT *r, uint8_t key, uint8_t velocity);
+static void releaseHeld(WaveSynthT *s, uint8_t channel);
+static int32_t stageDiv(uint32_t stageUs, int32_t span);
+static uint32_t tcToUs(int32_t tc);
+static void voiceDeliver(WaveSynthT *s, WaveVoiceT *v, uint32_t what);
+static void voiceFree(WaveSynthT *s, WaveVoiceT *v);
+static void voiceLevels(const WaveSynthT *s, WaveVoiceT *v);
+static void voiceRelease(WaveSynthT *s, WaveVoiceT *v);
+static void voicesFreeAll(WaveSynthT *s);
+static void voicesUpdate(WaveSynthT *s, uint8_t channel, uint32_t what);
+static WaveVoiceT *voiceTake(WaveSynthT *s);
+const WavePresetT *waveBankPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program);
+static bool waveHwActive(void *owner, int32_t voice);
+static int32_t waveHwAlloc(void *owner);
+void waveHwBackend(AudioCtxT *ctx, WaveBackendT *out);
+static void waveHwFree(void *owner, int32_t voice);
+static void waveHwRelease(void *owner, int32_t voice);
+static void waveHwStart(void *owner, int32_t voice, const AudioWtVoiceT *v);
+static void waveHwUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what);
+int32_t waveLockItems(AudioLockItemT *items);
+static void waveMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2);
+static void waveSilence(MidiSinkT *sink);
+void waveSynthInit(WaveSynthT *synth, const WaveBackendT *backend, const WaveBankT *bank, const WaveLoadedT *loaded, MidiSinkT *overflow, int32_t gain);
+void waveSynthSetGain(WaveSynthT *synth, int32_t gain);
+void waveSynthTick(WaveSynthT *synth, uint32_t us);
+static void waveSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len);
+
+
+static int32_t bendCents(const WaveChannelT *c) {
+ return (int32_t)c->bend * c->bendRange * WAVE_CENTS_SEMI / WAVE_BEND_STEPS;
+}
+
+
+// The preset a channel plays: its bank and program; channel 10's kit.
+static const WavePresetT *channelPreset(const WaveSynthT *s, uint8_t channel) {
+ const WaveChannelT *c = &s->channels[channel];
+
+ return waveBankPreset(s->bank, channel == MIDI_PERCUSSION ? SF_DRUM_BANK : c->bankMsb, c->program);
+}
+
+
+static void channelReset(WaveChannelT *c) {
+ c->bend = 0;
+ c->program = 0;
+ c->bankMsb = 0;
+ c->volume = MIDI_VOLUME_DEFAULT;
+ c->expression = WAVE_EXPRESSION_MAX;
+ c->pan = MIDI_CENTER;
+ c->bendRange = MIDI_BEND_RANGE_DEFAULT;
+ c->modWheel = 0;
+ c->pressure = 0;
+ c->reverb = 0;
+ c->chorus = 0;
+ c->rpnMsb = MIDI_RPN_NULL;
+ c->rpnLsb = MIDI_RPN_NULL;
+ c->sustain = false;
+}
+
+
+static int32_t clampInt(int32_t v, int32_t lo, int32_t hi) {
+ return v < lo ? lo : (v > hi ? hi : v);
+}
+
+
+static void controller(WaveSynthT *s, uint8_t channel, uint8_t cc, uint8_t value) {
+ WaveChannelT *c = &s->channels[channel];
+
+ switch (cc) {
+ case MIDI_CC_BANK:
+ c->bankMsb = value;
+ break;
+
+ case MIDI_CC_MODULATION:
+ c->modWheel = value;
+ voicesUpdate(s, channel, AUDIO_WT_SET_LFO);
+ break;
+
+ case MIDI_CC_VOLUME:
+ c->volume = value;
+ voicesUpdate(s, channel, AUDIO_WT_SET_VOLUME);
+ break;
+
+ case MIDI_CC_PAN:
+ c->pan = value;
+ voicesUpdate(s, channel, AUDIO_WT_SET_PAN);
+ break;
+
+ case MIDI_CC_EXPRESSION:
+ c->expression = value;
+ voicesUpdate(s, channel, AUDIO_WT_SET_VOLUME);
+ break;
+
+ case MIDI_CC_SUSTAIN:
+ c->sustain = value >= MIDI_CENTER;
+
+ if (!c->sustain) {
+ releaseHeld(s, channel);
+ }
+
+ break;
+
+ case MIDI_CC_REVERB:
+ c->reverb = value;
+ voicesUpdate(s, channel, AUDIO_WT_SET_EFFECTS);
+ break;
+
+ case MIDI_CC_CHORUS:
+ c->chorus = value;
+ voicesUpdate(s, channel, AUDIO_WT_SET_EFFECTS);
+ break;
+
+ case MIDI_CC_RPN_MSB:
+ c->rpnMsb = value;
+ break;
+
+ case MIDI_CC_RPN_LSB:
+ c->rpnLsb = value;
+ break;
+
+ case MIDI_CC_NRPN_MSB:
+ case MIDI_CC_NRPN_LSB:
+ c->rpnMsb = MIDI_RPN_NULL;
+ c->rpnLsb = MIDI_RPN_NULL;
+ break;
+
+ case MIDI_CC_DATA:
+ if (c->rpnMsb == 0 && c->rpnLsb == MIDI_RPN_BEND_RANGE) {
+ c->bendRange = value;
+ voicesUpdate(s, channel, AUDIO_WT_SET_PITCH);
+ }
+
+ break;
+
+ case MIDI_CC_SOUND_OFF:
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ if (s->voices[i].id && s->voices[i].channel == channel) {
+ voiceFree(s, &s->voices[i]);
+ }
+ }
+
+ break;
+
+ case MIDI_CC_RESET:
+ c->bend = 0;
+ c->expression = WAVE_EXPRESSION_MAX;
+ c->modWheel = 0;
+ c->pressure = 0;
+ c->sustain = false;
+ c->rpnMsb = MIDI_RPN_NULL;
+ c->rpnLsb = MIDI_RPN_NULL;
+ releaseHeld(s, channel);
+ voicesUpdate(s, channel, AUDIO_WT_SET_PITCH | AUDIO_WT_SET_VOLUME | AUDIO_WT_SET_LFO);
+ break;
+
+ case MIDI_CC_NOTES_OFF:
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ if (s->voices[i].id && s->voices[i].channel == channel && s->voices[i].state != WAVE_V_RELEASED) {
+ voiceRelease(s, &s->voices[i]);
+ }
+ }
+
+ break;
+
+ default:
+ break;
+ }
+}
+
+
+// The volume envelope (and the modulation envelope) a tick on. Decay and
+// release move WAVE_FULL_SCALE in their stage times; the voice is done
+// once its release has passed WAVE_FULL_SCALE (96 dB down).
+static void envTick(const WaveSynthT *s, WaveVoiceT *v, uint32_t us) {
+ const int16_t *env = v->note.volEnv;
+
+ (void)s;
+ v->stageUs += us;
+
+ switch (v->stage) {
+ case AUDIO_WT_ENV_DELAY:
+ v->envAtten = AUDIO_WT_ATTEN_MAX;
+
+ if (v->stageUs >= tcToUs(env[AUDIO_WT_ENV_DELAY])) {
+ v->stage = AUDIO_WT_ENV_ATTACK;
+ v->stageUs = 0;
+ }
+
+ break;
+
+ case AUDIO_WT_ENV_ATTACK: {
+ int32_t step = v->stageUs / (uint32_t)stageDiv(tcToUs(env[AUDIO_WT_ENV_ATTACK]), WAVE_STEP_MAX);
+
+ v->envAtten = waveCbForStep[step < WAVE_STEP_MAX ? step : WAVE_STEP_MAX];
+
+ if (step >= WAVE_STEP_MAX) {
+ v->stage = AUDIO_WT_ENV_HOLD;
+ v->stageUs = 0;
+ }
+
+ break;
+ }
+
+ case AUDIO_WT_ENV_HOLD:
+ v->envAtten = 0;
+
+ if (v->stageUs >= tcToUs(env[AUDIO_WT_ENV_HOLD])) {
+ v->stage = AUDIO_WT_ENV_DECAY;
+ v->stageUs = 0;
+ }
+
+ break;
+
+ case AUDIO_WT_ENV_DECAY:
+ v->envAtten = (int32_t)(v->stageUs / (uint32_t)stageDiv(tcToUs(env[AUDIO_WT_ENV_DECAY]), WAVE_FULL_SCALE));
+
+ if (v->envAtten >= env[AUDIO_WT_ENV_SUSTAIN]) {
+ v->envAtten = env[AUDIO_WT_ENV_SUSTAIN];
+ v->stage = AUDIO_WT_ENV_SUSTAIN;
+ }
+
+ break;
+
+ case AUDIO_WT_ENV_SUSTAIN:
+ v->envAtten = env[AUDIO_WT_ENV_SUSTAIN];
+ break;
+
+ case AUDIO_WT_ENV_RELEASE:
+ v->envAtten = v->relFrom + (int32_t)(v->stageUs / (uint32_t)stageDiv(tcToUs(env[AUDIO_WT_ENV_RELEASE]), WAVE_FULL_SCALE));
+
+ if (v->envAtten >= WAVE_FULL_SCALE) {
+ v->envAtten = AUDIO_WT_ATTEN_MAX;
+ v->stage = WAVE_ENV_DONE;
+ }
+
+ break;
+
+ default:
+ break;
+ }
+
+ if (v->note.modEnvToPitch || v->note.modEnvToFilter) {
+ modEnvTick(v, us);
+ }
+}
+
+
+// A triangle LFO's value (-32768..32767) after moving us microseconds at
+// freq (absolute cents).
+static int32_t lfoAt(uint32_t *phase, int16_t freq, uint32_t us) {
+ uint32_t p;
+
+ *phase = (*phase + lfoMilliHz(freq) * us / LFO_PHASE_DIV) % LFO_CYCLE;
+ p = *phase;
+ return (int32_t)(p < LFO_HALF ? p : LFO_CYCLE - 1 - p) * 2 - LFO_HALF;
+}
+
+
+// Absolute cents to millihertz (8.176 Hz x 2^(cents / 1200)).
+static uint32_t lfoMilliHz(int32_t cents) {
+ uint32_t frac;
+ int32_t oct = waveOctaves(cents, &frac);
+ uint32_t base = LFO_BASE_MILLIHZ * frac >> WAVE_FIX_SHIFT;
+
+ return oct >= 0 ? base << oct : base >> -oct;
+}
+
+
+// The modulation envelope (0..WAVE_PERMILLE): attack linear, hold,
+// decay toward the sustain level and release to 0 at full scale per
+// stage time.
+static void modEnvTick(WaveVoiceT *v, uint32_t us) {
+ const int16_t *env = v->note.modEnv;
+ int32_t sustain = WAVE_PERMILLE - env[AUDIO_WT_ENV_SUSTAIN];
+
+ v->modStageUs += us;
+
+ switch (v->modStage) {
+ case AUDIO_WT_ENV_DELAY:
+ v->modEnv = 0;
+
+ if (v->modStageUs >= tcToUs(env[AUDIO_WT_ENV_DELAY])) {
+ v->modStage = AUDIO_WT_ENV_ATTACK;
+ v->modStageUs = 0;
+ }
+
+ break;
+
+ case AUDIO_WT_ENV_ATTACK:
+ v->modEnv = (int32_t)(v->modStageUs / (uint32_t)stageDiv(tcToUs(env[AUDIO_WT_ENV_ATTACK]), WAVE_PERMILLE));
+
+ if (v->modEnv >= WAVE_PERMILLE) {
+ v->modEnv = WAVE_PERMILLE;
+ v->modStage = AUDIO_WT_ENV_HOLD;
+ v->modStageUs = 0;
+ }
+
+ break;
+
+ case AUDIO_WT_ENV_HOLD:
+ if (v->modStageUs >= tcToUs(env[AUDIO_WT_ENV_HOLD])) {
+ v->modStage = AUDIO_WT_ENV_DECAY;
+ v->modStageUs = 0;
+ }
+
+ break;
+
+ case AUDIO_WT_ENV_DECAY:
+ v->modEnv = WAVE_PERMILLE - (int32_t)(v->modStageUs / (uint32_t)stageDiv(tcToUs(env[AUDIO_WT_ENV_DECAY]), WAVE_PERMILLE));
+
+ if (v->modEnv <= sustain) {
+ v->modEnv = sustain;
+ v->modStage = AUDIO_WT_ENV_SUSTAIN;
+ }
+
+ break;
+
+ case AUDIO_WT_ENV_RELEASE:
+ v->modEnv = v->modFrom - (int32_t)(v->modStageUs / (uint32_t)stageDiv(tcToUs(env[AUDIO_WT_ENV_RELEASE]), WAVE_PERMILLE));
+ v->modEnv = v->modEnv > 0 ? v->modEnv : 0;
+ break;
+
+ default:
+ break;
+ }
+}
+
+
+// Every voice of the key on the channel released (held while the sustain
+// pedal is down).
+static void noteOff(WaveSynthT *s, uint8_t channel, uint8_t key) {
+ bool sustain = s->channels[channel].sustain;
+
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ WaveVoiceT *v = &s->voices[i];
+
+ if (v->id && v->state == WAVE_V_ON && v->channel == channel && v->key == key) {
+ if (sustain) {
+ v->state = WAVE_V_HELD;
+ } else {
+ voiceRelease(s, v);
+ }
+ }
+ }
+}
+
+
+// A voice for every region of the preset that holds the key and the
+// velocity and whose sample is in the synthesizer's memory.
+// Every region of the note's preset that holds it, a voice each
+// (notePlayable said they can play).
+static void noteOn(WaveSynthT *s, uint8_t channel, uint8_t key, uint8_t velocity) {
+ const WavePresetT *preset = channelPreset(s, channel);
+
+ for (uint32_t i = 0; i < preset->count; i++) {
+ const WaveRegionT *r = &s->bank->regions[preset->first + i];
+
+ if (key < r->keyLo || key > r->keyHi || velocity < r->velLo || velocity > r->velHi) {
+ continue;
+ }
+
+ uint8_t exclusive = (uint8_t)r->gen[SF_GEN_EXCLUSIVE_CLASS];
+
+ for (int32_t j = 0; exclusive && j < WAVE_VOICES_MAX; j++) {
+ if (s->voices[j].id && s->voices[j].channel == channel && s->voices[j].exclusive == exclusive) {
+ voiceFree(s, &s->voices[j]);
+ }
+ }
+
+ WaveVoiceT *v = voiceTake(s);
+
+ if (!v) {
+ return;
+ }
+
+ v->channel = channel;
+ v->key = key;
+ v->exclusive = exclusive;
+ v->state = WAVE_V_ON;
+ v->age = ++s->clock;
+ noteSetup(s, v, r, key, velocity);
+ voiceLevels(s, v);
+ s->backend.start(s->backend.owner, v->id, &v->note);
+ v->sentAtten = v->note.attenuation;
+ v->sentPitch = v->note.pitch;
+ }
+}
+
+
+// Some region of the channel's preset holds the note and every one that
+// does has its sample in the synthesizer's memory.
+static bool notePlayable(const WaveSynthT *s, uint8_t channel, uint8_t key, uint8_t velocity) {
+ const WavePresetT *preset = channelPreset(s, channel);
+ bool any = false;
+
+ for (uint32_t i = 0; preset && i < preset->count; i++) {
+ const WaveRegionT *r = &s->bank->regions[preset->first + i];
+
+ if (key < r->keyLo || key > r->keyHi || velocity < r->velLo || velocity > r->velHi) {
+ continue;
+ }
+
+ if (s->loaded[r->sample].addr == WAVE_NOT_LOADED) {
+ return false;
+ }
+
+ any = true;
+ }
+
+ return any;
+}
+
+
+// What a region makes of a key and velocity: the sample's place, loop
+// and pitch, the attenuation before the channel's, the filter, the
+// envelopes and LFOs (keyed hold and decay), the effect sends.
+static void noteSetup(const WaveSynthT *s, WaveVoiceT *v, const WaveRegionT *r, uint8_t key, uint8_t velocity) {
+ const int16_t *g = r->gen;
+ const WaveSampleT *sample = &s->bank->samples[r->sample];
+ const WaveLoadedT *ld = &s->loaded[r->sample];
+ AudioWtVoiceT *n = &v->note;
+ int32_t root = g[SF_GEN_ROOT_KEY] >= 0 ? g[SF_GEN_ROOT_KEY] : sample->rootKey;
+ int32_t k = g[SF_GEN_KEYNUM] >= 0 ? g[SF_GEN_KEYNUM] : key;
+ int32_t vel = g[SF_GEN_VELOCITY] >= 0 ? g[SF_GEN_VELOCITY] : velocity;
+ int32_t len = (int32_t)(sample->end - sample->start);
+ int32_t start = clampInt(g[SF_GEN_START_OFFSET] + (g[SF_GEN_START_COARSE] * WAVE_COARSE_FRAMES), 0, len);
+ int32_t end = clampInt(len + g[SF_GEN_END_OFFSET] + (g[SF_GEN_END_COARSE] * WAVE_COARSE_FRAMES), start, len);
+ int32_t lStart = clampInt((int32_t)(sample->loopStart - sample->start) + g[SF_GEN_LOOP_START_OFFSET] + (g[SF_GEN_LOOP_START_COARSE] * WAVE_COARSE_FRAMES), start, end);
+ int32_t lEnd = clampInt((int32_t)(sample->loopEnd - sample->start) + g[SF_GEN_LOOP_END_OFFSET] + (g[SF_GEN_LOOP_END_COARSE] * WAVE_COARSE_FRAMES), lStart, end);
+ int32_t keyed = WAVE_KEY_CENTER - k;
+ uint32_t frame = ld->bits / BYTE_BITS;
+ uint8_t modes = (uint8_t)(g[SF_GEN_SAMPLE_MODES] & WAVE_LOOP_MASK);
+
+ // Every field is set here or by voiceLevels (no library calls at
+ // interrupt time)
+ start >>= ld->shift;
+ end >>= ld->shift;
+ lStart >>= ld->shift;
+ lEnd >>= ld->shift;
+
+ n->addr = ld->addr + (uint32_t)start * frame;
+ n->offset = 0;
+ n->end = (uint32_t)(end - start);
+ n->loopStart = (uint32_t)(lStart - start);
+ n->loopEnd = (uint32_t)(lEnd - start);
+ n->bits = ld->bits;
+ n->loop = modes == WAVE_LOOP_UNUSED ? AUDIO_WT_LOOP_NONE : modes;
+
+ if (n->loopEnd <= n->loopStart) {
+ n->loop = AUDIO_WT_LOOP_NONE;
+ }
+
+ v->region = r;
+ v->basePitch = ld->rateCents + (k - root) * g[SF_GEN_SCALE_TUNING] + g[SF_GEN_COARSE_TUNE] * WAVE_CENTS_SEMI + g[SF_GEN_FINE_TUNE] + sample->correction;
+ v->baseAtten = g[SF_GEN_ATTENUATION] * WAVE_ATTEN_NUM / WAVE_ATTEN_DEN + sCbForLevel[vel];
+
+ n->filterFc = clampInt(g[SF_GEN_FILTER_FC] - WAVE_VEL_FILTER * (MIDI_DATA_MASK - vel) / MIDI_DATA_MASK, WAVE_FILTER_MIN, WAVE_FILTER_MAX);
+ n->filterQ = g[SF_GEN_FILTER_Q];
+ n->modEnvToPitch = g[SF_GEN_MOD_ENV_TO_PITCH];
+ n->modEnvToFilter = g[SF_GEN_MOD_ENV_TO_FILTER];
+ n->modLfoDelay = g[SF_GEN_MOD_LFO_DELAY];
+ n->modLfoFreq = g[SF_GEN_MOD_LFO_FREQ];
+ n->modLfoToPitch = g[SF_GEN_MOD_LFO_TO_PITCH];
+ n->modLfoToFilter = g[SF_GEN_MOD_LFO_TO_FILTER];
+ n->modLfoToVolume = g[SF_GEN_MOD_LFO_TO_VOLUME];
+ n->vibLfoDelay = g[SF_GEN_VIB_LFO_DELAY];
+ n->vibLfoFreq = g[SF_GEN_VIB_LFO_FREQ];
+
+ for (int32_t i = 0; i < AUDIO_WT_ENV_STAGES; i++) {
+ n->volEnv[i] = g[SF_GEN_VOL_ENV_DELAY + i];
+ n->modEnv[i] = g[SF_GEN_MOD_ENV_DELAY + i];
+ }
+
+ n->volEnv[AUDIO_WT_ENV_HOLD] = (int16_t)(n->volEnv[AUDIO_WT_ENV_HOLD] + g[SF_GEN_KEY_TO_VOL_ENV_HOLD] * keyed);
+ n->volEnv[AUDIO_WT_ENV_DECAY] = (int16_t)(n->volEnv[AUDIO_WT_ENV_DECAY] + g[SF_GEN_KEY_TO_VOL_ENV_DECAY] * keyed);
+ n->modEnv[AUDIO_WT_ENV_HOLD] = (int16_t)(n->modEnv[AUDIO_WT_ENV_HOLD] + g[SF_GEN_KEY_TO_MOD_ENV_HOLD] * keyed);
+ n->modEnv[AUDIO_WT_ENV_DECAY] = (int16_t)(n->modEnv[AUDIO_WT_ENV_DECAY] + g[SF_GEN_KEY_TO_MOD_ENV_DECAY] * keyed);
+
+ // Envelopes run here start silent; the chip's start at once
+ bool soft = !(s->backend.flags & AUDIO_WT_ENVELOPES);
+
+ v->stage = soft ? AUDIO_WT_ENV_DELAY : AUDIO_WT_ENV_SUSTAIN;
+ v->modStage = AUDIO_WT_ENV_DELAY;
+ v->envAtten = soft ? AUDIO_WT_ATTEN_MAX : 0;
+ v->modEnv = 0;
+ v->stageUs = 0;
+ v->modStageUs = 0;
+ v->sinceOn = 0;
+ v->modPhase = 0;
+ v->vibPhase = 0;
+}
+
+
+// The sustain pedal is up: the notes it held are released.
+static void releaseHeld(WaveSynthT *s, uint8_t channel) {
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ if (s->voices[i].id && s->voices[i].state == WAVE_V_HELD && s->voices[i].channel == channel) {
+ voiceRelease(s, &s->voices[i]);
+ }
+ }
+}
+
+
+// The microseconds one unit of a stage takes when span units take
+// stageUs (at least 1, so the stage ends).
+static int32_t stageDiv(uint32_t stageUs, int32_t span) {
+ uint32_t d = stageUs / (uint32_t)span;
+
+ return d > 0 ? (int32_t)d : 1;
+}
+
+
+// Timecents to microseconds: 1,000,000 x 2^(tc / 1200), all in 32 bits
+// (tc -12000 .. 8000: about 1 ms .. 101 s).
+static uint32_t tcToUs(int32_t tc) {
+ uint32_t frac;
+ int32_t oct = waveOctaves(tc, &frac);
+ uint32_t base = frac * US_MANT_MUL >> US_MANT_SHIFT;
+
+ return oct >= 0 ? base << oct : base >> -oct;
+}
+
+
+// Send the voice's note again (what changed), and remember what was sent.
+static void voiceDeliver(WaveSynthT *s, WaveVoiceT *v, uint32_t what) {
+ voiceLevels(s, v);
+ s->backend.update(s->backend.owner, v->id, &v->note, what);
+ v->sentAtten = v->note.attenuation;
+ v->sentPitch = v->note.pitch;
+}
+
+
+static void voiceFree(WaveSynthT *s, WaveVoiceT *v) {
+ s->backend.free(s->backend.owner, v->id);
+ v->id = 0;
+ v->state = WAVE_V_FREE;
+}
+
+
+// The note's pitch, attenuation, pan, effect sends and vibrato depth from
+// its region's values, its channel, the music's gain, and (run here) the
+// envelopes and LFOs as they stand.
+static void voiceLevels(const WaveSynthT *s, WaveVoiceT *v) {
+ const WaveChannelT *c = &s->channels[v->channel];
+ const int16_t *g = v->region->gen;
+ AudioWtVoiceT *n = &v->note;
+ int32_t gain = clampInt(s->gain, 0, AUDIO_VOLUME_MAX) >> WAVE_GAIN_SHIFT;
+ int32_t depth = g[SF_GEN_VIB_LFO_TO_PITCH] + (c->modWheel + c->pressure) * WAVE_MOD_DEPTH / MIDI_DATA_MASK;
+ int32_t pitch = v->basePitch + bendCents(c);
+ int32_t atten = v->baseAtten + sCbForLevel[c->volume] + sCbForLevel[c->expression] + waveCbForStep[gain] + v->envAtten;
+
+ if (!(s->backend.flags & AUDIO_WT_ENVELOPES)) {
+ pitch += n->modEnvToPitch * v->modEnv / WAVE_PERMILLE;
+ }
+
+ if (!(s->backend.flags & AUDIO_WT_LFOS) && v->sinceOn >= tcToUs(n->vibLfoDelay)) {
+ uint32_t phase = v->vibPhase;
+
+ pitch += depth * lfoAt(&phase, n->vibLfoFreq, 0) / LFO_HALF;
+ }
+
+ if (!(s->backend.flags & AUDIO_WT_LFOS) && v->sinceOn >= tcToUs(n->modLfoDelay)) {
+ uint32_t phase = v->modPhase;
+ int32_t lfo = lfoAt(&phase, n->modLfoFreq, 0);
+
+ pitch += n->modLfoToPitch * lfo / LFO_HALF;
+ atten += n->modLfoToVolume * lfo / LFO_HALF;
+ }
+
+ n->pitch = pitch;
+ n->attenuation = clampInt(atten, 0, AUDIO_WT_ATTEN_MAX);
+ n->pan = clampInt(g[SF_GEN_PAN] + (c->pan - MIDI_CENTER) * AUDIO_WT_PAN_MAX / MIDI_CENTER, -AUDIO_WT_PAN_MAX, AUDIO_WT_PAN_MAX);
+ n->reverb = clampInt(g[SF_GEN_REVERB_SEND] + c->reverb * WAVE_SEND_DEPTH / MIDI_DATA_MASK, 0, WAVE_SEND_MAX);
+ n->chorus = clampInt(g[SF_GEN_CHORUS_SEND] + c->chorus * WAVE_SEND_DEPTH / MIDI_DATA_MASK, 0, WAVE_SEND_MAX);
+ n->vibLfoToPitch = (int16_t)depth;
+}
+
+
+// The key is up: the backend's envelopes release (a sample looping while
+// the key is down plays out); ours start their release from where they
+// are.
+static void voiceRelease(WaveSynthT *s, WaveVoiceT *v) {
+ v->state = WAVE_V_RELEASED;
+ s->backend.release(s->backend.owner, v->id);
+
+ if (!(s->backend.flags & AUDIO_WT_ENVELOPES)) {
+ v->relFrom = v->stage == AUDIO_WT_ENV_DELAY ? WAVE_FULL_SCALE : v->envAtten;
+ v->modFrom = v->modEnv;
+ v->stage = AUDIO_WT_ENV_RELEASE;
+ v->modStage = AUDIO_WT_ENV_RELEASE;
+ v->stageUs = 0;
+ v->modStageUs = 0;
+ }
+}
+
+
+static void voicesFreeAll(WaveSynthT *s) {
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ if (s->voices[i].id) {
+ voiceFree(s, &s->voices[i]);
+ }
+ }
+}
+
+
+// Every sounding voice of the channel sent again (what changed).
+static void voicesUpdate(WaveSynthT *s, uint8_t channel, uint32_t what) {
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ if (s->voices[i].id && s->voices[i].channel == channel) {
+ voiceDeliver(s, &s->voices[i], what);
+ }
+ }
+}
+
+
+// A free slot and a backend voice for it; with none free, the oldest
+// released voice is taken, else the oldest.
+static WaveVoiceT *voiceTake(WaveSynthT *s) {
+ WaveVoiceT *slot = NULL;
+
+ for (int32_t i = 0; i < WAVE_VOICES_MAX && !slot; i++) {
+ slot = s->voices[i].id ? NULL : &s->voices[i];
+ }
+
+ int32_t id = slot ? s->backend.alloc(s->backend.owner) : 0;
+
+ if (id <= 0) {
+ WaveVoiceT *victim = NULL;
+
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ WaveVoiceT *v = &s->voices[i];
+
+ if (!v->id) {
+ continue;
+ }
+
+ bool better = !victim || (v->state == WAVE_V_RELEASED && victim->state != WAVE_V_RELEASED) || ((v->state == WAVE_V_RELEASED) == (victim->state == WAVE_V_RELEASED) && v->age < victim->age);
+
+ victim = better ? v : victim;
+ }
+
+ if (!victim) {
+ return NULL;
+ }
+
+ voiceFree(s, victim);
+ slot = victim;
+ id = s->backend.alloc(s->backend.owner);
+
+ if (id <= 0) {
+ return NULL;
+ }
+ }
+
+ slot->id = id;
+ return slot;
+}
+
+
+// That one; else, for drums, the standard kit (program 0, else the
+// bank's first); else the same program in bank 0 (a variation the bank
+// lacks).
+const WavePresetT *waveBankPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program) {
+ bool drums = bankNum == SF_DRUM_BANK;
+ const WavePresetT *spare = NULL;
+
+ for (uint32_t i = 0; i < bank->presetCount; i++) {
+ const WavePresetT *p = &bank->presets[i];
+
+ if (p->bank == bankNum && p->program == program) {
+ return p;
+ }
+
+ if (drums ? (p->bank == SF_DRUM_BANK && (!spare || p->program == 0)) : (p->bank == 0 && p->program == program)) {
+ spare = p;
+ }
+ }
+
+ return spare;
+}
+
+
+static bool waveHwActive(void *owner, int32_t voice) {
+ return audioWtActive((AudioCtxT *)owner, voice);
+}
+
+
+static int32_t waveHwAlloc(void *owner) {
+ return audioWtAlloc((AudioCtxT *)owner);
+}
+
+
+void waveHwBackend(AudioCtxT *ctx, WaveBackendT *out) {
+ AudioWtInfoT info;
+
+ audioWtGetInfo(&info);
+ out->alloc = waveHwAlloc;
+ out->free = waveHwFree;
+ out->start = waveHwStart;
+ out->update = waveHwUpdate;
+ out->release = waveHwRelease;
+ out->active = waveHwActive;
+ out->owner = ctx;
+ out->flags = info.flags;
+}
+
+
+static void waveHwFree(void *owner, int32_t voice) {
+ audioWtFree((AudioCtxT *)owner, voice);
+}
+
+
+static void waveHwRelease(void *owner, int32_t voice) {
+ audioWtRelease((AudioCtxT *)owner, voice);
+}
+
+
+static void waveHwStart(void *owner, int32_t voice, const AudioWtVoiceT *v) {
+ audioWtStart((AudioCtxT *)owner, voice, v);
+}
+
+
+static void waveHwUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ audioWtUpdate((AudioCtxT *)owner, voice, v, what);
+}
+
+
+int32_t waveLockItems(AudioLockItemT *items) {
+ const void *fns[] = {
+ (const void *)bendCents,
+ (const void *)channelPreset,
+ (const void *)channelReset,
+ (const void *)clampInt,
+ (const void *)controller,
+ (const void *)envTick,
+ (const void *)lfoAt,
+ (const void *)lfoMilliHz,
+ (const void *)modEnvTick,
+ (const void *)noteOff,
+ (const void *)noteOn,
+ (const void *)notePlayable,
+ (const void *)noteSetup,
+ (const void *)releaseHeld,
+ (const void *)stageDiv,
+ (const void *)tcToUs,
+ (const void *)voiceDeliver,
+ (const void *)voiceFree,
+ (const void *)voiceLevels,
+ (const void *)voiceRelease,
+ (const void *)voiceTake,
+ (const void *)voicesFreeAll,
+ (const void *)voicesUpdate,
+ (const void *)waveBankPreset,
+ (const void *)waveHwActive,
+ (const void *)waveHwAlloc,
+ (const void *)waveHwFree,
+ (const void *)waveHwRelease,
+ (const void *)waveHwStart,
+ (const void *)waveHwUpdate,
+ (const void *)waveMessage,
+ (const void *)waveSilence,
+ (const void *)waveSynthTick,
+ (const void *)waveSysex
+ };
+ int32_t count = (int32_t)(sizeof(fns) / sizeof(fns[0]));
+
+ for (int32_t i = 0; i < count; i++) {
+ items[i].addr = fns[i];
+ items[i].size = 0;
+ }
+
+ items[count].addr = sCbForLevel;
+ items[count].size = sizeof(sCbForLevel);
+ count++;
+ return count + waveCentsLockItems(&items[count]);
+}
+
+
+// A note on that plays here is not passed on; every other message is
+// (to keep the overflow's channels in step, and its notes ending).
+static void waveMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2) {
+ WaveSynthT *s = (WaveSynthT *)sink;
+ uint8_t channel = status & MIDI_CHANNEL_MASK;
+
+ switch (status & MIDI_TYPE_MASK) {
+ case MIDI_NOTE_ON:
+ if (!data2) {
+ noteOff(s, channel, data1);
+ } else if (notePlayable(s, channel, data1, data2)) {
+ noteOn(s, channel, data1, data2);
+ return;
+ }
+
+ break;
+
+ case MIDI_NOTE_OFF:
+ noteOff(s, channel, data1);
+ break;
+
+ case MIDI_CONTROL:
+ controller(s, channel, data1, data2);
+ break;
+
+ case MIDI_PROGRAM:
+ s->channels[channel].program = data1;
+ break;
+
+ case MIDI_CHAN_PRESSURE:
+ s->channels[channel].pressure = data1;
+ voicesUpdate(s, channel, AUDIO_WT_SET_LFO);
+ break;
+
+ case MIDI_PITCH_BEND:
+ s->channels[channel].bend = (int16_t)((data1 | (data2 << MIDI_DATA_BITS)) - MIDI_BEND_CENTER);
+ voicesUpdate(s, channel, AUDIO_WT_SET_PITCH);
+ break;
+
+ default:
+ break;
+ }
+
+ if (s->overflow) {
+ s->overflow->message(s->overflow, status, data1, data2);
+ }
+}
+
+
+static void waveSilence(MidiSinkT *sink) {
+ WaveSynthT *s = (WaveSynthT *)sink;
+
+ voicesFreeAll(s);
+
+ if (s->overflow) {
+ s->overflow->silence(s->overflow);
+ }
+}
+
+
+void waveSynthInit(WaveSynthT *synth, const WaveBackendT *backend, const WaveBankT *bank, const WaveLoadedT *loaded, MidiSinkT *overflow, int32_t gain) {
+ memset(synth, 0, sizeof(*synth));
+ synth->sink.message = waveMessage;
+ synth->sink.sysex = waveSysex;
+ synth->sink.silence = waveSilence;
+ synth->backend = *backend;
+ synth->bank = bank;
+ synth->loaded = loaded;
+ synth->overflow = overflow;
+ synth->gain = gain;
+
+ for (int32_t i = 0; i < MUSIC_MIDI_CHANNELS; i++) {
+ channelReset(&synth->channels[i]);
+ }
+}
+
+
+void waveSynthSetGain(WaveSynthT *synth, int32_t gain) {
+ synth->gain = gain;
+
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ if (synth->voices[i].id) {
+ voiceDeliver(synth, &synth->voices[i], AUDIO_WT_SET_VOLUME);
+ }
+ }
+}
+
+
+// Each voice: what runs here moves on (sent when the volume or the pitch
+// moved by a step, or the envelope finished); a voice whose envelope has
+// ended, or that the backend says has fallen silent, is given back.
+void waveSynthTick(WaveSynthT *synth, uint32_t us) {
+ bool softEnv = !(synth->backend.flags & AUDIO_WT_ENVELOPES);
+ bool softLfo = !(synth->backend.flags & AUDIO_WT_LFOS);
+
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ WaveVoiceT *v = &synth->voices[i];
+
+ if (!v->id) {
+ continue;
+ }
+
+ if (!synth->backend.active(synth->backend.owner, v->id)) {
+ voiceFree(synth, v);
+ continue;
+ }
+
+ v->sinceOn += us;
+
+ if (softEnv) {
+ envTick(synth, v, us);
+ }
+
+ if (softLfo) {
+ lfoAt(&v->vibPhase, v->note.vibLfoFreq, us);
+ lfoAt(&v->modPhase, v->note.modLfoFreq, us);
+ }
+
+ if (softEnv && v->stage == WAVE_ENV_DONE) {
+ voiceFree(synth, v);
+ continue;
+ }
+
+ if (!softEnv && !softLfo) {
+ continue;
+ }
+
+ voiceLevels(synth, v);
+
+ uint32_t what = 0;
+ int32_t moved = v->note.attenuation - v->sentAtten;
+
+ what |= (moved >= WAVE_ATTEN_STEP || moved <= -WAVE_ATTEN_STEP) ? AUDIO_WT_SET_VOLUME : 0;
+ what |= v->note.pitch != v->sentPitch ? AUDIO_WT_SET_PITCH : 0;
+
+ if (what) {
+ synth->backend.update(synth->backend.owner, v->id, &v->note, what);
+ v->sentAtten = v->note.attenuation;
+ v->sentPitch = v->note.pitch;
+ }
+ }
+}
+
+
+// General MIDI System On: every voice freed, every channel reset (the
+// overflow gets the message itself).
+static void waveSysex(MidiSinkT *sink, const uint8_t *data, uint32_t len) {
+ WaveSynthT *s = (WaveSynthT *)sink;
+ bool gmOn = len == sizeof(sGmOn);
+
+ if (s->overflow) {
+ s->overflow->sysex(s->overflow, data, len);
+ }
+
+ for (uint32_t i = 0; gmOn && i < len; i++) {
+ gmOn = data[i] == sGmOn[i];
+ }
+
+ if (!gmOn) {
+ return;
+ }
+
+ voicesFreeAll(s);
+
+ for (int32_t i = 0; i < MUSIC_MIDI_CHANNELS; i++) {
+ channelReset(&s->channels[i]);
+ }
+}
diff --git a/src/libs/kpunch/music/musicWaveLoad.c b/src/libs/kpunch/music/musicWaveLoad.c
new file mode 100644
index 00000000..917ef442
--- /dev/null
+++ b/src/libs/kpunch/music/musicWaveLoad.c
@@ -0,0 +1,386 @@
+// 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.
+
+// musicWaveLoad.c -- SoundFont samples in the synthesizer's memory (see
+// musicInt.h)
+//
+// A wavetable synthesizer has little memory (256 KB to 1 MB on a Gravis
+// UltraSound, 512 KB on most AWE32s), so a song loads only the samples
+// its notes play: its bank selects, programs and notes are walked first
+// (waveNeedsFile). What a song loads stays for the next song while it
+// fits; when room runs out the samples the song does not need go first,
+// those used longest ago first. When the needed samples do not fit even
+// then, they are all loaded again smaller: 8-bit where the chip plays
+// that, then at half the rate (each pair of frames averaged), then a
+// quarter, each try at one quality for every sample of the song; at the
+// least, the samples that still do not fit are left out (their notes play
+// on the FM synthesizer, musicWave.c). A ROM sample (a SoundFont made for
+// an AWE's General MIDI ROM) needs no memory: on a chip with that ROM it
+// plays from there, on any other it is left out.
+//
+// A sample is stored from its first frame through its end and the few
+// frames after it (a SoundFont puts 46 zero frames after every sample, and
+// matching frames after a loop's end) that a chip's interpolation reads.
+
+#include "musicInt.h"
+
+#include
+#include
+
+#define BITS_8 8
+#define BITS_16 16
+#define BYTE_BITS 8
+#define SHIFT_MAX 2 // a quarter of the rate at the least
+
+// The qualities tried, best first
+typedef struct {
+ uint8_t bits;
+ uint8_t shift;
+} QualityT;
+
+static const QualityT sQualities[] = {
+ { BITS_16, 0 },
+ { BITS_8, 0 },
+ { BITS_16, 1 },
+ { BITS_8, 1 },
+ { BITS_16, SHIFT_MAX },
+ { BITS_8, SHIFT_MAX }
+};
+
+#define QUALITY_COUNT ((int32_t)(sizeof(sQualities) / sizeof(sQualities[0])))
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool evictOldest(AudioCtxT *ctx, WaveCacheT *cache, const uint8_t *need);
+static int32_t loadAt(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, const QualityT *q, bool leaveOut, uint32_t *missing);
+static int32_t loadOne(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t sample, const QualityT *q);
+static uint32_t loadRom(const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags);
+static uint32_t markPreset(const WaveBankT *bank, const WavePresetT *preset, int32_t key, int32_t velocity, uint8_t *need);
+static bool qualityPlays(const QualityT *q, uint32_t flags);
+static uint32_t storedFrames(const WaveBankT *bank, uint32_t sample);
+static void unload(AudioCtxT *ctx, WaveCacheT *cache, uint32_t sample);
+void waveCacheFree(AudioCtxT *ctx, WaveCacheT *cache);
+int32_t waveCacheInit(WaveCacheT *cache, const WaveBankT *bank);
+int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags, uint32_t *missing);
+void waveNeedsFile(const WaveBankT *bank, const MidiFileT *file, uint8_t *need);
+uint32_t waveNeedsPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program, uint8_t *need);
+
+
+// Give up the loaded sample the song does not need that was used longest
+// ago; false when there is none.
+static bool evictOldest(AudioCtxT *ctx, WaveCacheT *cache, const uint8_t *need) {
+ int32_t oldest = -1;
+
+ for (uint32_t i = 0; i < cache->count; i++) {
+ uint32_t addr = cache->loaded[i].addr;
+
+ if (addr != WAVE_NOT_LOADED && !(addr & AUDIO_WT_ADDR_ROM) && !need[i] && (oldest < 0 || cache->lastUse[i] < cache->lastUse[oldest])) {
+ oldest = (int32_t)i;
+ }
+ }
+
+ if (oldest < 0) {
+ return false;
+ }
+
+ unload(ctx, cache, (uint32_t)oldest);
+ return true;
+}
+
+
+// The needed samples at quality q (those stored at another quality given
+// up and loaded again); MUSIC_ERR_NO_DEVICE when one does not fit, unless
+// leaveOut: it is counted in *missing and the rest go on. ROM samples
+// are loadRom's.
+static int32_t loadAt(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, const QualityT *q, bool leaveOut, uint32_t *missing) {
+ int32_t rc = MUSIC_OK;
+
+ *missing = 0;
+
+ for (uint32_t i = 0; i < cache->count; i++) {
+ const WaveLoadedT *ld = &cache->loaded[i];
+
+ if (need[i] && !(bank->samples[i].type & SF_SAMPLE_ROM) && ld->addr != WAVE_NOT_LOADED && (ld->bits != q->bits || ld->shift != q->shift)) {
+ unload(ctx, cache, i);
+ }
+ }
+
+ for (uint32_t i = 0; i < cache->count && rc == MUSIC_OK; i++) {
+ if (need[i] && !(bank->samples[i].type & SF_SAMPLE_ROM) && cache->loaded[i].addr == WAVE_NOT_LOADED) {
+ rc = loadOne(ctx, bank, cache, need, i, q);
+
+ if (rc == MUSIC_ERR_NO_DEVICE && leaveOut) {
+ (*missing)++;
+ rc = MUSIC_OK;
+ }
+ }
+ }
+
+ return rc;
+}
+
+
+// One sample into the synthesizer's memory at quality q: read, halved
+// shift times, room made (samples not needed given up) and uploaded.
+static int32_t loadOne(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t sample, const QualityT *q) {
+ const WaveSampleT *s = &bank->samples[sample];
+ uint32_t frames = storedFrames(bank, sample);
+ int16_t *pcm = (int16_t *)malloc((frames ? frames : 1) * sizeof(int16_t));
+ uint32_t addr = 0;
+ int32_t rc;
+
+ if (!pcm) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ if (!waveBankRead(bank, s->start, frames, pcm)) {
+ free(pcm);
+ return MUSIC_ERR_OPEN;
+ }
+
+ for (uint32_t h = 0; h < q->shift; h++) {
+ for (uint32_t i = 0; i < frames / 2; i++) {
+ pcm[i] = (int16_t)(((int32_t)pcm[i * 2] + pcm[i * 2 + 1]) / 2);
+ }
+
+ frames /= 2;
+ }
+
+ uint32_t bytes = frames * (q->bits / BYTE_BITS);
+
+ while ((rc = audioWtMemAlloc(ctx, bytes, q->bits, &addr)) == AUDIO_ERR_NO_ROOM && evictOldest(ctx, cache, need)) {
+ }
+
+ if (rc == AUDIO_OK && audioWtUpload(ctx, addr, pcm, frames, q->bits) != AUDIO_OK) {
+ audioWtMemFree(ctx, addr);
+ rc = AUDIO_ERR_ARG;
+ }
+
+ free(pcm);
+
+ if (rc != AUDIO_OK) {
+ return MUSIC_ERR_NO_DEVICE;
+ }
+
+ WaveLoadedT *ld = &cache->loaded[sample];
+
+ ld->addr = addr;
+ ld->frames = frames;
+ ld->bits = q->bits;
+ ld->shift = q->shift;
+ ld->rateCents = waveRatioCents(s->rate, AUDIO_WT_PITCH_RATE) - (int32_t)q->shift * WAVE_CENTS_OCTAVE;
+ return MUSIC_OK;
+}
+
+
+// The needed ROM samples where the chip has the ROM (flags): there, as
+// they are; returns how many it cannot play (no ROM).
+static uint32_t loadRom(const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags) {
+ uint32_t left = 0;
+
+ for (uint32_t i = 0; i < cache->count; i++) {
+ const WaveSampleT *s = &bank->samples[i];
+ WaveLoadedT *ld = &cache->loaded[i];
+
+ if (!need[i] || !(s->type & SF_SAMPLE_ROM) || ld->addr != WAVE_NOT_LOADED) {
+ continue;
+ }
+
+ if (!(flags & AUDIO_WT_ROM)) {
+ left++;
+ continue;
+ }
+
+ ld->addr = AUDIO_WT_ADDR_ROM | s->start * (BITS_16 / BYTE_BITS);
+ ld->frames = s->end - s->start;
+ ld->bits = BITS_16;
+ ld->shift = 0;
+ ld->rateCents = waveRatioCents(s->rate, AUDIO_WT_PITCH_RATE);
+ }
+
+ return left;
+}
+
+
+// The samples of a preset's regions that hold the key and velocity (any
+// key and velocity when they are negative); how many were not needed
+// before.
+static uint32_t markPreset(const WaveBankT *bank, const WavePresetT *preset, int32_t key, int32_t velocity, uint8_t *need) {
+ uint32_t added = 0;
+
+ for (uint32_t i = 0; preset && i < preset->count; i++) {
+ const WaveRegionT *r = &bank->regions[preset->first + i];
+
+ if ((key < 0 || (key >= r->keyLo && key <= r->keyHi)) && (velocity < 0 || (velocity >= r->velLo && velocity <= r->velHi))) {
+ added += !need[r->sample];
+ need[r->sample] = 1;
+ }
+ }
+
+ return added;
+}
+
+
+// The chip plays samples at q's bits.
+static bool qualityPlays(const QualityT *q, uint32_t flags) {
+ return q->bits == BITS_16 || (flags & AUDIO_WT_8BIT) != 0;
+}
+
+
+// A sample's frames as stored: through its end and the guard frames after
+// it (as many as the bank has).
+static uint32_t storedFrames(const WaveBankT *bank, uint32_t sample) {
+ const WaveSampleT *s = &bank->samples[sample];
+ uint32_t guard = bank->dataFrames - s->end < WAVE_GUARD_FRAMES ? bank->dataFrames - s->end : WAVE_GUARD_FRAMES;
+
+ return s->end - s->start + guard;
+}
+
+
+// A sample out of the synthesizer's memory (a ROM sample only forgotten).
+static void unload(AudioCtxT *ctx, WaveCacheT *cache, uint32_t sample) {
+ WaveLoadedT *ld = &cache->loaded[sample];
+
+ if (!(ld->addr & AUDIO_WT_ADDR_ROM)) {
+ audioWtMemFree(ctx, ld->addr);
+ }
+
+ ld->addr = WAVE_NOT_LOADED;
+}
+
+
+void waveCacheFree(AudioCtxT *ctx, WaveCacheT *cache) {
+ for (uint32_t i = 0; i < cache->count; i++) {
+ if (cache->loaded[i].addr != WAVE_NOT_LOADED) {
+ unload(ctx, cache, i);
+ }
+ }
+
+ if (cache->loaded) {
+ platformUnlockRegion(cache->loaded, cache->count * sizeof(WaveLoadedT), false);
+ }
+
+ free(cache->loaded);
+ free(cache->lastUse);
+ memset(cache, 0, sizeof(*cache));
+}
+
+
+int32_t waveCacheInit(WaveCacheT *cache, const WaveBankT *bank) {
+ uint32_t count = bank->sampleCount ? bank->sampleCount : 1;
+
+ memset(cache, 0, sizeof(*cache));
+ cache->loaded = (WaveLoadedT *)calloc(count, sizeof(WaveLoadedT));
+ cache->lastUse = (uint32_t *)calloc(count, sizeof(uint32_t));
+ cache->count = bank->sampleCount;
+
+ if (!cache->loaded || !cache->lastUse || platformLockRegion(cache->loaded, count * sizeof(WaveLoadedT), false) != 0) {
+ free(cache->loaded);
+ free(cache->lastUse);
+ memset(cache, 0, sizeof(*cache));
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ for (uint32_t i = 0; i < cache->count; i++) {
+ cache->loaded[i].addr = WAVE_NOT_LOADED;
+ }
+
+ return MUSIC_OK;
+}
+
+
+// The ROM samples, then each quality in turn until the needed samples
+// fit; at the least one those that do not are left out.
+int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags, uint32_t *missing) {
+ int32_t last = QUALITY_COUNT - 1;
+ uint32_t needed = 0;
+ uint32_t romLeft = loadRom(bank, cache, need, flags);
+ int32_t rc = MUSIC_ERR_NO_DEVICE;
+
+ cache->songs++;
+ *missing = 0;
+
+ for (uint32_t i = 0; i < cache->count; i++) {
+ cache->lastUse[i] = need[i] ? cache->songs : cache->lastUse[i];
+ needed += need[i];
+ }
+
+ while (!qualityPlays(&sQualities[last], flags)) {
+ last--;
+ }
+
+ for (int32_t qi = 0; qi <= last && rc == MUSIC_ERR_NO_DEVICE; qi++) {
+ if (qualityPlays(&sQualities[qi], flags)) {
+ rc = loadAt(ctx, bank, cache, need, &sQualities[qi], qi == last, missing);
+ }
+ }
+
+ *missing += romLeft;
+
+ // None fitted even at the least
+ return rc == MUSIC_OK && needed > 0 && *missing == needed ? MUSIC_ERR_NO_DEVICE : rc;
+}
+
+
+void waveNeedsFile(const WaveBankT *bank, const MidiFileT *file, uint8_t *need) {
+ uint8_t program[MUSIC_MIDI_CHANNELS];
+ uint8_t bankMsb[MUSIC_MIDI_CHANNELS];
+
+ memset(need, 0, bank->sampleCount);
+ memset(program, 0, sizeof(program));
+ memset(bankMsb, 0, sizeof(bankMsb));
+
+ for (uint32_t i = 0; i < file->count; i++) {
+ const MidiEventT *e = &file->events[i];
+ uint8_t ch = e->status & MIDI_CHANNEL_MASK;
+
+ switch (e->status & MIDI_TYPE_MASK) {
+ case MIDI_CONTROL:
+ if (e->data1 == MIDI_CC_BANK) {
+ bankMsb[ch] = e->data2;
+ }
+
+ break;
+
+ case MIDI_PROGRAM:
+ program[ch] = e->data1;
+ break;
+
+ case MIDI_NOTE_ON:
+ if (e->data2 > 0) {
+ markPreset(bank, waveBankPreset(bank, ch == MIDI_PERCUSSION ? SF_DRUM_BANK : bankMsb[ch], program[ch]), e->data1, e->data2, need);
+ }
+
+ break;
+
+ default:
+ break;
+ }
+ }
+}
+
+
+uint32_t waveNeedsPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program, uint8_t *need) {
+ return markPreset(bank, waveBankPreset(bank, bankNum, program), -1, -1, need);
+}
diff --git a/src/libs/kpunch/music/musicWaveSoft.c b/src/libs/kpunch/music/musicWaveSoft.c
new file mode 100644
index 00000000..f77df3bd
--- /dev/null
+++ b/src/libs/kpunch/music/musicWaveSoft.c
@@ -0,0 +1,260 @@
+// 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.
+
+// musicWaveSoft.c -- SoundFont MIDI in software (see musicInt.h)
+//
+// DVX.INI [audio] midi = softwave: musicWave.c's General MIDI on voices
+// mixed here instead of a chip's, for a sound card that only plays
+// sampled sound. A song's samples are read into memory (16-bit, at
+// their own rates: no card memory to fit) and stay with the music. A
+// voice steps through its sample by 2^(pitch / 1200) x 44,100 / the
+// output rate, interpolating between two frames; its attenuation and pan
+// become two gains (the pan as a balance: both full at the centre). The
+// envelopes and LFOs are musicWave.c's, sent as pitch and volume updates.
+// Program time only (the music's stream fill), so nothing here is pinned.
+
+#include "musicInt.h"
+
+#include
+#include
+
+#define ATTEN_CENTS_NUM 19932 // 2^(-cB x 19.932 / 1200) = 10^(-cB / 200)
+#define ATTEN_CENTS_DEN 1000
+#define FIX_SHIFT 16
+#define FIX_MASK 0xFFFF
+#define FRAC_HALF_SHIFT 1 // the fraction as 15 bits for the multiply
+#define PAN_HALF 500
+#define RENDER_CHUNK 64
+#define SAMPLE_MAX 32767
+#define SAMPLE_MIN (-32768)
+#define STEREO 2
+#define BITS_16 16
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool softActive(void *owner, int32_t voice);
+static int32_t softAlloc(void *owner);
+static void softFree(void *owner, int32_t voice);
+static void softGains(WaveSoftVoiceT *v, const AudioWtVoiceT *note);
+static void softRelease(void *owner, int32_t voice);
+static void softStart(void *owner, int32_t voice, const AudioWtVoiceT *v);
+static uint32_t softStep(const WaveSoftT *soft, int32_t pitch);
+static void softUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what);
+void waveSoftBackend(WaveSoftT *soft, uint32_t rate, WaveBackendT *out);
+void waveSoftFree(WaveSoftT *soft);
+int32_t waveSoftLoad(WaveSoftT *soft, const WaveBankT *bank, WaveLoadedT *loaded, const uint8_t *need);
+void waveSoftRender(WaveSoftT *soft, int16_t *out, uint32_t frames, uint32_t channels);
+
+
+static bool softActive(void *owner, int32_t voice) {
+ return ((WaveSoftT *)owner)->voices[voice - 1].playing;
+}
+
+
+// Voices are numbered from 1 (0 is no voice).
+static int32_t softAlloc(void *owner) {
+ WaveSoftT *s = (WaveSoftT *)owner;
+
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ if (!s->voices[i].used) {
+ s->voices[i].used = true;
+ s->voices[i].playing = false;
+ return i + 1;
+ }
+ }
+
+ return AUDIO_ERR_NO_VOICE;
+}
+
+
+static void softFree(void *owner, int32_t voice) {
+ WaveSoftVoiceT *v = &((WaveSoftT *)owner)->voices[voice - 1];
+
+ v->used = false;
+ v->playing = false;
+}
+
+
+// The attenuation as a gain, the pan as a balance.
+static void softGains(WaveSoftVoiceT *v, const AudioWtVoiceT *note) {
+ int32_t gain = (int32_t)(wavePow2(-note->attenuation * ATTEN_CENTS_NUM / ATTEN_CENTS_DEN) >> (FIX_SHIFT - WAVE_SOFT_GAIN_SHIFT));
+ int32_t pan = note->pan < -PAN_HALF ? -PAN_HALF : (note->pan > PAN_HALF ? PAN_HALF : note->pan);
+
+ v->gainL = pan > 0 ? gain * (PAN_HALF - pan) / PAN_HALF : gain;
+ v->gainR = pan < 0 ? gain * (PAN_HALF + pan) / PAN_HALF : gain;
+}
+
+
+// A loop that lasts while the key is down ends: the sample plays out.
+static void softRelease(void *owner, int32_t voice) {
+ WaveSoftVoiceT *v = &((WaveSoftT *)owner)->voices[voice - 1];
+
+ if (v->loop == AUDIO_WT_LOOP_RELEASE) {
+ v->loop = AUDIO_WT_LOOP_NONE;
+ }
+}
+
+
+// The note's sample is in the arena at addr (16-bit frames).
+static void softStart(void *owner, int32_t voice, const AudioWtVoiceT *v) {
+ WaveSoftT *s = (WaveSoftT *)owner;
+ WaveSoftVoiceT *sv = &s->voices[voice - 1];
+ uint32_t first = v->addr / sizeof(int16_t);
+
+ sv->pos = first + v->offset;
+ sv->frac = 0;
+ sv->step = softStep(s, v->pitch);
+ sv->loopStart = first + v->loopStart;
+ sv->loopEnd = first + v->loopEnd;
+ sv->end = first + v->end;
+ sv->loop = v->loopEnd > v->loopStart ? v->loop : AUDIO_WT_LOOP_NONE;
+ sv->playing = v->end > 0;
+ softGains(sv, v);
+}
+
+
+// 2^(pitch / 1200) x 44,100 / the output rate, 16.16.
+static uint32_t softStep(const WaveSoftT *soft, int32_t pitch) {
+ return wavePow2(pitch + soft->rateCents);
+}
+
+
+static void softUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ WaveSoftT *s = (WaveSoftT *)owner;
+ WaveSoftVoiceT *sv = &s->voices[voice - 1];
+
+ if (what & AUDIO_WT_SET_PITCH) {
+ sv->step = softStep(s, v->pitch);
+ }
+
+ if (what & (AUDIO_WT_SET_VOLUME | AUDIO_WT_SET_PAN)) {
+ softGains(sv, v);
+ }
+}
+
+
+// Program time: the output rate's cents worked out here, not as notes
+// start in the mixer.
+void waveSoftBackend(WaveSoftT *soft, uint32_t rate, WaveBackendT *out) {
+ soft->rateCents = waveRatioCents(AUDIO_WT_PITCH_RATE, rate);
+
+ out->alloc = softAlloc;
+ out->free = softFree;
+ out->start = softStart;
+ out->update = softUpdate;
+ out->release = softRelease;
+ out->active = softActive;
+ out->owner = soft;
+ out->flags = 0;
+}
+
+
+void waveSoftFree(WaveSoftT *soft) {
+ free(soft->arena);
+ soft->arena = NULL;
+ soft->frames = 0;
+}
+
+
+// Each needed sample not there yet: its frames and the guard frames
+// after it (as many as the bank has), at the end of the arena. A ROM
+// sample has no data here: its notes go to the FM synthesizer.
+int32_t waveSoftLoad(WaveSoftT *soft, const WaveBankT *bank, WaveLoadedT *loaded, const uint8_t *need) {
+ for (uint32_t i = 0; i < bank->sampleCount; i++) {
+ const WaveSampleT *s = &bank->samples[i];
+
+ if (!need[i] || (s->type & SF_SAMPLE_ROM) || loaded[i].addr != WAVE_NOT_LOADED) {
+ continue;
+ }
+
+ uint32_t guard = bank->dataFrames - s->end < WAVE_GUARD_FRAMES ? bank->dataFrames - s->end : WAVE_GUARD_FRAMES;
+ uint32_t frames = s->end - s->start + guard;
+ int16_t *grown = (int16_t *)realloc(soft->arena, (size_t)(soft->frames + frames) * sizeof(int16_t));
+
+ if (!grown) {
+ return MUSIC_ERR_NO_MEMORY;
+ }
+
+ soft->arena = grown;
+
+ if (!waveBankRead(bank, s->start, frames, soft->arena + soft->frames)) {
+ return MUSIC_ERR_OPEN;
+ }
+
+ loaded[i].addr = soft->frames * sizeof(int16_t);
+ loaded[i].frames = frames;
+ loaded[i].rateCents = waveRatioCents(s->rate, AUDIO_WT_PITCH_RATE);
+ loaded[i].bits = BITS_16;
+ loaded[i].shift = 0;
+ soft->frames += frames;
+ }
+
+ return MUSIC_OK;
+}
+
+
+// A chunk at a time: every playing voice into 32-bit sums, then added to
+// out and clipped.
+void waveSoftRender(WaveSoftT *soft, int16_t *out, uint32_t frames, uint32_t channels) {
+ int32_t acc[RENDER_CHUNK * STEREO];
+
+ for (uint32_t done = 0; done < frames;) {
+ uint32_t n = frames - done < RENDER_CHUNK ? frames - done : RENDER_CHUNK;
+
+ memset(acc, 0, sizeof(acc));
+
+ for (int32_t i = 0; i < WAVE_VOICES_MAX; i++) {
+ WaveSoftVoiceT *v = &soft->voices[i];
+
+ for (uint32_t f = 0; f < n && v->playing; f++) {
+ int32_t s0 = soft->arena[v->pos];
+ int32_t s1 = soft->arena[v->pos + 1];
+ int32_t s = s0 + (((s1 - s0) * (int32_t)(v->frac >> FRAC_HALF_SHIFT)) >> (FIX_SHIFT - FRAC_HALF_SHIFT));
+
+ acc[f * STEREO] += (s * v->gainL) >> WAVE_SOFT_GAIN_SHIFT;
+ acc[f * STEREO + 1] += (s * v->gainR) >> WAVE_SOFT_GAIN_SHIFT;
+ v->frac += v->step & FIX_MASK;
+ v->pos += (v->step >> FIX_SHIFT) + (v->frac >> FIX_SHIFT);
+ v->frac &= FIX_MASK;
+
+ if (v->loop != AUDIO_WT_LOOP_NONE && v->pos >= v->loopEnd) {
+ v->pos -= v->loopEnd - v->loopStart;
+ } else if (v->pos >= v->end) {
+ v->playing = false;
+ }
+ }
+ }
+
+ for (uint32_t f = 0; f < n; f++) {
+ for (uint32_t c = 0; c < channels; c++) {
+ int16_t *o = &out[(done + f) * channels + c];
+ int32_t sum = *o + (channels > 1 ? acc[f * STEREO + c] : (acc[f * STEREO] + acc[f * STEREO + 1]) / STEREO);
+
+ *o = (int16_t)(sum > SAMPLE_MAX ? SAMPLE_MAX : (sum < SAMPLE_MIN ? SAMPLE_MIN : sum));
+ }
+ }
+
+ done += n;
+ }
+}
diff --git a/src/libs/kpunch/music/musicXmp.c b/src/libs/kpunch/music/musicXmp.c
index 95104843..b092866f 100644
--- a/src/libs/kpunch/music/musicXmp.c
+++ b/src/libs/kpunch/music/musicXmp.c
@@ -22,11 +22,15 @@
// musicXmp.c -- tracker modules (MOD, S3M, XM, IT) through libxmp-lite
//
-// libxmp-lite (thirdparty/libxmp-lite, unmodified) loads the module from
-// memory and renders 16-bit frames at the stream's rate with linear
-// interpolation; the stream's fill function calls xmpRender at program
-// time (libxmp uses floating point). xmp_play_buffer's loop count ends
-// a song that does not loop after one play.
+// libxmp-lite (thirdparty/libxmp-lite, with DVX's patches) loads the
+// module from memory and renders 16-bit frames at the stream's rate with
+// linear interpolation; the stream's fill function calls xmpRender at
+// program time (libxmp uses floating point). xmp_play_buffer's loop
+// count ends a song that does not loop after one play. On a wavetable
+// synthesizer (musicModHw.c) libxmp mixes nothing instead and reports its
+// voices as every frame starts (xmpHwOn, the hardware voice patch); the
+// loop count ends a song there the same way. This is the only file that
+// includes libxmp's header.
#include "musicInt.h"
#include "xmp.h"
@@ -43,6 +47,10 @@
struct XmpPlayerT {
xmp_context ctx;
uint32_t channels;
+ uint32_t rate;
+ int32_t voices; // libxmp's voices before xmpHwOn
+ XmpHwFnT hwFn; // NULL: rendering
+ void *hwUser;
char title[MUSIC_TITLE_MAX];
};
@@ -50,16 +58,38 @@ struct XmpPlayerT {
// Prototypes
// ============================================================
+uint32_t xmpBidiAdjust(XmpPlayerT *player);
+uint32_t xmpChannels(XmpPlayerT *player);
void xmpClose(XmpPlayerT *player);
+bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us);
+static void xmpHwHook(void *user, const struct xmp_hw_voice *v);
+void xmpHwOff(XmpPlayerT *player);
+bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user);
uint32_t xmpLength(XmpPlayerT *player);
int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out);
uint32_t xmpPosition(XmpPlayerT *player);
bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop);
void xmpRestart(XmpPlayerT *player);
+bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out);
+uint32_t xmpSampleCount(XmpPlayerT *player);
void xmpSeek(XmpPlayerT *player, uint32_t ms);
+static bool xmpStart(XmpPlayerT *player);
const char *xmpTitle(XmpPlayerT *player);
+uint32_t xmpBidiAdjust(XmpPlayerT *player) {
+ return (uint32_t)xmp_hw_bidir_adjust(player->ctx);
+}
+
+
+uint32_t xmpChannels(XmpPlayerT *player) {
+ struct xmp_module_info mi;
+
+ xmp_get_module_info(player->ctx, &mi);
+ return mi.mod ? (uint32_t)mi.mod->chn : 0;
+}
+
+
void xmpClose(XmpPlayerT *player) {
if (!player) {
return;
@@ -72,6 +102,75 @@ void xmpClose(XmpPlayerT *player) {
}
+bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) {
+ struct xmp_frame_info fi;
+
+ int rc = xmp_play_frame(player->ctx);
+
+ xmp_get_frame_info(player->ctx, &fi);
+ *us = fi.frame_time > 0 ? (uint32_t)fi.frame_time : 0;
+ return rc == 0 && (loop || fi.loop_count == 0);
+}
+
+
+// libxmp's voice as XmpHwVoiceT, to the music library's function.
+static void xmpHwHook(void *user, const struct xmp_hw_voice *v) {
+ XmpPlayerT *p = (XmpPlayerT *)user;
+ XmpHwVoiceT hv;
+
+ hv.pos = v->pos;
+ hv.rate = v->rate;
+ hv.voice = (uint32_t)v->voice;
+ hv.sample = (uint32_t)v->sample;
+ hv.trigger = v->trigger;
+ hv.loopStart = v->loop_start > 0 ? (uint32_t)v->loop_start : 0;
+ hv.loopEnd = v->loop_end > 0 ? (uint32_t)v->loop_end : 0;
+ hv.vol = v->vol;
+ hv.pan = v->pan;
+ hv.loop = v->loop == XMP_HW_LOOP_BIDIR ? XMP_VOICE_LOOP_PINGPONG : (v->loop == XMP_HW_LOOP_FORWARD ? XMP_VOICE_LOOP_FORWARD : XMP_VOICE_LOOP_NONE);
+ hv.surround = v->surround != 0;
+ hv.reverse = v->reverse != 0;
+ p->hwFn(p->hwUser, &hv);
+}
+
+
+// Back to rendering, where the song was.
+void xmpHwOff(XmpPlayerT *player) {
+ uint32_t ms = xmpPosition(player);
+
+ player->hwFn = NULL;
+ xmp_set_hw_voices(player->ctx, NULL, NULL);
+ xmp_end_player(player->ctx);
+ xmp_set_player(player->ctx, XMP_PLAYER_VOICES, player->voices);
+ xmpStart(player);
+ xmpSeek(player, ms);
+}
+
+
+// Started again (where the song was) with the voices and the hook: the
+// voice count is set before libxmp starts playing.
+bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user) {
+ uint32_t ms = xmpPosition(player);
+
+ player->hwFn = fn;
+ player->hwUser = user;
+ xmp_end_player(player->ctx);
+ xmp_set_player(player->ctx, XMP_PLAYER_VOICES, (int)voices);
+ xmp_set_hw_voices(player->ctx, xmpHwHook, player);
+
+ if (!xmpStart(player)) {
+ xmpHwOff(player);
+ return false;
+ }
+
+ // Nothing is mixed: no interpolation spares libxmp copying each
+ // loop's ends at every turn of it
+ xmp_set_player(player->ctx, XMP_PLAYER_INTERP, XMP_INTERP_NEAREST);
+ xmpSeek(player, ms);
+ return true;
+}
+
+
uint32_t xmpLength(XmpPlayerT *player) {
struct xmp_frame_info fi;
@@ -91,6 +190,7 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan
p->ctx = xmp_create_context();
p->channels = channels;
+ p->rate = rate;
if (!p->ctx) {
free(p);
@@ -112,14 +212,14 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan
return rc == -XMP_ERROR_SYSTEM ? MUSIC_ERR_NO_MEMORY : MUSIC_ERR_CORRUPT;
}
- if (xmp_start_player(p->ctx, (int)rate, channels == 1 ? XMP_FORMAT_MONO : 0) != 0) {
+ if (!xmpStart(p)) {
xmp_release_module(p->ctx);
xmp_free_context(p->ctx);
free(p);
return MUSIC_ERR_NO_MEMORY;
}
- xmp_set_player(p->ctx, XMP_PLAYER_INTERP, XMP_INTERP_LINEAR);
+ p->voices = xmp_get_player(p->ctx, XMP_PLAYER_VOICES);
struct xmp_module_info mi;
@@ -153,12 +253,59 @@ void xmpRestart(XmpPlayerT *player) {
}
+bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) {
+ struct xmp_module_info mi;
+
+ xmp_get_module_info(player->ctx, &mi);
+
+ if (!mi.mod || i >= (uint32_t)mi.mod->smp) {
+ return false;
+ }
+
+ const struct xmp_sample *x = &mi.mod->xxs[i];
+
+ if (!x->data || x->len <= 0 || (x->flg & XMP_SAMPLE_SYNTH)) {
+ return false;
+ }
+
+ out->data = x->data;
+ out->frames = (uint32_t)x->len;
+ out->loopStart = x->lps > 0 ? (uint32_t)x->lps : 0;
+ out->loopEnd = x->lpe > x->lps && x->lpe <= x->len ? (uint32_t)x->lpe : out->loopStart;
+ out->bits16 = (x->flg & XMP_SAMPLE_16BIT) != 0;
+ out->loops = (x->flg & XMP_SAMPLE_LOOP) && out->loopEnd > out->loopStart;
+ out->bidi = out->loops && (x->flg & XMP_SAMPLE_LOOP_BIDIR);
+ out->stereo = (x->flg & XMP_SAMPLE_STEREO) != 0;
+ return true;
+}
+
+
+uint32_t xmpSampleCount(XmpPlayerT *player) {
+ struct xmp_module_info mi;
+
+ xmp_get_module_info(player->ctx, &mi);
+ return mi.mod ? (uint32_t)mi.mod->smp : 0;
+}
+
+
void xmpSeek(XmpPlayerT *player, uint32_t ms) {
xmp_seek_time(player->ctx, (int)ms);
xmp_play_buffer(player->ctx, NULL, 0, 0);
}
+// The player started at the stream's rate and format, linear
+// interpolation (what xmp_start_player resets is set again).
+static bool xmpStart(XmpPlayerT *player) {
+ if (xmp_start_player(player->ctx, (int)player->rate, player->channels == 1 ? XMP_FORMAT_MONO : 0) != 0) {
+ return false;
+ }
+
+ xmp_set_player(player->ctx, XMP_PLAYER_INTERP, XMP_INTERP_LINEAR);
+ return true;
+}
+
+
const char *xmpTitle(XmpPlayerT *player) {
return player->title;
}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
index 0f51b70f..f33df121 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
@@ -6,9 +6,9 @@
| Version | 4.7.3 (released 2026-09-11) |
| Download | https://github.com/libxmp/libxmp/releases/download/libxmp-4.7.3/libxmp-lite-4.7.3.tar.gz |
| Tarball SHA-256 | e199da3f7552f5ac688a091b8d28ea3c9ffd2beb845d15aac17d4d264f851d71 (262,671 bytes) |
-| Files kept | src/*.c, src/*.h, src/loaders/*.c, src/loaders/*.h, include/libxmp-lite/xmp.h (as include/xmp.h), README (it holds the licence), test/test.it and test/test.xm (used by the host tests) -- byte for byte as in the tarball. Not kept: the build files (configure, CMake, the Makefiles, including src/Makefile and src/loaders/Makefile), jni, the rest of test/. |
+| Files kept | src/*.c, src/*.h, src/loaders/*.c, src/loaders/*.h, include/libxmp-lite/xmp.h (as include/xmp.h), README (it holds the licence), test/test.it and test/test.xm (used by the host tests) -- byte for byte as in the tarball but for the patches below. Not kept: the build files (configure, CMake, the Makefiles, including src/Makefile and src/loaders/Makefile), jni, the rest of test/. |
| Licence | MIT ("Extended Module Player Lite, Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr", in README). Keep the notice with the source; reproduced in the music library's developer help (topic lib.music) for binaries. |
-| Patches | None |
+| Patches | dvx-patches/0001-hardware-voices.patch (see Patches) |
## Build options
@@ -27,11 +27,38 @@ loaded from memory (`xmp_load_module_from_memory`); `src/filetype.c`
(DJGPP branch) calls `_chmod`, which the DVX loader exports for it
(dvxPlatformDos.c), so the file is built unchanged.
+## Patches
+
+Each is a unified diff in `dvx-patches/`, applied from this folder with
+`patch -p1 < dvx-patches/`; every change is marked "DVX
+patch" in the sources.
+
+- `0001-hardware-voices.patch` -- `xmp_set_hw_voices(ctx, fn, user)`
+ (include/xmp.h, src/control.c, src/common.h): with a hook set, the
+ software mixer (src/mixer.c) mixes nothing and renders nothing, but
+ keeps every voice's position, loops, and sample ends as it would; as
+ each frame starts it calls the hook once for every voice sounding
+ (`struct xmp_hw_voice`: the sample, its position, its rate in frames a
+ second, its volume 0..1024, pan, whether it plays backwards, the loop
+ it is in (none, forward, or ping-pong, `XMP_HW_LOOP_*`) and that
+ loop's bounds, and `trigger`, a count src/mixer.h's `hw_trigger` keeps
+ of the times libxmp started or moved it, from `libxmp_mixer_voicepos`).
+ With the hook set the voices only move on (`hw_advance`, src/mixer.c:
+ to each loop or sample end at once and whole turns of a forward loop
+ at once, with the mixer's handling of sample ends, ping-pong turns and
+ Protracker sample swaps), which keeps short loops from costing a turn
+ of the mixer's loop each. `xmp_hw_bidir_adjust(ctx)` says how many
+ frames the loaded module's ping-pong loops are shortened by on the way
+ back (Impulse Tracker's 1). music.lib (musicModHw.c) plays modules on
+ a wavetable synthesizer's voices that way.
+
## Updating
1. Download the new libxmp-lite tarball, check it against the published
release, record the version and SHA-256 here.
-2. Replace src/, include/xmp.h, README and the two test modules.
+2. Replace src/, include/xmp.h, README and the two test modules, then
+ apply each patch in dvx-patches/ (in order) and fix any that no longer
+ applies, keeping the "DVX patch" marks.
3. Rebuild music.lib; run the music host suite (`make -C src/test/music`,
`UPDATE_GOLDENS=1` if the module render hashes change after a check by
ear) and the music DOS scripts (src/test/dos/scripts/mus*.scr).
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0001-hardware-voices.patch b/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0001-hardware-voices.patch
new file mode 100644
index 00000000..9e7fa1ff
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0001-hardware-voices.patch
@@ -0,0 +1,238 @@
+--- a/include/xmp.h
++++ b/include/xmp.h
+@@ -343,6 +343,31 @@
+ struct xmp_channel_info channel_info[XMP_MAX_CHANNELS]; /* Current channel information */
+ };
+
++/* DVX patch (hardware voices): with a hook set, the software mixer mixes
++ * nothing; it keeps every voice's position, loops and sample ends as it
++ * would, and as each frame starts reports each voice sounding, for a
++ * caller that plays them on a synthesizer's own voices. */
++struct xmp_hw_voice {
++ int voice; /* Mixer voice */
++ int sample; /* Module sample (xxs index) */
++ unsigned int trigger; /* Changes when the voice starts or moves */
++ double pos; /* Sample frames at the frame's start */
++ double rate; /* Sample frames per second */
++ int vol; /* Volume, 0 to 1024 */
++ int pan; /* Pan, -128 (left) to 127 (right) */
++ int surround; /* Surround (pan meaningless) */
++ int reverse; /* Playing backwards (a ping-pong loop) */
++ int loop; /* The loop it is in: XMP_HW_LOOP_* */
++ int loop_start; /* ... its first frame */
++ int loop_end; /* ... one past its last (no loop: the end) */
++};
++
++#define XMP_HW_LOOP_NONE 0 /* DVX patch: plays to loop_end and stops */
++#define XMP_HW_LOOP_FORWARD 1
++#define XMP_HW_LOOP_BIDIR 2 /* ping-pong (see xmp_hw_bidir_adjust) */
++
++typedef void (*xmp_hw_voice_fn)(void *, const struct xmp_hw_voice *);
++
+ struct xmp_callbacks {
+ unsigned long (*read_func)(void *dest, unsigned long len,
+ unsigned long nmemb, void *priv);
+@@ -397,6 +422,8 @@
+ LIBXMP_EXPORT int xmp_channel_mute (xmp_context, int, int);
+ LIBXMP_EXPORT int xmp_channel_vol (xmp_context, int, int);
+ LIBXMP_EXPORT int xmp_set_player (xmp_context, int, int);
++LIBXMP_EXPORT int xmp_set_hw_voices (xmp_context, xmp_hw_voice_fn, void *); /* DVX patch */
++LIBXMP_EXPORT int xmp_hw_bidir_adjust (xmp_context); /* DVX patch */
+ LIBXMP_EXPORT int xmp_get_player (xmp_context, int);
+ LIBXMP_EXPORT int xmp_set_instrument_path (xmp_context, const char *);
+
+--- a/src/common.h
++++ b/src/common.h
+@@ -670,6 +670,8 @@
+ int dtleft; /* anticlick control, left channel */
+ int bidir_adjust; /* adjustment for IT bidirectional loops */
+ double pbase; /* period base */
++ xmp_hw_voice_fn hw_voice; /* DVX patch: hardware voices (NULL: mix) */
++ void *hw_user; /* DVX patch: its argument */
+ };
+
+ struct rng_state {
+--- a/src/control.c
++++ b/src/control.c
+@@ -412,6 +412,33 @@
+ #define xmp_set_player__ xmp_set_player
+ #endif
+
++/* DVX patch: hardware voices (see xmp.h); a NULL fn mixes again. */
++int xmp_set_hw_voices(xmp_context opaque, xmp_hw_voice_fn fn, void *user)
++{
++ struct context_data *ctx = (struct context_data *)opaque;
++ struct mixer_data *s = &ctx->s;
++
++ s->hw_voice = fn;
++ s->hw_user = user;
++ return 0;
++}
++
++/* DVX patch: the frames a ping-pong loop's way back is shortened by in
++ * the loaded module (Impulse Tracker's 1, else 0): a loop of n frames
++ * takes 2n minus this to play there and back. */
++int xmp_hw_bidir_adjust(xmp_context opaque)
++{
++#ifndef LIBXMP_CORE_DISABLE_IT
++ struct context_data *ctx = (struct context_data *)opaque;
++ struct module_data *m = &ctx->m;
++
++ return IS_PLAYER_MODE_IT() ? 1 : 0;
++#else
++ (void)opaque;
++ return 0;
++#endif
++}
++
+ int xmp_set_player__(xmp_context opaque, int parm, int val)
+ {
+ struct context_data *ctx = (struct context_data *)opaque;
+--- a/src/mixer.c
++++ b/src/mixer.c
+@@ -421,6 +421,67 @@
+ adjust_voice_end(ctx, vi, *xxs, *xtra);
+ }
+
++/* DVX patch: hardware voices: the voice moved on by frames sample frames
++ * as the software mixer's loop moves it, with nothing mixed: to each loop
++ * or sample end at once, whole turns of a forward loop at once. */
++static void hw_advance(struct context_data *ctx, int voc, struct mixer_voice *vi,
++ struct xmp_sample **xxs, struct extra_sample_data **xtra,
++ int *c5spd, double frames)
++{
++ struct player_data *p = &ctx->p;
++ struct module_data *m = &ctx->m;
++ int split_noloop = p->xc_data[vi->chn].split != 0;
++ int stuck = 0;
++
++ while (frames > 0) {
++ int reverse = (vi->flags & VOICE_REVERSE) != 0;
++ double room = reverse ? vi->pos - vi->start : vi->end - vi->pos;
++
++ if (room > frames) {
++ vi->pos += reverse ? -frames : frames;
++ return;
++ }
++
++ if (room > 0) {
++ vi->pos += reverse ? -room : room;
++ frames -= room;
++ } else if (++stuck > 1) {
++ /* a loop of no length */
++ return;
++ }
++
++ /* One-shot samples do not loop. */
++ if ((!has_active_loop(ctx, vi, *xxs) || split_noloop) &&
++ !(vi->flags & SAMPLE_QUEUED)) {
++ set_sample_end(ctx, voc, 1);
++ return;
++ }
++
++ /* Protracker sample swap */
++ if (vi->flags & SAMPLE_QUEUED) {
++ if (vi->queued.smp < 0 ||
++ (!has_active_loop(ctx, vi, *xxs) &&
++ !(m->mod.xxs[vi->queued.smp].flg & XMP_SAMPLE_LOOP))) {
++ vi->flags &= ~SAMPLE_QUEUED;
++ vi->flags |= SAMPLE_PAUSED;
++ set_sample_end(ctx, voc, 1);
++ return;
++ }
++ hotswap_sample(ctx, vi, voc, vi->queued.smp);
++ get_current_sample(ctx, vi, xxs, xtra, c5spd);
++ vi->pos = vi->start;
++ continue;
++ }
++
++ if ((vi->flags & SAMPLE_LOOP) && (~vi->flags & VOICE_BIDIR) &&
++ vi->end > vi->start) {
++ frames = fmod(frames, vi->end - vi->start);
++ }
++
++ loop_reposition(ctx, vi, *xxs, *xtra);
++ }
++}
++
+ /* Calculate the required number of sample frames to render a tick.
+ * Returns -1 if any of the parameters are invalid. */
+ int libxmp_mixer_get_ticksize(int freq, double time_factor, double rrate, int bpm)
+@@ -461,6 +522,11 @@
+ s->ticksize = s->total_size / 2;
+ }
+
++ /* DVX patch: hardware voices mix nothing */
++ if (s->hw_voice != NULL) {
++ return;
++ }
++
+ bytelen = s->ticksize * sizeof(int32);
+ if (~s->format & XMP_FORMAT_MONO) {
+ bytelen *= 2;
+@@ -592,6 +658,35 @@
+
+ init_sample_wraparound(s, &loop_data, vi, xxs);
+
++ /* DVX patch: the voice as the frame starts, for the caller's
++ * synthesizer (one that ends in the frame plays out there) */
++ if (s->hw_voice != NULL && (vi->fidx & FLAG_ACTIVE)) {
++ struct xmp_hw_voice hv;
++
++ hv.voice = voc;
++ hv.sample = vi->smp;
++ hv.trigger = vi->hw_trigger;
++ hv.pos = vi->pos;
++ hv.rate = step * s->freq;
++ hv.vol = vol;
++ hv.pan = vi->pan == PAN_SURROUND ? 0 : vi->pan;
++ hv.surround = vi->pan == PAN_SURROUND;
++ hv.reverse = (vi->flags & VOICE_REVERSE) != 0;
++ hv.loop = !has_active_loop(ctx, vi, xxs) ? XMP_HW_LOOP_NONE : (vi->flags & VOICE_BIDIR) ? XMP_HW_LOOP_BIDIR : XMP_HW_LOOP_FORWARD;
++ hv.loop_start = vi->start;
++ hv.loop_end = vi->end;
++ s->hw_voice(s->hw_user, &hv);
++ }
++
++ /* DVX patch: hardware voices only move on */
++ if (s->hw_voice != NULL) {
++ reset_sample_wraparound(&loop_data);
++ hw_advance(ctx, voc, vi, &xxs, &xtra, &c5spd, step * s->ticksize);
++ vi->old_vl = vol_l;
++ vi->old_vr = vol_r;
++ continue;
++ }
++
+ rampsize = s->ticksize >> ANTICLICK_SHIFT;
+ delta_l = (vol_l - vi->old_vl) / rampsize;
+ delta_r = (vol_r - vi->old_vr) / rampsize;
+@@ -761,6 +856,11 @@
+ vi->old_vr = vol_r;
+ }
+
++ /* DVX patch: hardware voices render nothing */
++ if (s->hw_voice != NULL) {
++ return;
++ }
++
+ /* Render final frame */
+
+ size = s->ticksize;
+@@ -819,6 +919,7 @@
+ }
+
+ vi->pos = pos;
++ vi->hw_trigger++; /* DVX patch */
+
+ adjust_voice_end(ctx, vi, xxs, xtra);
+
+--- a/src/mixer.h
++++ b/src/mixer.h
+@@ -46,6 +46,7 @@
+ #define SAMPLE_PAUSED (1 << 6)
+ int flags; /* flags */
+ void *sptr; /* sample pointer */
++ unsigned int hw_trigger; /* DVX patch: counts starts and moves */
+ #ifdef LIBXMP_PAULA_SIMULATOR
+ struct paula_state *paula; /* paula simulation state */
+ #endif
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
index 9f7aa781..65d7fef4 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
@@ -343,6 +343,31 @@ struct xmp_frame_info { /* Current frame information */
struct xmp_channel_info channel_info[XMP_MAX_CHANNELS]; /* Current channel information */
};
+/* DVX patch (hardware voices): with a hook set, the software mixer mixes
+ * nothing; it keeps every voice's position, loops and sample ends as it
+ * would, and as each frame starts reports each voice sounding, for a
+ * caller that plays them on a synthesizer's own voices. */
+struct xmp_hw_voice {
+ int voice; /* Mixer voice */
+ int sample; /* Module sample (xxs index) */
+ unsigned int trigger; /* Changes when the voice starts or moves */
+ double pos; /* Sample frames at the frame's start */
+ double rate; /* Sample frames per second */
+ int vol; /* Volume, 0 to 1024 */
+ int pan; /* Pan, -128 (left) to 127 (right) */
+ int surround; /* Surround (pan meaningless) */
+ int reverse; /* Playing backwards (a ping-pong loop) */
+ int loop; /* The loop it is in: XMP_HW_LOOP_* */
+ int loop_start; /* ... its first frame */
+ int loop_end; /* ... one past its last (no loop: the end) */
+};
+
+#define XMP_HW_LOOP_NONE 0 /* DVX patch: plays to loop_end and stops */
+#define XMP_HW_LOOP_FORWARD 1
+#define XMP_HW_LOOP_BIDIR 2 /* ping-pong (see xmp_hw_bidir_adjust) */
+
+typedef void (*xmp_hw_voice_fn)(void *, const struct xmp_hw_voice *);
+
struct xmp_callbacks {
unsigned long (*read_func)(void *dest, unsigned long len,
unsigned long nmemb, void *priv);
@@ -397,6 +422,8 @@ LIBXMP_EXPORT int xmp_seek_time_frame (xmp_context, int);
LIBXMP_EXPORT int xmp_channel_mute (xmp_context, int, int);
LIBXMP_EXPORT int xmp_channel_vol (xmp_context, int, int);
LIBXMP_EXPORT int xmp_set_player (xmp_context, int, int);
+LIBXMP_EXPORT int xmp_set_hw_voices (xmp_context, xmp_hw_voice_fn, void *); /* DVX patch */
+LIBXMP_EXPORT int xmp_hw_bidir_adjust (xmp_context); /* DVX patch */
LIBXMP_EXPORT int xmp_get_player (xmp_context, int);
LIBXMP_EXPORT int xmp_set_instrument_path (xmp_context, const char *);
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
index 324ed65e..042bafaf 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
@@ -670,6 +670,8 @@ struct mixer_data {
int dtleft; /* anticlick control, left channel */
int bidir_adjust; /* adjustment for IT bidirectional loops */
double pbase; /* period base */
+ xmp_hw_voice_fn hw_voice; /* DVX patch: hardware voices (NULL: mix) */
+ void *hw_user; /* DVX patch: its argument */
};
struct rng_state {
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
index bcbaf370..9412de9b 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
@@ -412,6 +412,33 @@ LIBXMP_END_DECLS
#define xmp_set_player__ xmp_set_player
#endif
+/* DVX patch: hardware voices (see xmp.h); a NULL fn mixes again. */
+int xmp_set_hw_voices(xmp_context opaque, xmp_hw_voice_fn fn, void *user)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct mixer_data *s = &ctx->s;
+
+ s->hw_voice = fn;
+ s->hw_user = user;
+ return 0;
+}
+
+/* DVX patch: the frames a ping-pong loop's way back is shortened by in
+ * the loaded module (Impulse Tracker's 1, else 0): a loop of n frames
+ * takes 2n minus this to play there and back. */
+int xmp_hw_bidir_adjust(xmp_context opaque)
+{
+#ifndef LIBXMP_CORE_DISABLE_IT
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+
+ return IS_PLAYER_MODE_IT() ? 1 : 0;
+#else
+ (void)opaque;
+ return 0;
+#endif
+}
+
int xmp_set_player__(xmp_context opaque, int parm, int val)
{
struct context_data *ctx = (struct context_data *)opaque;
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.c
index 804f5021..6f5ee819 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.c
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.c
@@ -421,6 +421,67 @@ static void get_current_sample(struct context_data *ctx, struct mixer_voice *vi,
adjust_voice_end(ctx, vi, *xxs, *xtra);
}
+/* DVX patch: hardware voices: the voice moved on by frames sample frames
+ * as the software mixer's loop moves it, with nothing mixed: to each loop
+ * or sample end at once, whole turns of a forward loop at once. */
+static void hw_advance(struct context_data *ctx, int voc, struct mixer_voice *vi,
+ struct xmp_sample **xxs, struct extra_sample_data **xtra,
+ int *c5spd, double frames)
+{
+ struct player_data *p = &ctx->p;
+ struct module_data *m = &ctx->m;
+ int split_noloop = p->xc_data[vi->chn].split != 0;
+ int stuck = 0;
+
+ while (frames > 0) {
+ int reverse = (vi->flags & VOICE_REVERSE) != 0;
+ double room = reverse ? vi->pos - vi->start : vi->end - vi->pos;
+
+ if (room > frames) {
+ vi->pos += reverse ? -frames : frames;
+ return;
+ }
+
+ if (room > 0) {
+ vi->pos += reverse ? -room : room;
+ frames -= room;
+ } else if (++stuck > 1) {
+ /* a loop of no length */
+ return;
+ }
+
+ /* One-shot samples do not loop. */
+ if ((!has_active_loop(ctx, vi, *xxs) || split_noloop) &&
+ !(vi->flags & SAMPLE_QUEUED)) {
+ set_sample_end(ctx, voc, 1);
+ return;
+ }
+
+ /* Protracker sample swap */
+ if (vi->flags & SAMPLE_QUEUED) {
+ if (vi->queued.smp < 0 ||
+ (!has_active_loop(ctx, vi, *xxs) &&
+ !(m->mod.xxs[vi->queued.smp].flg & XMP_SAMPLE_LOOP))) {
+ vi->flags &= ~SAMPLE_QUEUED;
+ vi->flags |= SAMPLE_PAUSED;
+ set_sample_end(ctx, voc, 1);
+ return;
+ }
+ hotswap_sample(ctx, vi, voc, vi->queued.smp);
+ get_current_sample(ctx, vi, xxs, xtra, c5spd);
+ vi->pos = vi->start;
+ continue;
+ }
+
+ if ((vi->flags & SAMPLE_LOOP) && (~vi->flags & VOICE_BIDIR) &&
+ vi->end > vi->start) {
+ frames = fmod(frames, vi->end - vi->start);
+ }
+
+ loop_reposition(ctx, vi, *xxs, *xtra);
+ }
+}
+
/* Calculate the required number of sample frames to render a tick.
* Returns -1 if any of the parameters are invalid. */
int libxmp_mixer_get_ticksize(int freq, double time_factor, double rrate, int bpm)
@@ -461,6 +522,11 @@ static void libxmp_mixer_prepare(struct context_data *ctx)
s->ticksize = s->total_size / 2;
}
+ /* DVX patch: hardware voices mix nothing */
+ if (s->hw_voice != NULL) {
+ return;
+ }
+
bytelen = s->ticksize * sizeof(int32);
if (~s->format & XMP_FORMAT_MONO) {
bytelen *= 2;
@@ -592,6 +658,35 @@ void libxmp_mixer_softmixer(struct context_data *ctx)
init_sample_wraparound(s, &loop_data, vi, xxs);
+ /* DVX patch: the voice as the frame starts, for the caller's
+ * synthesizer (one that ends in the frame plays out there) */
+ if (s->hw_voice != NULL && (vi->fidx & FLAG_ACTIVE)) {
+ struct xmp_hw_voice hv;
+
+ hv.voice = voc;
+ hv.sample = vi->smp;
+ hv.trigger = vi->hw_trigger;
+ hv.pos = vi->pos;
+ hv.rate = step * s->freq;
+ hv.vol = vol;
+ hv.pan = vi->pan == PAN_SURROUND ? 0 : vi->pan;
+ hv.surround = vi->pan == PAN_SURROUND;
+ hv.reverse = (vi->flags & VOICE_REVERSE) != 0;
+ hv.loop = !has_active_loop(ctx, vi, xxs) ? XMP_HW_LOOP_NONE : (vi->flags & VOICE_BIDIR) ? XMP_HW_LOOP_BIDIR : XMP_HW_LOOP_FORWARD;
+ hv.loop_start = vi->start;
+ hv.loop_end = vi->end;
+ s->hw_voice(s->hw_user, &hv);
+ }
+
+ /* DVX patch: hardware voices only move on */
+ if (s->hw_voice != NULL) {
+ reset_sample_wraparound(&loop_data);
+ hw_advance(ctx, voc, vi, &xxs, &xtra, &c5spd, step * s->ticksize);
+ vi->old_vl = vol_l;
+ vi->old_vr = vol_r;
+ continue;
+ }
+
rampsize = s->ticksize >> ANTICLICK_SHIFT;
delta_l = (vol_l - vi->old_vl) / rampsize;
delta_r = (vol_r - vi->old_vr) / rampsize;
@@ -761,6 +856,11 @@ void libxmp_mixer_softmixer(struct context_data *ctx)
vi->old_vr = vol_r;
}
+ /* DVX patch: hardware voices render nothing */
+ if (s->hw_voice != NULL) {
+ return;
+ }
+
/* Render final frame */
size = s->ticksize;
@@ -819,6 +919,7 @@ void libxmp_mixer_voicepos(struct context_data *ctx, int voc, double pos, int ac
}
vi->pos = pos;
+ vi->hw_trigger++; /* DVX patch */
adjust_voice_end(ctx, vi, xxs, xtra);
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.h
index a582b1f0..6f09dd17 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.h
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/mixer.h
@@ -46,6 +46,7 @@ struct mixer_voice {
#define SAMPLE_PAUSED (1 << 6)
int flags; /* flags */
void *sptr; /* sample pointer */
+ unsigned int hw_trigger; /* DVX patch: counts starts and moves */
#ifdef LIBXMP_PAULA_SIMULATOR
struct paula_state *paula; /* paula simulation state */
#endif
diff --git a/src/libs/kpunch/pkg/pkg.dhs b/src/libs/kpunch/pkg/pkg.dhs
index 3919fda2..b66fa399 100644
--- a/src/libs/kpunch/pkg/pkg.dhs
+++ b/src/libs/kpunch/pkg/pkg.dhs
@@ -45,7 +45,7 @@ Header: pkg/pkg.h (it includes the others below)
Loaded as: bin/libs/kpunch/pkg/pkg.lib
-BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys).
+BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys; PkgFileOwner$ is pkgDbOwner).
.h2 A Package
@@ -57,7 +57,7 @@ format = 1
id = acme.chess ; vendor.name, each 1..8 of a-z 0-9 _
name = Chess
version = 1.2.0
-kind = app ; app library widget font bank theme wallpaper sound help bundle system
+kind = app ; app library widget font bank theme wallpaper sound saver help bundle system
summary = Play chess against the computer.
publisher = Acme Games ; also author, licence, homepage
dvx = 1.0.0 ; the oldest DVX it runs on
@@ -78,6 +78,7 @@ Every file is listed with its size and SHA-256 and every listed file is in the p
font, bank SHARED/FONTS/VENDOR, SHARED/BANKS/VENDOR
theme, wallpaper SHARED/THEMES/VENDOR, SHARED/WPAPER/VENDOR
sound SHARED/SOUNDS/VENDOR
+ saver SHARED/SAVERS/VENDOR (a screen saver: a program, so it needs abi)
bundle no files: a set of packages, its dependencies
system DVX itself (kpunch.dvx): anywhere but the user folder
.endtable
diff --git a/src/libs/kpunch/pkg/pkgBas.c b/src/libs/kpunch/pkg/pkgBas.c
index 9c1d35a2..96ef0eb0 100644
--- a/src/libs/kpunch/pkg/pkgBas.c
+++ b/src/libs/kpunch/pkg/pkgBas.c
@@ -99,6 +99,7 @@ int32_t PkgDvxAbi(void);
const char *PkgDvxVersion(void);
const char *PkgError(int32_t s);
const char *PkgField(int32_t s, const char *id, int32_t field);
+const char *PkgFileOwner(int32_t s, const char *fileName);
const char *PkgFilesRows(int32_t s, const char *id);
const char *PkgFolderRows(int32_t s, const char *folder, const char *prefix);
const char *PkgIdPart(const char *text);
@@ -512,6 +513,16 @@ const char *PkgField(int32_t s, const char *id, int32_t field) {
}
+// The id of the installed package that put fileName there (relative to
+// the DVX folder, either separator, any case), or "".
+const char *PkgFileOwner(int32_t s, const char *fileName) {
+ SessionT *ss = sessionOf(s);
+ const PkgInstalledT *owner = ss && fileName ? pkgDbOwner(&ss->db, fileName, NULL) : NULL;
+
+ return basTextSet(&sText, owner ? owner->manifest.id : "");
+}
+
+
// The files of the installed package id. Rows: name, size.
const char *PkgFilesRows(int32_t s, const char *id) {
SessionT *ss = sessionOf(s);
diff --git a/src/libs/kpunch/pkg/pkgBas.h b/src/libs/kpunch/pkg/pkgBas.h
index 1b2eb354..2c73bd94 100644
--- a/src/libs/kpunch/pkg/pkgBas.h
+++ b/src/libs/kpunch/pkg/pkgBas.h
@@ -67,6 +67,7 @@ int32_t PkgDvxAbi(void);
const char *PkgDvxVersion(void);
const char *PkgError(int32_t s);
const char *PkgField(int32_t s, const char *id, int32_t field);
+const char *PkgFileOwner(int32_t s, const char *fileName);
const char *PkgFilesRows(int32_t s, const char *id);
const char *PkgFolderRows(int32_t s, const char *folder, const char *prefix);
const char *PkgIdPart(const char *text);
diff --git a/src/libs/kpunch/pkg/pkgCli.c b/src/libs/kpunch/pkg/pkgCli.c
index fc05ca50..9fef22f3 100644
--- a/src/libs/kpunch/pkg/pkgCli.c
+++ b/src/libs/kpunch/pkg/pkgCli.c
@@ -83,7 +83,7 @@ static int cmdVerify(int argc, char *argv[]);
static bool loadKey(const char *path, PkgKeyT *key);
static bool loadSecret(const char *path, PkgSecretT *secret);
static bool loadSig(const char *path, PkgSigT *sig);
-static bool parseOpts(int argc, char *argv[], CliOptsT *o);
+static bool parseOpts(int argc, char *const argv[], CliOptsT *o);
int pkgCliRun(int argc, char *argv[], PkgRandomFnT random, bool *handled);
void pkgCliUsage(FILE *f);
static int runOp(PkgOpT *op, const char *name);
@@ -516,7 +516,7 @@ static bool loadSig(const char *path, PkgSigT *sig) {
// install / remove / list's options; false for anything else (or a second
// argument).
-static bool parseOpts(int argc, char *argv[], CliOptsT *o) {
+static bool parseOpts(int argc, char *const argv[], CliOptsT *o) {
memset(o, 0, sizeof(*o));
o->root = "";
diff --git a/src/libs/kpunch/pkg/pkgDpk.c b/src/libs/kpunch/pkg/pkgDpk.c
index 0459b591..1cfc3c68 100644
--- a/src/libs/kpunch/pkg/pkgDpk.c
+++ b/src/libs/kpunch/pkg/pkgDpk.c
@@ -271,7 +271,7 @@ bool pkgDpkCheckFiles(const char *path, const PkgDpkInfoT *info, char *err, int3
PkgZipStepE step = PkgZipFailE;
if (!ok) {
- ok = fail(err, errSize, "Cannot read %s.", path);
+ fail(err, errSize, "Cannot read %s.", path);
}
while (ok && (step = pkgZipNext(zip, &e)) == PkgZipEntryE) {
@@ -442,7 +442,11 @@ bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char
pkgSign(secret, (const uint8_t *)info.text, info.textLen, &sig);
sigText = pkgSigFormat(&sig, &sigLen);
- ok = sigText != NULL || fail(err, errSize, "There is not enough memory.");
+ ok = sigText != NULL;
+
+ if (!ok) {
+ fail(err, errSize, "There is not enough memory.");
+ }
if (ok && !siblingPath(path, detached ? PKG_SIG_EXT : TEMP_EXT, other, sizeof(other))) {
ok = fail(err, errSize, "The path of %s is too long.", path);
diff --git a/src/libs/kpunch/pkg/pkgManifest.c b/src/libs/kpunch/pkg/pkgManifest.c
index 91e28cd0..6bc36a75 100644
--- a/src/libs/kpunch/pkg/pkgManifest.c
+++ b/src/libs/kpunch/pkg/pkgManifest.c
@@ -92,7 +92,7 @@ static const char *const sPackageKeys[KeyCountE] = { "format", "id", "name", "ve
#define REQUIRED_KEYS ((1u << KeyFormatE) | (1u << KeyIdE) | (1u << KeyNameE) | (1u << KeyVersionE) | (1u << KeyKindE))
// Kind names (PkgKindE order)
-static const char *const sKindNames[PkgKindCountE] = { "app", "library", "widget", "font", "bank", "theme", "wallpaper", "sound", "help", "bundle", "system" };
+static const char *const sKindNames[PkgKindCountE] = { "app", "library", "widget", "font", "bank", "theme", "wallpaper", "sound", "saver", "help", "bundle", "system" };
// Each kind's folders; help goes in its program's folder, or beside a
// library or widget of its name (pkgFileAllowed); bundle has none; system
@@ -106,6 +106,7 @@ static const KindFolderT sKindFolders[PkgKindCountE] = {
[PkgKindThemeE] = { DVX_SHARED_DIR "/" DVX_THEMES_DIR, false },
[PkgKindWallpaperE] = { DVX_SHARED_DIR "/" DVX_WPAPER_DIR, false },
[PkgKindSoundE] = { DVX_SHARED_DIR "/" DVX_SOUNDS_DIR, false },
+ [PkgKindSaverE] = { DVX_SHARED_DIR "/" DVX_SAVERS_DIR, false },
[PkgKindHelpE] = { DVX_APPS_DIR, true },
[PkgKindBundleE] = { NULL, false },
[PkgKindSystemE] = { NULL, false }
@@ -485,7 +486,7 @@ bool pkgKindFolder(PkgKindE kind, const char *id, char *out, int32_t outSize) {
bool pkgKindIsCode(PkgKindE kind) {
- return kind == PkgKindAppE || kind == PkgKindLibraryE || kind == PkgKindWidgetE;
+ return kind == PkgKindAppE || kind == PkgKindLibraryE || kind == PkgKindWidgetE || kind == PkgKindSaverE;
}
diff --git a/src/libs/kpunch/pkg/pkgManifest.h b/src/libs/kpunch/pkg/pkgManifest.h
index 26603482..c575f0d8 100644
--- a/src/libs/kpunch/pkg/pkgManifest.h
+++ b/src/libs/kpunch/pkg/pkgManifest.h
@@ -90,6 +90,7 @@ typedef enum {
PkgKindThemeE,
PkgKindWallpaperE,
PkgKindSoundE,
+ PkgKindSaverE, // screen savers (programs in SAVERS)
PkgKindHelpE,
PkgKindBundleE, // no files: a set of packages (its dependencies)
PkgKindSystemE, // DVX itself
diff --git a/src/libs/kpunch/pkg/pkgOp.c b/src/libs/kpunch/pkg/pkgOp.c
index 11e51d3c..ca7029b9 100644
--- a/src/libs/kpunch/pkg/pkgOp.c
+++ b/src/libs/kpunch/pkg/pkgOp.c
@@ -366,7 +366,7 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool
// Every new file is unpacked before any old one goes
if (platformDiskSpace(db->root[0] ? db->root : ".", &diskTotal, &diskFree) && diskFree < op->total) {
- opFail(op, "There is not enough room on the disk: %s needs %lu KB and %lu KB are free.", op->manifest.name, (unsigned long)(op->total / OP_KB + 1), (unsigned long)(diskFree / OP_KB));
+ opFail(op, "There is not enough room on the disk: %s needs %lu KB and %lu KB are free.", op->manifest.name, (unsigned long)op->total / OP_KB + 1, (unsigned long)diskFree / OP_KB);
return op;
}
diff --git a/src/libs/kpunch/pkg/pkgPlan.c b/src/libs/kpunch/pkg/pkgPlan.c
index 8c6c995e..2d8aabdc 100644
--- a/src/libs/kpunch/pkg/pkgPlan.c
+++ b/src/libs/kpunch/pkg/pkgPlan.c
@@ -133,7 +133,6 @@ static bool checkOffer(PlanT *p, int32_t i, PkgActE *act) {
const PkgOfferT *o = &p->offers[i];
const PkgManifestT *m = &o->manifest;
const PkgInstalledT *inst = pkgDbFind(p->db, m->id);
- char need[PKG_VERSION_MAX];
char have[PKG_VERSION_MAX];
if (pkgTrustIsDvxId(m->id) && o->trust != PkgTrustSystemE) {
@@ -141,6 +140,8 @@ static bool checkOffer(PlanT *p, int32_t i, PkgActE *act) {
}
if (pkgVersionCompare(m->dvx, p->dvx) > 0) {
+ char need[PKG_VERSION_MAX];
+
pkgVersionFormat(m->dvx, need, sizeof(need));
pkgVersionFormat(p->dvx, have, sizeof(have));
return fail(p, "%s needs DVX %s or later (this is DVX %s).", m->name, need, have);
@@ -287,13 +288,10 @@ bool pkgOfferRead(const char *path, const PkgKeyringT *ring, PkgOfferT *offer, c
offer->hasSigner = signer != NULL;
offer->hasNamed = signer != NULL || info.hasSig || hasDetached;
+ // The key the signature names: the trusted one (the signer too), else
+ // the package's own signature's, else the detached one's
if (signer) {
memcpy(offer->signer, signer->id, PKG_KEY_ID_SIZE);
- }
-
- // The key the signature names: the trusted one, else the package's own
- // signature's, else the detached one's
- if (signer) {
memcpy(offer->named, signer->id, PKG_KEY_ID_SIZE);
} else if (info.hasSig) {
memcpy(offer->named, info.sig.keyId, PKG_KEY_ID_SIZE);
diff --git a/src/libs/kpunch/pkgnet/pkgNet.c b/src/libs/kpunch/pkgnet/pkgNet.c
index 55c3a7a5..fe5b22ab 100644
--- a/src/libs/kpunch/pkgnet/pkgNet.c
+++ b/src/libs/kpunch/pkgnet/pkgNet.c
@@ -238,9 +238,7 @@ static void accountAnswer(PkgNetT *n, uint32_t nowMs) {
// An account's job: sign in when there is no session, else send what the
// job sends (an upload, the list of the account's versions).
static void accountNext(PkgNetT *n, uint32_t nowMs) {
- char auth[AUTH_MAX];
- char *data = NULL;
- int32_t len = 0;
+ char auth[AUTH_MAX];
if (!n->token[0]) {
loginStart(n, nowMs);
@@ -250,7 +248,8 @@ static void accountNext(PkgNetT *n, uint32_t nowMs) {
snprintf(auth, sizeof(auth), "%s%s", BEARER, n->token);
if (n->job == JobUploadE) {
- data = platformReadFile(n->upload, &len);
+ int32_t len = 0;
+ char *data = platformReadFile(n->upload, &len);
if (!data) {
failed(n, "%s cannot be read.", n->upload);
diff --git a/src/libs/kpunch/print/print.dep b/src/libs/kpunch/print/print.dep
index e064b831..5517cb67 100644
--- a/src/libs/kpunch/print/print.dep
+++ b/src/libs/kpunch/print/print.dep
@@ -1,2 +1,3 @@
libdvx
libtasks
+serial
diff --git a/src/libs/kpunch/print/print.dhs b/src/libs/kpunch/print/print.dhs
index 8578c940..dd7ceaf9 100644
--- a/src/libs/kpunch/print/print.dhs
+++ b/src/libs/kpunch/print/print.dhs
@@ -28,12 +28,21 @@
.index printJobOpen
.index printJobWrite
.index printTextJob
-.index printRasterJob
+.index printDocOpen
+.index printDocPage
+.index printDocPicture
+.index printUiDocOpen
+.index printModelPage
+.index printFontOpen
+.index PrintFontT
+.index Printer Fonts
+.index PrintDocT
+.index printVecBegin
+.index Vector Pages
.index printDialog
.index printPageSetupDialog
.index printUiText
.index PrintJobT
-.index PrintDriverT
.index INT 17h
.h1 Printing Library
@@ -46,7 +55,7 @@ Loaded as: bin/libs/kpunch/print/print.lib
.h2 Settings
-The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1, LPT2, LPT3, or a file), the printer model (PrintModelE: text only, Epson 9-pin, Epson 24-pin, HP LaserJet), and the page setup (PrintPageSetupT).
+The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1 to LPT3, COM1 to COM4, or a file), a serial port's speed and handshake (baud, PrintHandshakeE), the printer model (PrintModelE: text only; Epson 9-pin or 24-pin, each with a black or a color ribbon; HP LaserJet at 150 dpi; HP LaserJet III or DeskJet at 300 dpi; HP DeskJet 500C; HP DeskJet 550C and later; PostScript, black or color; Apple ImageWriter II, black or color), and the page setup (PrintPageSetupT).
.table
Function Description
@@ -65,7 +74,7 @@ The printer is chosen once for everything, in DVX.INI [printer] (the Control Pan
.h2 Jobs and Printer Problems
-A job sends bytes to a printer port or into a file. On DOS a port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line, or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.
+A job sends bytes to a printer port or into a file. On DOS a parallel port is driven through the BIOS printer service (INT 17h); a byte is sent only when a status check says the printer is ready, so the BIOS never waits by itself. A serial port (COM1 to COM4, for serial printers such as the ImageWriter II) is opened through the serial library when the job starts, at the speed and handshake in the settings (8 data bits, no parity, 1 stop bit); its interrupt handler sends from a buffer as the printer's handshake allows (XON / XOFF, or the printer's DTR seen on the computer's CTS, or on its DSR as Apple's cable wires it), the port is busy while the buffer is well filled, and when the job closes it waits for the buffer to empty before closing the port. A PrintPortOpsT's open, pending, and close (NULL for none) are these steps. While the printer is busy the job calls the idle hook; a printer busy for PRINT_BUSY_TIMEOUT_MS (10 seconds), out of paper, off line, or reporting an error is a problem, and so is a file that cannot be written (disk full). A problem goes to the ask hook: Retry tries again, Cancel ends the job and every later write fails at once.
.code
PrintHooksT hooks = { myAsk, myIdle, NULL, myState };
@@ -92,25 +101,122 @@ printJobWrite returns once every byte was taken, or false when the job is cancel
.h2 Text
-printTextJob(job, model, text, len, &pageSetup, &info) prints plain text on pages. Lines end with LF or CR LF; a form feed in the text starts a new page. Each printed line starts with marginLeft blanks and ends with CR LF, tabs are expanded to tabWidth, other control characters print as blanks, and a line longer than the text width is wrapped at the last blank before it (a word longer than a whole line is split) or, with wrap off, cut off. Every page ends with a form feed. For the Epson and LaserJet models the printer's reset command comes first (and, for the LaserJet, last).
+printTextJob(job, model, text, len, &pageSetup, &info) prints plain text on pages. Lines end with LF or CR LF; a form feed in the text starts a new page. Each printed line starts with marginLeft blanks and ends with CR LF, tabs are expanded to tabWidth, other control characters print as blanks, and a line longer than the text width is wrapped at the last blank before it (a word longer than a whole line is split) or, with wrap off, cut off. Every page ends with a form feed. For the Epson and LaserJet models the printer's reset command comes first (and, for the LaserJet, last). A PostScript printer prints no plain text, so on the PostScript models the pages are PostScript: Courier as large as lets the page setup's columns fill 8 inches and its lines 10, a show per line (CP437 letters and signs in ISO Latin-1; line and block characters as -, |, +, and #).
A page is marginTop lines with the header in their middle line, the text, then marginBottom lines with the footer in their middle line. Header and footer codes: &f file name (PrintTextInfoT.title), &p page number, &n number of pages, &d date, &t time (PrintTextInfoT.date / time, or today and now), && an ampersand; &l, &c, and &r put what follows at the left, centre, or right (text before any of them is centred). printFormatHeader expands a header for one page.
PrintTextInfoT also chooses a page range (firstPage, lastPage; 0 = all) and the number of copies. printTextPageCount returns the number of pages text takes.
-.h2 Pictures and Printer Drivers
+.h2 Documents: Pictures and Drawn Pages
-printRasterJob(job, driver, rgb, w, h, pitch) prints a picture of R, G, B bytes on one page: scaled to the largest size that fits the printable area (8 by 10 inches) keeping its shape, and dithered to black and white with a 4x4 ordered pattern. The driver gets one band of dot rows at a time, so only one band is ever in memory. printDriver(model) returns the driver of a model (NULL for text only).
+A document is pages of dot graphics on a picture-printing model. printDocOpen(job, model) starts one on a job (the document owns the job from then on, and printDocClose closes it). Each page is the whole printable area (8 by 10 inches): printDocPage(doc, fn, ctx) asks the program's PrintBandFnT for it a band at a time, top down, as R, G, B bytes at the printer's resolution (the band comes white), and printDocPicture(doc, rgb, w, h, pitch) prints a picture scaled to the largest size that fits, keeping its shape. Only one band of the page is ever in memory, so a page costs the same however large it is.
.table
- Model Driver
- ----- ------
- Epson 9-pin (ESC/P) 120 x 72 dots per inch (ESC * 1), 8-row bands, paper fed with ESC J; blank bands only feed paper, and each pass stops after its last black dot.
- Epson 24-pin (ESC/P) 180 x 180 dots per inch (ESC * 39, 3 bytes per column), 24-row bands.
- HP LaserJet (PCL) 150 dots per inch raster graphics, one uncompressed row transfer per dot row (every LaserJet understands it), trailing white bytes left out.
+ Function Description
+ -------- -----------
+ printModelPage(model, &info) The page a model prints on (PrintPageInfoT: dots per inch across and down, the printable area in dots, color or not, and vector: a PostScript printer, which can draw the page itself); false for text only. info may be NULL to ask only whether the model prints pictures.
+ printDocOpen(job, model) Start a document; NULL (the job closed) for a text-only model.
+ printDocInfo(doc, &info) The document's page.
+ printDocPage(doc, fn, ctx) Print a page the program draws. fn(ctx, rgb, width, y0, rows) fills rows y0 to y0 + rows - 1; false from it stops the page.
+ printDocPicture(doc, rgb, w, h, pitch) Print a picture as a page.
+ printDocCancelled(doc) True when the job was cancelled.
+ printDocClose(doc) End the document and close its job; true when every page printed. After a failure every later page fails at once.
+ printUiDocOpen(ctx, title, &choice) A document on the printer in the settings, at a Print dialog answer's destination (NULL: the settings'), with the Printing window up until it closes and the Retry box; tells the user why when it cannot open. Copies are the program's: print each page as often as wanted.
+ printUiDocClose(ctx, doc) Close it, telling the user of a failure (not of a cancel).
.endtable
-A PrintDriverT holds the resolution, the printable size in dots, the band height, the text reset strings and usual page length, and begin / band / end functions that write the printer's commands; a new printer family is one more driver.
+Each band is dithered to the printer's inks with an ordered pattern (4x4, 17 levels, on the dot-matrix printers and the 150 dpi LaserJet; 8x8, 65 levels, at 300 dpi). A black-only printer prints the brightness. A color printer prints cyan, magenta, and yellow from the red, green, and blue; with black ink as well, black takes what the three have in common (full undercolor removal). Each ink is dithered with its own offset of the pattern, so dots of different inks do not land on each other in a regular way, and the pattern follows the row's place on the page, so it runs on from band to band.
+
+.code
+static bool drawBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows) {
+ // a red bar one inch down, a quarter inch high
+ const PrintPageInfoT *pi = (const PrintPageInfoT *)ctx;
+
+ for (int32_t r = 0; r < rows; r++) {
+ int32_t y = y0 + r;
+
+ if (y >= pi->dpiY && y < pi->dpiY + pi->dpiY / 4) {
+ for (int32_t x = 0; x < width; x++) {
+ uint8_t *p = rgb + ((r * width) + x) * 3;
+
+ p[0] = 0xFF;
+ p[1] = 0;
+ p[2] = 0;
+ }
+ }
+ }
+
+ return true;
+}
+
+PrintDocT *doc = printUiDocOpen(ctx, "Report", NULL);
+PrintPageInfoT pi;
+
+if (doc) {
+ printDocInfo(doc, &pi);
+ printDocPage(doc, drawBand, &pi);
+ printUiDocClose(ctx, doc);
+}
+.endcode
+
+.h2 Vector Pages
+
+On a PostScript printer (PrintPageInfoT.vector) a page can be drawn by the printer itself, as lines, curves, fills, and its own letters, instead of being sent as dots: sharper at any size, and much smaller. printVecBegin(doc) starts such a page and printVecEnd(doc) prints it; in between, the drawing calls below go to the printer as they are made. Coordinates are the page's dots (y down, a dot's centre at whole numbers, so a shape covers the same dots it would in a band), colors are 0xRRGGBB (grey on the black-and-white model), and widths are in dots. printVecBegin is false, and the drawing calls do nothing, on a printer that cannot or a document that failed; a raster page (printDocPage, printDocPicture) is refused while a vector page is open, and printDocClose ends one left open.
+
+.table
+ Function Description
+ -------- -----------
+ printVecBegin(doc), printVecEnd(doc) Start a vector page; print it (false when it did not print).
+ printVecLine(doc, x0, y0, x1, y1, color, width, dash) A line, round at the ends; dash is a 16-step pattern from its top bit (0xFFFF solid), a step a dot along the line's longer side, as BASIC's LINE style.
+ printVecRect(doc, x0, y0, x1, y1, color, fill) A filled box, corners inclusive.
+ printVecEllipseFill(doc, cx, cy, rx, ry, color, fill) A filled ellipse, out to the dots rx and ry from its centre.
+ printVecEllipse(doc, cx, cy, rx, ry, color, width, dash, arc, start, end) An ellipse's outline, or (arc) the part from start to end radians counterclockwise; dash as printVecLine's, along the outline from angle 0.
+ printVecDot(doc, x, y, color, width) A round dot width dots across.
+ printVecText(doc, font, x, y, text, len, color) Text with the top of its line at (x, y), with the font's underline and strike-through.
+ printVecImage(doc, rgb, w, h, sx, sy, sw, sh, dx, dy, dw, dh) The part sx, sy, sw, sh of a picture (R, G, B bytes) stretched over the dots dx, dy, dw, dh by the printer.
+.endtable
+
+A fill (PrintFillE) is solid or a hatch of lines one dot wide every PRINT_HATCH_DOTS (8) dots, horizontal, vertical, either diagonal, or crossed, lined up across the page as BASIC's FillStyle hatches are. The pen (its color, width, and dashes) and the font are sent only when they change, and each of the printer's faces is made Latin-1 the first time the document uses it.
+
+A font opened for a vector page (printFontOpen with page.vector set) is the printer's own Helvetica, Times, or Courier in the style asked for (FONT_STYLE_BOLD, FONT_STYLE_ITALIC): Courier for a fixed-pitch family or one with Mono or Courier in its name, Times for one with Serif, Times, Roman, Tinos, or Ledger in it (but not Sans), else Helvetica. It is exactly the size asked for, and printFontWidth, printFontHeight (1.2 times the size), and printFontAscent measure the printer's letters by Adobe's published widths, so text placed with them lines up as it prints. It draws only with printVecText (printFontSpans and printFontDraw draw nothing with it); for DVX's own letters drawn in bands on a PostScript printer, open the font with vector cleared in a copy of the page.
+
+.h2 Text on a Page
+
+A page a program draws can carry text in any font family (dvxText.h) at a size in points. printFontOpen(&page, family, decipoints, style) gives a PrintFontT for a page's resolution (its PrintPageInfoT): the size is in tenths of a point (120 for 12 points), the style any FONT_STYLE_ bits, underline and strike-through included. The font is the family's smallest strike at least as large as the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180), else its largest, scaled to the size across and down apart, since a 9-pin printer's dots are not square. Scaling is nearest pixel; scaling down, every pixel row and column of the strike still marks a dot, so thin strokes stay.
+
+The base set's sizes go up to 24 pixels, so most printed sizes are scaled up from it and look blocky at 300 dpi. The Printer Fonts add-on (kpunch.prfonts) gives DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, about 6 to 24 points at 300 dpi and 10 to 40 at 180: installed, those families print crisp at those sizes with no change to the program.
+
+.table
+ Function Description
+ -------- -----------
+ printFontOpen(&page, family, decipoints, style) The font; NULL when out of memory or for a size of 0 or less. printFontClose gives it up.
+ printFontAscent(font), printFontHeight(font) Dots from the top of a line to its baseline, and to the next line.
+ printFontWidth(font, text, len) Dots across len characters of text.
+ printFontSpans(font, x, y, text, len, top, bottom, fn, ctx) The text with the top of its line at (x, y) as runs of dots (PrintSpanFnT fn(ctx, x0, x1, y)), only rows top to bottom: a line that misses them is skipped at once, so a page drawn band by band costs little for the lines elsewhere.
+ printFontDraw(font, x, y, text, len, color, rgb, width, y0, rows) The same painted in color (0xRRGGBB) into the band a PrintBandFnT is filling.
+.endtable
+
+Positions are worked out in the strike's pixels from the start of the text and only then turned into dots, so printFontWidth and what is drawn always agree.
+
+.h2 Printer Drivers
+
+.table
+ Model Driver
+ ----- ------
+ Epson 9-pin (ESC/P) 120 x 72 dots per inch (ESC * 1), 8-row bands, paper fed with ESC J; blank bands only feed paper, and each pass stops after its last black dot.
+ Epson 24-pin (ESC/P) 180 x 180 dots per inch (ESC * 39, 3 bytes per column), 24-row bands.
+ HP LaserJet (PCL) 150 dots per inch raster graphics, one uncompressed row transfer per dot row (every LaserJet understands it), trailing white bytes left out.
+ HP LaserJet III or DeskJet (300 dpi) 300 dots per inch (PCL 5, and PCL 3 on DeskJets): each row packed (compression mode 2, TIFF PackBits) and blank rows skipped (ESC * b n Y) instead of sent, so a page with white space on it costs next to nothing.
+ Epson 9-pin / 24-pin color As the black ones, with a four-color ribbon: a pass of the head for each ink with ESC r n choosing its band of the ribbon, yellow first and black last (dark ink does not smear into the light ones).
+ HP DeskJet 500C As the 300 dpi LaserJet, in three planes (ESC * r -3 U: cyan, magenta, yellow; black is the three together): ESC * b n V for each plane of a row but the last, ESC * b n W for the last.
+ HP DeskJet 550C and later As the 500C, in four planes (ESC * r -4 U: black, cyan, magenta, yellow).
+ PostScript Level 2 at 300 dpi: each band an imagemask of its black dots, packed (RunLengthDecode, the same PackBits) and in ASCII85, which any port carries; a band with no ink is not sent. The 8 by 10 inch area is centred across the paper the printer has loaded, half an inch from its top.
+ PostScript (color) As PostScript, each band a colorimage of 4 bits a dot (cyan, magenta, yellow, black) from the dithered planes. Both PostScript models also draw vector pages (see Vector Pages).
+ Apple ImageWriter II 144 x 144 dots per inch: 144 across (ESC p), and down by two passes of the 8 graphics wires 1/144 inch apart (the even rows of a 16-row band, a 1/144-inch line feed, the odd rows, then ESC T 15 and a line feed). A line is ESC G nnnn and its columns (bit 0 the top dot), placed at its first dot (ESC F nnnn). The document resets the printer (ESC c) and sets its switches whatever its DIP switches say: no line feed after CR, 8-bit data, no perforation skip; then unidirectional printing (ESC >), which keeps the passes in line.
+ Apple ImageWriter II (color) As the ImageWriter II with the four-color ribbon: a line for each ink with ESC K n choosing its band, yellow first and black last, CR between them.
+.endtable
+
+The drivers are inside the library: each holds its resolution, the printable size in dots, the band height, the dither size, its inks, the text reset strings and usual page length, and begin / band / end functions that write the printer's commands. A new printer family is one more driver.
.h2 Dialogs and Ready-Made Printing
@@ -130,7 +236,9 @@ Jobs opened by these ask about printer problems with a Retry / Cancel message bo
.h2 BASIC
-src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup, and PrintCanPrintPictures. For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem), and PrintSettingsStore (printDeviceStore).
+BASIC's Printer object (the form runtime, formPrinter.c) is built on the calls above: it records a page's drawing, prints it with printUiDocOpen / printDocPage (each band drawn again from the record), and prints text with printFontOpen and printFontDraw; on a PostScript printer it draws the record once more as a vector page (printVec*), in the printer's own faces; its document goes where the program's last PrintChoose chose (printBasChoice, the running program's own answer only).
+
+src/include/basic/print.bas declares PrintTextDialog, PrintText, PrintPictureDialog, PrintPicture, PrintPageSetup, PrintCanPrintPictures, and PrintCanPrintColor (printModelPage). For serial printers it declares PrintDestIsSerial (printDestIsSerial), PrintBaudRate (printBaudRate), PrintHandshakeCount / PrintHandshakeLabel$ (printHandshakeLabel), PrintSettingBaud / PrintSettingHandshake (printSettingsRead), and PrintSerialStore (printSerialStore). For the printer settings it also declares PrintModelCount / PrintModelLabel$, PrintDestCount / PrintDestLabel$ / PrintDestIsFile, PrintSettingModel / PrintSettingDest / PrintSettingFile$ (printSettingsRead on a prefs.bas handle, through prefsBasHandle), PrintSettingsProblem$ (printDeviceProblem), and PrintSettingsStore (printDeviceStore).
.table
BASIC C it calls
diff --git a/src/libs/kpunch/print/print.h b/src/libs/kpunch/print/print.h
index 55812d80..7b32bdc6 100644
--- a/src/libs/kpunch/print/print.h
+++ b/src/libs/kpunch/print/print.h
@@ -38,11 +38,12 @@
// date, time, page number), tab expansion, word wrap
// or cut-off at the right margin, a page eject after
// every page, a page range and several copies.
-// Pictures A small printer-driver interface turns a picture
-// into dot graphics for Epson ESC/P 9-pin and 24-pin
-// printers and HP LaserJet (PCL) printers: the picture
-// is scaled to fit the page and dithered to black and
-// white (printRasterJob).
+// Documents Pages of pictures and drawings: a page is drawn a
+// band at a time in RGB (or is a picture scaled to
+// fit), dithered to the printer's inks, and sent as
+// dot graphics to Epson ESC/P 9-pin and 24-pin
+// printers (black or a color ribbon) or HP LaserJet
+// and DeskJet (PCL) printers (printDocOpen).
// Settings The printer model, port and page setup live in
// DVX.INI [printer] (printSettingsLoad / Save).
// Dialogs The common Print dialog (printer, copies, pages), the
@@ -62,8 +63,17 @@
#include
#include
-// Printer ports LPT1..LPT3 (port numbers 0..2)
-#define PRINT_PORT_COUNT 3
+// Printer ports: LPT1..LPT3 (port numbers 0..2), then the serial ports
+// COM1..COM4 (3..6) for serial printers such as the ImageWriter II
+#define PRINT_LPT_COUNT 3
+#define PRINT_COM_COUNT 4
+#define PRINT_PORT_COM1 PRINT_LPT_COUNT
+#define PRINT_PORT_COUNT (PRINT_LPT_COUNT + PRINT_COM_COUNT)
+
+// A serial port's speed and data (DVX.INI [printer] baud, handshake):
+// 8 data bits, no parity, 1 stop bit
+#define PRINT_BAUD_DEFAULT 9600
+#define PRINT_BAUD_COUNT 7 // the speeds offered (printBaudRate)
// printUiOpenPort: the printer chosen in the settings
#define PRINT_PORT_DEFAULT (-1)
@@ -111,19 +121,44 @@
#define PRINT_FILE_FILTER "Printer Files (*.prn)" // Browse... for a print file: its filter
#define PRINT_FILE_TITLE "Print to File" // and its title
+// Where output goes: a port (the value is its port number) or a file
typedef enum {
PrintDestLpt1E,
PrintDestLpt2E,
PrintDestLpt3E,
+ PrintDestCom1E,
+ PrintDestCom2E,
+ PrintDestCom3E,
+ PrintDestCom4E,
PrintDestFileE,
PrintDestCountE
} PrintDestE;
+// How a serial printer says it is busy: not at all, XON / XOFF
+// characters, or a line (the printer's DTR on the computer's CTS, or on
+// its DSR as Apple's ImageWriter cable wires it)
typedef enum {
- PrintModelTextE, // any printer, text only
- PrintModelEpson9E, // Epson ESC/P 9-pin (FX, MX, and compatibles)
- PrintModelEpson24E, // Epson ESC/P 24-pin (LQ and compatibles)
- PrintModelLaserJetE, // HP LaserJet (PCL) and compatibles
+ PrintHandshakeNoneE,
+ PrintHandshakeXonXoffE,
+ PrintHandshakeRtsCtsE,
+ PrintHandshakeDtrDsrE,
+ PrintHandshakeCountE
+} PrintHandshakeE;
+
+typedef enum {
+ PrintModelTextE, // any printer, text only
+ PrintModelEpson9E, // Epson ESC/P 9-pin (FX, MX, and compatibles)
+ PrintModelEpson24E, // Epson ESC/P 24-pin (LQ and compatibles)
+ PrintModelLaserJetE, // HP LaserJet (PCL) and compatibles
+ PrintModelLaserJet300E, // HP LaserJet III, DeskJet and later: 300 dpi, compressed
+ PrintModelEpson9ColorE, // Epson 9-pin with a color ribbon (JX-80, EX-800, and compatibles)
+ PrintModelEpson24ColorE, // Epson 24-pin with a color ribbon (LQ-2550, LQ-860, and color kits)
+ PrintModelDeskJet500CE, // HP DeskJet 500C: cyan, magenta, and yellow (black made of the three)
+ PrintModelDeskJetColorE, // HP DeskJet 550C and later: black and the three colors
+ PrintModelPostScriptE, // PostScript (Level 2) printers
+ PrintModelPostScriptColorE, // PostScript color printers
+ PrintModelImageWriterE, // Apple ImageWriter II
+ PrintModelImageWriterColorE, // Apple ImageWriter II with the color ribbon
PrintModelCountE
} PrintModelE;
@@ -159,6 +194,8 @@ typedef struct {
PrintModelE model;
char filePath[PRINT_PATH_MAX]; // PrintDestFileE
PrintPageSetupT page;
+ int32_t baud; // a serial port's speed (printBaudRate)
+ PrintHandshakeE handshake; // ... and how it is held back
} PrintSettingsT;
// What one text print is about (printTextJob)
@@ -190,48 +227,81 @@ typedef struct {
} PrintHooksT;
// The printer port services (port 0..PRINT_PORT_COUNT-1). status and
-// write return PRINT_STATUS_* bits; write sends one byte.
+// write return PRINT_STATUS_* bits; write sends one byte. open (a job
+// starts: a serial port's speed and handshake), pending (bytes taken but
+// not sent yet) and close (the job is done) may be NULL.
typedef struct {
bool (*present)(void *ctx, int32_t port);
uint8_t (*status)(void *ctx, int32_t port);
uint8_t (*write)(void *ctx, int32_t port, uint8_t byte);
void *ctx;
+ bool (*open)(void *ctx, int32_t port, int32_t baud, PrintHandshakeE handshake);
+ int32_t (*pending)(void *ctx, int32_t port);
+ void (*close)(void *ctx, int32_t port);
} PrintPortOpsT;
-// A printer driver for pictures: dots per inch, the printable area in
-// dots, and how many dot rows it takes at a time. begin / band / end
-// write the printer's commands; band gets rows of 1-bit dots (most
-// significant bit leftmost, 1 = black), rowBytes apart.
-typedef struct PrintDriverS PrintDriverT;
-
struct PrintJobS;
typedef struct PrintJobS PrintJobT;
-struct PrintDriverS {
- const char *name;
- int32_t dpiX;
- int32_t dpiY;
- int32_t pageWidthDots;
- int32_t pageHeightDots;
- int32_t bandRows;
- const char *textBegin; // sent before printed text ("" = nothing)
- const char *textEnd; // sent after printed text
- int32_t linesPerPage; // the usual page length for text
- bool (*begin)(PrintJobT *job, int32_t widthDots, int32_t heightDots);
- bool (*band)(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots);
- bool (*end)(PrintJobT *job);
-};
+// A document of picture pages (printDocOpen)
+struct PrintDocS;
+typedef struct PrintDocS PrintDocT;
-// Shared preference vocabularies (see PrefsChoiceT): [printer] port and
-// model, indexed by PrintDestE / PrintModelE.
+// The page a printer model prints pictures on: its dots per inch across
+// and down (they differ on the 9-pin printers), the printable area in
+// dots (8 by 10 inches), and whether it prints in color (else in shades
+// of grey).
+typedef struct {
+ int32_t dpiX;
+ int32_t dpiY;
+ int32_t widthDots;
+ int32_t heightDots;
+ bool color;
+ bool vector; // a PostScript printer: pages may be drawn with printVec*, and fonts measure as its own
+} PrintPageInfoT;
+
+// How printVecRect / printVecEllipseFill fill: solid, or lines one dot
+// wide every PRINT_HATCH_DOTS dots, lined up across the page (as BASIC's
+// FillStyle hatches)
+#define PRINT_HATCH_DOTS 8
+
+typedef enum {
+ PrintFillSolidE,
+ PrintFillHorizontalE,
+ PrintFillVerticalE,
+ PrintFillUpDiagonalE,
+ PrintFillDownDiagonalE,
+ PrintFillCrossE,
+ PrintFillDiagonalCrossE,
+ PrintFillCountE
+} PrintFillE;
+
+// Where a page's pixels come from: fill rows y0 .. y0 + rows - 1 of the
+// page, width dots across, into rgb (three bytes a dot, red first, rows
+// width * 3 bytes apart), which comes white. The page is asked a band at
+// a time, top down, at the printer's resolution. False stops the page.
+typedef bool (*PrintBandFnT)(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows);
+
+// Shared preference vocabularies (see PrefsChoiceT): [printer] port,
+// model, and handshake, indexed by PrintDestE / PrintModelE /
+// PrintHandshakeE.
extern const PrefsChoiceT printDestChoices[PrintDestCountE];
+extern const PrefsChoiceT printHandshakeChoices[PrintHandshakeCountE];
extern const PrefsChoiceT printModelChoices[PrintModelCountE];
// The user-facing labels of those choices (for programs that look the
// library up by name, which only finds functions).
const char *printDestLabel(PrintDestE dest);
+const char *printHandshakeLabel(PrintHandshakeE handshake);
const char *printModelLabel(PrintModelE model);
+// The serial speeds offered, index 0 .. PRINT_BAUD_COUNT - 1 (0 past the
+// end).
+int32_t printBaudRate(int32_t index);
+
+// True when dest is a serial port.
+bool printDestIsSerial(PrintDestE dest);
+
// ------------------------------------------------------------
// Settings
// ------------------------------------------------------------
@@ -259,6 +329,11 @@ const char *printDeviceProblem(PrintDestE dest, const char *filePath);
// saves h). The Control Panel's OK and print.bas PrintSettingsStore.
void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const char *filePath);
+// Store a serial port's speed and handshake the same way; false (nothing
+// stored) for a speed printBaudRate does not offer or a handshake out of
+// range.
+bool printSerialStore(PrefsHandleT *h, int32_t baud, PrintHandshakeE handshake);
+
// NULL when the page setup can be used, else why not.
const char *printPageSetupProblem(const PrintPageSetupT *ps);
@@ -272,10 +347,10 @@ int32_t printPageLines(const PrintPageSetupT *ps, PrintModelE model);
// Replace the port services (NULL = the real ones again).
void printSetPortOps(const PrintPortOpsT *ops);
-// True when LPTn (0-based) exists.
+// True when port (0-based: LPT1 .. LPT3, then COM1 .. COM4) exists.
bool printPortPresent(int32_t port);
-// BIOS status bits of LPTn.
+// The status bits of port (PRINT_STATUS_*: a parallel port's BIOS bits).
uint8_t printPortStatus(int32_t port);
// Describe the problem in a status (from a status check, or from a
@@ -283,9 +358,10 @@ uint8_t printPortStatus(int32_t port);
// False (out untouched) when the status shows no problem.
bool printStatusProblem(uint8_t status, bool afterWrite, const char *where, char *out, int32_t outSize);
-// Start a job on a port or into filePath (created, or emptied). hooks
-// may be NULL. NULL with a reason in err when the port does not exist
-// or the file cannot be created.
+// Start a job on a port or into filePath (created, or emptied); a serial
+// port is opened at the speed and handshake in the settings. hooks may
+// be NULL. NULL with a reason in err when the port does not exist or
+// cannot be opened, or the file cannot be created.
PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT *hooks, char *err, int32_t errSize);
// Send bytes. Returns once all of them have been taken, or false when
@@ -322,17 +398,127 @@ int32_t printTextPageCount(const char *text, int32_t len, const PrintPageSet
bool printTextJob(PrintJobT *job, PrintModelE model, const char *text, int32_t len, const PrintPageSetupT *ps, const PrintTextInfoT *info);
// ------------------------------------------------------------
-// Pictures
+// Documents: pages of pictures and drawings
// ------------------------------------------------------------
-// The picture driver of a model, NULL for PrintModelTextE.
-const PrintDriverT *printDriver(PrintModelE model);
+// The page model prints on (info may be NULL); false for text only.
+bool printModelPage(PrintModelE model, PrintPageInfoT *info);
+
+// Start a document on job for model. The document owns the job from
+// here on, and printDocClose closes it; NULL (the job closed) for a
+// text-only model or out of memory.
+PrintDocT *printDocOpen(PrintJobT *job, PrintModelE model);
+
+// The document's page (printModelPage of its model).
+void printDocInfo(const PrintDocT *doc, PrintPageInfoT *info);
+
+// Print a page the program draws: fn is asked for the whole printable
+// area a band at a time, top down, in RGB at the printer's resolution,
+// and each band goes to the printer as the printer's inks. False when
+// fn returned false, the job failed or was cancelled, or out of memory;
+// after a failure every later page fails at once.
+bool printDocPage(PrintDocT *doc, PrintBandFnT fn, void *ctx);
// Print a picture of w x h pixels (3 bytes R, G, B per pixel, rows pitch
-// bytes apart) on one page: scaled to the largest size that fits the
-// printable area, dithered to black and white. False when the job
-// failed or was cancelled, or out of memory.
-bool printRasterJob(PrintJobT *job, const PrintDriverT *drv, const uint8_t *rgb, int32_t w, int32_t h, int32_t pitch);
+// bytes apart) as a page: scaled to the largest size that fits the
+// printable area, keeping its shape. False as printDocPage.
+bool printDocPicture(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t pitch);
+
+// True when the job was cancelled (the Printing window's Cancel, or the
+// Retry box answered Cancel).
+bool printDocCancelled(const PrintDocT *doc);
+
+// End the document and close its job. True when every page printed.
+bool printDocClose(PrintDocT *doc);
+
+// ------------------------------------------------------------
+// Text on a page: fonts at printer sizes
+// ------------------------------------------------------------
+
+// A font of a family (dvxText.h) at a size in points, for one page's
+// resolution (printFontOpen)
+struct PrintFontS;
+typedef struct PrintFontS PrintFontT;
+
+// Where text's dots go: a run of them, x0 to x1 (inclusive), on row y of
+// the page.
+typedef void (*PrintSpanFnT)(void *ctx, int32_t x0, int32_t x1, int32_t y);
+
+// family (as dvxFontOpen: NULL or "" the system font) at decipoints
+// tenths of a point in style (FONT_STYLE_*, lines too) on page (its dots
+// per inch: printModelPage or printDocInfo), and given up. The family's
+// smallest strike at least that many dots is scaled to it, else its
+// largest: text is crisp where the family has large sizes (the
+// printer-fonts add-on) and blockier where it is scaled up. On a vector
+// page (page->vector) the font is instead the printer's own face nearest
+// the family (Courier for a fixed-pitch one, Times for a serif one, else
+// Helvetica), which measures as it prints and is drawn with printVecText
+// only (printFontSpans and printFontDraw draw nothing with it); clear
+// vector in a copy of the page for DVX's own letters in bands. NULL
+// when out of memory or for a size of 0 or less.
+void printFontClose(PrintFontT *font);
+PrintFontT *printFontOpen(const PrintPageInfoT *page, const char *family, int32_t decipoints, uint32_t style);
+
+// Dots from the top of a line of text to its baseline, and to the top of
+// the next line.
+int32_t printFontAscent(const PrintFontT *font);
+int32_t printFontHeight(const PrintFontT *font);
+
+// Dots across len characters of text take.
+int32_t printFontWidth(const PrintFontT *font, const char *text, int32_t len);
+
+// len characters of text with the top of their line at (x, y) dots, as
+// spans to fn: only rows top .. bottom (a band; whole lines are skipped
+// at once when they miss it).
+void printFontSpans(const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
+
+// The same painted in color (0xRRGGBB) into a band a PrintBandFnT is
+// filling: rows y0 .. y0 + rows - 1 of the page, width dots across.
+void printFontDraw(const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows);
+
+// ------------------------------------------------------------
+// Vector pages (PostScript printers: PrintPageInfoT.vector)
+// ------------------------------------------------------------
+
+// A page drawn with the printer's own lines, shapes, and fonts instead of
+// dots: printVecBegin, the drawing, printVecEnd. Coordinates are the
+// page's dots (y down; a dot's centre at whole numbers), colors 0xRRGGBB,
+// widths in dots. printVecBegin is false (and the rest do nothing) for a
+// document whose printer cannot, or that failed; printVecEnd is false
+// when the page did not print. A raster page (printDocPage,
+// printDocPicture) is refused while a vector page is open, and
+// printDocClose ends one left open. A font on such a page is the
+// printer's own (Helvetica, Times, or Courier, as near the family as can
+// be) and measures as it will print.
+bool printVecBegin(PrintDocT *doc);
+
+// A pen dot width dots across.
+void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width);
+
+// An ellipse's outline, or (arc) the part from start to end radians,
+// counterclockwise as the page shows it; dash as printVecLine's, a step a
+// dot along the outline from angle 0.
+void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end);
+
+// A filled ellipse, out to the dots rx and ry from its centre.
+void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill);
+
+bool printVecEnd(PrintDocT *doc);
+
+// The part sx, sy, sw, sh of a picture (R, G, B bytes, w x h) stretched
+// over the dots dx, dy, dw, dh.
+void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh);
+
+// A line; dash is a 16-step pattern from its top bit (0xFFFF solid), a
+// step a dot along the line's longer side.
+void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash);
+
+// A filled box, corners (in any order) inclusive.
+void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill);
+
+// Text with the top of its line at (x, y) in font (printFontOpen on this
+// page), with its underline and strike-through.
+void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color);
// ------------------------------------------------------------
// Dialogs and ready-made printing (need the GUI)
@@ -365,8 +551,25 @@ bool printUiWindow(AppContextT *ctx, WindowT *win, bool askFirst);
// Print dialog answer (or one filled by the program), without asking.
bool printUiTextChoice(AppContextT *ctx, const char *text, const char *title, const PrintChoiceT *choice);
+// Open a document on the printer in the settings, at the destination of
+// choice (a Print dialog answer; NULL: the settings'), with the Printing
+// window shown until printUiDocClose, Cancel in it, and a Retry / Cancel
+// box for problems. title names it in the window. NULL, after telling
+// the user why, when the model prints text only or the printer cannot be
+// opened. Copies are the caller's: print each page as often as wanted.
+PrintDocT *printUiDocOpen(AppContextT *ctx, const char *title, const PrintChoiceT *choice);
+
+// End it; a failure (not a cancel) is told to the user. True when every
+// page printed.
+bool printUiDocClose(AppContextT *ctx, PrintDocT *doc);
+
// The context the BASIC functions (print.bas) use; the BASIC runtime
// sets it.
void printBasSetContext(AppContextT *ctx);
+// The last PrintChoose answer the user gave with Print (its destination,
+// file, and copies: where the BASIC Printer object prints); false when
+// there was none.
+bool printBasChoice(PrintChoiceT *out);
+
#endif
diff --git a/src/libs/kpunch/print/printBas.c b/src/libs/kpunch/print/printBas.c
index 9aa1dfbe..b647d422 100644
--- a/src/libs/kpunch/print/printBas.c
+++ b/src/libs/kpunch/print/printBas.c
@@ -37,6 +37,7 @@
#include "printInt.h"
#include "dvxPrefs.h"
#include "dvxBas.h"
+#include "dvxMem.h"
#include
#include
@@ -45,6 +46,7 @@
#include
#define BAS_NONE (-1)
+#define NO_APP (-1) // no program made the choice
#define BAS_LPT_FIRST 1 // BASIC numbers the printer ports LPT1 = 1
// A number field of PrintPageSetupT by name (PrintPageSetting)
@@ -70,17 +72,22 @@ static const BasPageFieldT sPageFields[] = {
static AppContextT *sBasCtx = NULL;
static PrintChoiceT sChoice; // the last PrintChoose answer
+static int32_t sChoiceApp = NO_APP; // the program (app id) that chose Print in it
static char sText[PRINT_MSG_MAX];
// ============================================================
// Prototypes
// ============================================================
+static int32_t appNow(void);
static int32_t basBool(bool value);
static bool destValid(int32_t dest);
static bool modelValid(int32_t model);
static void pageSetupFrom(PrintPageSetupT *ps, int32_t lines, int32_t columns, int32_t marginTop, int32_t marginBottom, int32_t marginLeft, int32_t marginRight, int32_t tabWidth, int32_t wrap, const char *header, const char *footer);
+bool printBasChoice(PrintChoiceT *out);
void printBasSetContext(AppContextT *ctx);
+int32_t PrintBaudRate(int32_t index);
+int32_t PrintCanPrintColor(void);
int32_t PrintCanPrintPictures(void);
int32_t PrintChoiceCopies(void);
int32_t PrintChoiceDest(void);
@@ -90,7 +97,10 @@ int32_t PrintChoiceLast(void);
int32_t PrintChoose(const char *title, int32_t pageCount);
int32_t PrintDestCount(void);
int32_t PrintDestIsFile(int32_t dest);
+int32_t PrintDestIsSerial(int32_t dest);
const char *PrintDestLabel(int32_t dest);
+int32_t PrintHandshakeCount(void);
+const char *PrintHandshakeLabel(int32_t handshake);
int32_t PrintModelCount(void);
const char *PrintModelLabel(int32_t model);
int32_t PrintPageCount(const char *text);
@@ -104,8 +114,11 @@ int32_t PrintPicture(const char *path, const char *title);
int32_t PrintPictureDialog(const char *path, const char *title);
int32_t PrintPortPresent(int32_t port);
int32_t PrintPortStatus(int32_t port);
+int32_t PrintSerialStore(int32_t prefs, int32_t baud, int32_t handshake);
+int32_t PrintSettingBaud(int32_t prefs);
int32_t PrintSettingDest(int32_t prefs);
const char *PrintSettingFile(int32_t prefs);
+int32_t PrintSettingHandshake(int32_t prefs);
int32_t PrintSettingModel(int32_t prefs);
const char *PrintSettingsProblem(int32_t dest, const char *filePath);
int32_t PrintSettingsStore(int32_t prefs, int32_t model, int32_t dest, const char *filePath);
@@ -115,6 +128,12 @@ int32_t PrintTextDialog(const char *text, const char *title);
int32_t PrintTextPages(const char *text, const char *title, int32_t firstPage, int32_t lastPage, int32_t copies);
+// The running program's app id (0 where there are none).
+static int32_t appNow(void) {
+ return dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
+}
+
+
static int32_t basBool(bool value) {
return value ? BAS_TRUE : BAS_FALSE;
}
@@ -147,8 +166,37 @@ static void pageSetupFrom(PrintPageSetupT *ps, int32_t lines, int32_t columns, i
}
+// Only the running program's own answer counts: print.lib is shared by
+// every program.
+bool printBasChoice(PrintChoiceT *out) {
+ *out = sChoice;
+ return sChoiceApp == appNow();
+}
+
+
+// A program starting (its runtime binds) has made no choice yet.
void printBasSetContext(AppContextT *ctx) {
sBasCtx = ctx;
+
+ if (sChoiceApp == appNow()) {
+ sChoiceApp = NO_APP;
+ }
+}
+
+
+// The serial speeds offered; 0 past the last.
+int32_t PrintBaudRate(int32_t index) {
+ return printBaudRate(index);
+}
+
+
+// True when the printer model in the settings prints in color.
+int32_t PrintCanPrintColor(void) {
+ PrintSettingsT s;
+ PrintPageInfoT pi;
+
+ printSettingsLoad(&s);
+ return basBool(printModelPage(s.model, &pi) && pi.color);
}
@@ -157,7 +205,7 @@ int32_t PrintCanPrintPictures(void) {
PrintSettingsT s;
printSettingsLoad(&s);
- return basBool(printDriver(s.model) != NULL);
+ return basBool(printModelPage(s.model, NULL));
}
@@ -199,7 +247,13 @@ int32_t PrintChoose(const char *title, int32_t pageCount) {
return BAS_FALSE;
}
- return basBool(printDialog(sBasCtx, title, pageCount, &sChoice));
+ bool chose = printDialog(sBasCtx, title, pageCount, &sChoice);
+
+ if (chose) {
+ sChoiceApp = appNow();
+ }
+
+ return basBool(chose);
}
@@ -214,12 +268,29 @@ int32_t PrintDestIsFile(int32_t dest) {
}
+// True for a serial port.
+int32_t PrintDestIsSerial(int32_t dest) {
+ return basBool(destValid(dest) && printDestIsSerial((PrintDestE)dest));
+}
+
+
// "" for a number out of range.
const char *PrintDestLabel(int32_t dest) {
return destValid(dest) ? printDestLabel((PrintDestE)dest) : "";
}
+int32_t PrintHandshakeCount(void) {
+ return PrintHandshakeCountE;
+}
+
+
+// "" for a number out of range.
+const char *PrintHandshakeLabel(int32_t handshake) {
+ return handshake >= 0 && handshake < PrintHandshakeCountE ? printHandshakeLabel((PrintHandshakeE)handshake) : "";
+}
+
+
int32_t PrintModelCount(void) {
return PrintModelCountE;
}
@@ -370,8 +441,26 @@ int32_t PrintPortStatus(int32_t port) {
}
+// Store a serial printer's speed and handshake; False for a bad handle,
+// a speed not offered or a handshake out of range.
+int32_t PrintSerialStore(int32_t prefs, int32_t baud, int32_t handshake) {
+ PrefsHandleT *h = prefsBasHandle(prefs);
+
+ return basBool(h && printSerialStore(h, baud, (PrintHandshakeE)handshake));
+}
+
+
// The destination in the [printer] settings of a prefs.bas handle
// (the defaults for 0).
+// A serial printer's speed in the settings of prefs.
+int32_t PrintSettingBaud(int32_t prefs) {
+ PrintSettingsT s;
+
+ printSettingsRead(prefsBasHandle(prefs), &s);
+ return s.baud;
+}
+
+
int32_t PrintSettingDest(int32_t prefs) {
PrintSettingsT s;
@@ -391,6 +480,15 @@ const char *PrintSettingFile(int32_t prefs) {
}
+// A serial printer's handshake (a PrintHandshakeE number).
+int32_t PrintSettingHandshake(int32_t prefs) {
+ PrintSettingsT s;
+
+ printSettingsRead(prefsBasHandle(prefs), &s);
+ return s.handshake;
+}
+
+
int32_t PrintSettingModel(int32_t prefs) {
PrintSettingsT s;
diff --git a/src/libs/kpunch/print/printEscp.c b/src/libs/kpunch/print/printEscp.c
new file mode 100644
index 00000000..e8a9defb
--- /dev/null
+++ b/src/libs/kpunch/print/printEscp.c
@@ -0,0 +1,264 @@
+// 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.
+
+// printEscp.c -- Printing library: the Epson ESC/P picture drivers (see
+// printInt.h)
+//
+// 9-pin ESC/P, 120 x 72 dpi (ESC * 1: double density), bands of 8
+// rows (one pass of the print head), paper advanced with ESC J
+// n (n/216 inch).
+// 24-pin ESC/P, 180 x 180 dpi (ESC * 39), bands of 24 rows (3 bytes
+// per column), paper advanced with ESC J n (n/180 inch).
+// Color Either with a four-color ribbon (JX-80, EX-800, LQ-2550, and
+// color kits): a pass for each ink, its ribbon band chosen
+// with ESC r n, yellow first and black last (a dark ink does
+// not smear into the light ones).
+//
+// A band is one pass of the head for each ink (ESC * mode nL nH and the
+// columns, then CR) followed by the paper feed of its height. A band
+// with no ink only feeds; columns after the last dot are not sent.
+
+#include "printInt.h"
+
+#include
+
+#define ESC "\x1B"
+#define ESC_CHAR 0x1B
+#define CR_CHAR '\r'
+#define FORM_FEED "\f"
+#define BITS_PER_BYTE 8
+#define TOP_BIT 0x80
+
+#define EPSON_RESET ESC "@"
+#define EPSON_GRAPHICS '*'
+#define EPSON_FEED 'J'
+#define EPSON_RIBBON 'r' // ESC r n: the ribbon band
+#define EPSON_RIBBON_BLACK 0
+#define EPSON_RIBBON_MAGENTA 1
+#define EPSON_RIBBON_CYAN 2
+#define EPSON_RIBBON_YELLOW 4
+#define EPSON9_DPI_X 120
+#define EPSON9_DPI_Y 72
+#define EPSON9_MODE 1 // ESC * 1: double density
+#define EPSON9_PINS 8
+#define EPSON9_FEED_UNITS 216 // ESC J n feeds n/216 inch
+#define EPSON24_DPI 180
+#define EPSON24_MODE 39 // ESC * 39: 180 dpi, 24 pins
+#define EPSON24_PINS 24
+#define EPSON24_FEED_UNITS 180 // ESC J n feeds n/180 inch
+#define EPSON_FEED_MAX 255 // largest n of ESC J n
+#define EPSON_COUNT_SHIFT 8 // nL nH
+#define EPSON_COUNT_MASK 0xFF
+#define EPSON_HEADER_LEN 5 // ESC * m nL nH
+#define EPSON_RIBBON_LEN 3 // ESC r n
+
+// An Epson driver's own settings (PrintDriverT.params)
+typedef struct {
+ int32_t mode; // ESC * m: the graphics density
+ int32_t pins; // dot rows a pass prints
+ int32_t feedPerRow; // ESC J units a dot row is high
+} EpsonParamsT;
+
+static const EpsonParamsT sEpson9Params = { EPSON9_MODE, EPSON9_PINS, EPSON9_FEED_UNITS / EPSON9_DPI_Y };
+static const EpsonParamsT sEpson24Params = { EPSON24_MODE, EPSON24_PINS, EPSON24_FEED_UNITS / EPSON24_DPI };
+
+// A color ribbon's passes, lightest first
+static const PrintInkE sInksRibbon[PRINT_PLANES_MAX] = { PrintInkYellowE, PrintInkMagentaE, PrintInkCyanE, PrintInkBlackE };
+
+// ESC r n for each ink, indexed by PrintInkE
+static const uint8_t sRibbonOf[PrintInkCountE] = { EPSON_RIBBON_BLACK, EPSON_RIBBON_CYAN, EPSON_RIBBON_MAGENTA, EPSON_RIBBON_YELLOW };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool epsonBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes);
+static bool epsonDocBegin(const PrintDriverT *drv, PrintJobT *job);
+static bool epsonDocEnd(const PrintDriverT *drv, PrintJobT *job);
+static bool epsonFeed(PrintJobT *job, int32_t units);
+static bool epsonPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots);
+static bool epsonPageEnd(const PrintDriverT *drv, PrintJobT *job);
+static bool epsonPass(PrintJobT *job, const PrintPlanesT *planes, int32_t plane, const EpsonParamsT *ep);
+static int32_t lastInkColumn(const PrintPlanesT *planes, int32_t plane);
+
+const PrintDriverT printDrvEpson9 = {
+ EPSON9_DPI_X, EPSON9_DPI_Y, EPSON9_DPI_X * PRINT_PAGE_WIDTH_IN, EPSON9_DPI_Y * PRINT_PAGE_HEIGHT_IN, EPSON9_PINS,
+ PRINT_DITHER_SMALL, 1, printInksBlack, EPSON_RESET, "", PRINT_LINES_STANDARD, NULL, &sEpson9Params,
+ epsonDocBegin, epsonPageBegin, epsonBand, epsonPageEnd, epsonDocEnd, false
+};
+
+const PrintDriverT printDrvEpson24 = {
+ EPSON24_DPI, EPSON24_DPI, EPSON24_DPI * PRINT_PAGE_WIDTH_IN, EPSON24_DPI * PRINT_PAGE_HEIGHT_IN, EPSON24_PINS,
+ PRINT_DITHER_SMALL, 1, printInksBlack, EPSON_RESET, "", PRINT_LINES_STANDARD, NULL, &sEpson24Params,
+ epsonDocBegin, epsonPageBegin, epsonBand, epsonPageEnd, epsonDocEnd, false
+};
+
+const PrintDriverT printDrvEpson9Color = {
+ EPSON9_DPI_X, EPSON9_DPI_Y, EPSON9_DPI_X * PRINT_PAGE_WIDTH_IN, EPSON9_DPI_Y * PRINT_PAGE_HEIGHT_IN, EPSON9_PINS,
+ PRINT_DITHER_SMALL, PRINT_PLANES_MAX, sInksRibbon, EPSON_RESET, "", PRINT_LINES_STANDARD, NULL, &sEpson9Params,
+ epsonDocBegin, epsonPageBegin, epsonBand, epsonPageEnd, epsonDocEnd, false
+};
+
+const PrintDriverT printDrvEpson24Color = {
+ EPSON24_DPI, EPSON24_DPI, EPSON24_DPI * PRINT_PAGE_WIDTH_IN, EPSON24_DPI * PRINT_PAGE_HEIGHT_IN, EPSON24_PINS,
+ PRINT_DITHER_SMALL, PRINT_PLANES_MAX, sInksRibbon, EPSON_RESET, "", PRINT_LINES_STANDARD, NULL, &sEpson24Params,
+ epsonDocBegin, epsonPageBegin, epsonBand, epsonPageEnd, epsonDocEnd, false
+};
+
+
+// A pass of the head for each plane with ink, then the paper feed of the
+// band's height.
+static bool epsonBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes) {
+ const EpsonParamsT *ep = (const EpsonParamsT *)drv->params;
+
+ for (int32_t p = 0; p < planes->count; p++) {
+ if (!epsonPass(job, planes, p, ep)) {
+ return false;
+ }
+ }
+
+ return epsonFeed(job, planes->rows * ep->feedPerRow);
+}
+
+
+static bool epsonDocBegin(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, EPSON_RESET);
+}
+
+
+static bool epsonDocEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, EPSON_RESET);
+}
+
+
+// ESC J n, split when more than one command's worth.
+static bool epsonFeed(PrintJobT *job, int32_t units) {
+ while (units > 0) {
+ int32_t n = units > EPSON_FEED_MAX ? EPSON_FEED_MAX : units;
+ uint8_t cmd[3] = { ESC_CHAR, EPSON_FEED, (uint8_t)n };
+
+ if (!printJobWrite(job, cmd, (int32_t)sizeof(cmd))) {
+ return false;
+ }
+
+ units -= n;
+ }
+
+ return true;
+}
+
+
+static bool epsonPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots) {
+ (void)drv;
+ (void)job;
+ (void)widthDots;
+ (void)heightDots;
+ return true;
+}
+
+
+static bool epsonPageEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, FORM_FEED);
+}
+
+
+// One pass of the head over plane: on a color printer ESC r n for its
+// ink, then ESC * mode nL nH, for every column pins / 8 bytes (top dot in
+// the high bit), and CR. Nothing when the plane has no ink in the band.
+static bool epsonPass(PrintJobT *job, const PrintPlanesT *planes, int32_t plane, const EpsonParamsT *ep) {
+ int32_t cols = lastInkColumn(planes, plane) + 1;
+ int32_t bytesPerCol = ep->pins / BITS_PER_BYTE;
+ uint8_t *line;
+ int32_t n = 0;
+ bool ok;
+
+ if (cols == 0) {
+ return true;
+ }
+
+ line = (uint8_t *)malloc((size_t)(EPSON_RIBBON_LEN + EPSON_HEADER_LEN + cols * bytesPerCol + 1));
+
+ if (!line) {
+ return false;
+ }
+
+ if (planes->count > 1) {
+ line[n++] = ESC_CHAR;
+ line[n++] = EPSON_RIBBON;
+ line[n++] = sRibbonOf[planes->ink[plane]];
+ }
+
+ line[n++] = ESC_CHAR;
+ line[n++] = EPSON_GRAPHICS;
+ line[n++] = (uint8_t)ep->mode;
+ line[n++] = (uint8_t)(cols & EPSON_COUNT_MASK);
+ line[n++] = (uint8_t)(cols >> EPSON_COUNT_SHIFT);
+
+ for (int32_t x = 0; x < cols; x++) {
+ for (int32_t b = 0; b < bytesPerCol; b++) {
+ uint8_t v = 0;
+
+ for (int32_t bit = 0; bit < BITS_PER_BYTE; bit++) {
+ int32_t row = b * BITS_PER_BYTE + bit;
+
+ if (row < planes->rows && printPlaneDot(planes, plane, row, x)) {
+ v |= (uint8_t)(TOP_BIT >> bit);
+ }
+ }
+
+ line[n++] = v;
+ }
+ }
+
+ line[n++] = CR_CHAR;
+ ok = printJobWrite(job, line, n);
+ free(line);
+ return ok;
+}
+
+
+// Index of the last column with a dot in plane, -1 when none: the last
+// byte column with any ink in the band's rows, then its last dot.
+static int32_t lastInkColumn(const PrintPlanesT *planes, int32_t plane) {
+ for (int32_t b = planes->rowBytes - 1; b >= 0; b--) {
+ uint8_t any = 0;
+
+ for (int32_t r = 0; r < planes->rows; r++) {
+ any |= planes->bits[plane][r * planes->rowBytes + b];
+ }
+
+ if (any) {
+ int32_t x = b * BITS_PER_BYTE + BITS_PER_BYTE - 1;
+
+ while (!(any & (TOP_BIT >> (x % BITS_PER_BYTE)))) {
+ x--;
+ }
+
+ return x;
+ }
+ }
+
+ return -1;
+}
diff --git a/src/libs/kpunch/print/printFont.c b/src/libs/kpunch/print/printFont.c
new file mode 100644
index 00000000..3fdf1e66
--- /dev/null
+++ b/src/libs/kpunch/print/printFont.c
@@ -0,0 +1,386 @@
+// 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.
+
+// printFont.c -- Printing library: text on a page at printer sizes (see
+// print.h)
+//
+// A print font is a DVX font (dvxFontOpen) scaled to a size in points at
+// a page's resolution, across and down apart (a 9-pin printer's dots are
+// not square). The strike scaled is the family's smallest at least as
+// large as the size in dots, else its largest, so text is scaled down or
+// as little up as the family allows (the printer-fonts add-on gives the
+// DejaVu families sizes up to 24 points at 300 dpi). Scaling is nearest
+// pixel: a strike pixel becomes the dots it covers, and scaling down
+// every strike row and column still marks a dot, so thin strokes stay.
+//
+// Text goes out as spans (runs of dots on a row) to a PrintSpanFnT;
+// printFontDraw paints them into an RGB band. Positions are worked out
+// in strike pixels from the start of the text and only then turned into
+// dots, so a line of text and its width (printFontWidth) always agree.
+//
+// On a vector page the font is a PostScript printer's own face instead
+// (printPs.c draws it): its size is kept unrounded and it measures by
+// the face's character widths.
+
+#include "printInt.h"
+#include "dvxText.h"
+
+#include
+#include
+#include
+
+#define POINTS_PER_INCH 72
+#define DECIPOINTS_PER_INCH (POINTS_PER_INCH * 10)
+#define FONT_SIZES_MAX 64 // sizes a family may list (dvxFontSizes)
+#define RGB_BYTES 3
+#define RGB_RED_SHIFT 16
+#define RGB_GREEN_SHIFT 8
+#define RGB_MASK 0xFF
+#define ROUND_HALF 0.5
+
+// One glyph as a bitmap: the system font's cells and a loaded font's
+// glyphs alike
+typedef struct {
+ int32_t width;
+ int32_t height;
+ int32_t left; // pen to the bitmap's left column
+ int32_t top; // baseline to its top row (up is positive)
+ int32_t advance;
+ int32_t rowBytes;
+ bool gray; // FONT_GRAY_BPP bits a pixel
+ const uint8_t *bits;
+} GlyphT;
+
+// printFontDraw's band
+typedef struct {
+ uint8_t *rgb;
+ int32_t width;
+ int32_t y0;
+ uint8_t color[RGB_BYTES];
+} BandT;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void bandSpan(void *ctx, int32_t x0, int32_t x1, int32_t y);
+static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col);
+static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g);
+static void glyphSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
+static void lineSpans(const PrintFontT *pf, int32_t x, int32_t y, int32_t row, int32_t thick, int32_t width, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
+int32_t printFontAscent(const PrintFontT *font);
+void printFontClose(PrintFontT *font);
+void printFontDraw(const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows);
+int32_t printFontHeight(const PrintFontT *font);
+PrintFontT *printFontOpen(const PrintPageInfoT *page, const char *family, int32_t decipoints, uint32_t style);
+void printFontSpans(const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
+int32_t printFontWidth(const PrintFontT *font, const char *text, int32_t len);
+static int32_t scaleX(const PrintFontT *pf, int32_t px);
+static int32_t scaleY(const PrintFontT *pf, int32_t px);
+static int32_t textPixels(const BitmapFontT *font, const char *text, int32_t len);
+
+
+// A span in the band's color, the part of it inside the band.
+static void bandSpan(void *ctx, int32_t x0, int32_t x1, int32_t y) {
+ const BandT *b = (const BandT *)ctx;
+
+ x0 = x0 < 0 ? 0 : x0;
+ x1 = x1 >= b->width ? b->width - 1 : x1;
+
+ for (uint8_t *p = b->rgb + ((int64_t)(y - b->y0) * b->width + x0) * RGB_BYTES; x0 <= x1; x0++, p += RGB_BYTES) {
+ memcpy(p, b->color, RGB_BYTES);
+ }
+}
+
+
+// Whether the glyph pixel at col of a row is ink (a smoothed one from
+// half covered).
+static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col) {
+ if (g->gray) {
+ return FONT_GRAY_LEVEL(row, col) >= FONT_GRAY_HALF;
+ }
+
+ return (row[FONT_PIXEL_BYTE(col)] & FONT_PIXEL_BIT(col)) != 0;
+}
+
+
+// Character ch of font as a bitmap; false when the font has no glyph for
+// it (outside its range).
+static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g) {
+ int32_t idx = ch - font->firstChar;
+
+ if (idx < 0 || idx >= font->numChars) {
+ return false;
+ }
+
+ if (!font->glyphs) {
+ g->width = font->charWidth;
+ g->height = font->charHeight;
+ g->left = 0;
+ g->top = font->ascent;
+ g->advance = font->charWidth;
+ g->rowBytes = FONT_ROW_BYTES(font->charWidth);
+ g->gray = false;
+ g->bits = font->glyphData + idx * font->charHeight * g->rowBytes;
+ return true;
+ }
+
+ const FontGlyphT *fg = &font->glyphs[idx];
+
+ g->width = fg->width;
+ g->height = fg->height;
+ g->left = fg->left;
+ g->top = fg->top;
+ g->advance = fg->advance;
+ g->gray = (font->flags & FONT_FLAG_GRAY) != 0;
+ g->rowBytes = FONT_ROW_BYTES_BPP(fg->width, FONT_BPP(font));
+ g->bits = font->glyphData + font->glyphAt[idx];
+ return true;
+}
+
+
+// A glyph's dots, its pen pen strike pixels from the text's start at x,
+// the top of its line at y: each run of ink on a strike row, scaled.
+static void glyphSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx) {
+ int32_t rowTop = pf->font->ascent - g->top;
+
+ for (int32_t r = 0; r < g->height; r++) {
+ int32_t y0 = y + scaleY(pf, rowTop + r);
+ int32_t y1 = y + scaleY(pf, rowTop + r + 1);
+
+ y1 = y1 > y0 ? y1 : y0 + 1;
+
+ if (y1 <= top || y0 > bottom) {
+ continue;
+ }
+
+ const uint8_t *row = g->bits + r * g->rowBytes;
+ int32_t rt0 = y0 < top ? top : y0;
+ int32_t rt1 = y1 - 1 > bottom ? bottom : y1 - 1;
+
+ for (int32_t c = 0; c < g->width;) {
+ if (!glyphInk(g, row, c)) {
+ c++;
+ continue;
+ }
+
+ int32_t end = c;
+
+ while (end < g->width && glyphInk(g, row, end)) {
+ end++;
+ }
+
+ int32_t xa = x + scaleX(pf, pen + g->left + c);
+ int32_t xb = x + scaleX(pf, pen + g->left + end) - 1;
+
+ xb = xb >= xa ? xb : xa;
+
+ for (int32_t t = rt0; t <= rt1; t++) {
+ fn(ctx, xa, xb, t);
+ }
+
+ c = end;
+ }
+ }
+}
+
+
+// An underline or strike-through: strike rows row .. row + thick - 1 of
+// the line at y, width strike pixels from x.
+static void lineSpans(const PrintFontT *pf, int32_t x, int32_t y, int32_t row, int32_t thick, int32_t width, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx) {
+ int32_t y0 = y + scaleY(pf, row);
+ int32_t y1 = y + scaleY(pf, row + thick);
+ int32_t xb = x + scaleX(pf, width) - 1;
+
+ y1 = y1 > y0 ? y1 : y0 + 1;
+
+ if (xb < x) {
+ return;
+ }
+
+ for (int32_t t = y0 < top ? top : y0; t < y1 && t <= bottom; t++) {
+ fn(ctx, x, xb, t);
+ }
+}
+
+
+int32_t printFontAscent(const PrintFontT *font) {
+ if (font->psFace != PRINT_PS_NONE) {
+ return (int32_t)floor(printPsAscent(font) + ROUND_HALF);
+ }
+
+ return scaleY(font, font->font->ascent);
+}
+
+
+void printFontClose(PrintFontT *font) {
+ if (!font) {
+ return;
+ }
+
+ dvxFontClose(font->font);
+ free(font);
+}
+
+
+void printFontDraw(const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows) {
+ BandT b;
+
+ b.rgb = rgb;
+ b.width = width;
+ b.y0 = y0;
+ b.color[0] = (uint8_t)((color >> RGB_RED_SHIFT) & RGB_MASK);
+ b.color[1] = (uint8_t)((color >> RGB_GREEN_SHIFT) & RGB_MASK);
+ b.color[2] = (uint8_t)(color & RGB_MASK);
+ printFontSpans(font, x, y, text, len, y0, y0 + rows - 1, bandSpan, &b);
+}
+
+
+int32_t printFontHeight(const PrintFontT *font) {
+ if (font->psFace != PRINT_PS_NONE) {
+ return (int32_t)floor(PRINT_PS_LEADING * font->psSizeY / PRINT_PS_UNITS + ROUND_HALF);
+ }
+
+ return scaleY(font, font->font->charHeight);
+}
+
+
+// The strike: the family's smallest size at least the larger of the
+// sizes across and down, else its largest. A vector page's font is the
+// printer's face.
+PrintFontT *printFontOpen(const PrintPageInfoT *page, const char *family, int32_t decipoints, uint32_t style) {
+ int32_t sizes[FONT_SIZES_MAX];
+ PrintFontT *pf = (PrintFontT *)calloc(1, sizeof(PrintFontT));
+
+ if (!pf || !page || decipoints <= 0) {
+ free(pf);
+ return NULL;
+ }
+
+ pf->dotsX = (int32_t)((int64_t)decipoints * page->dpiX / DECIPOINTS_PER_INCH);
+ pf->dotsY = (int32_t)((int64_t)decipoints * page->dpiY / DECIPOINTS_PER_INCH);
+ pf->dotsX = pf->dotsX < 1 ? 1 : pf->dotsX;
+ pf->dotsY = pf->dotsY < 1 ? 1 : pf->dotsY;
+ pf->style = style;
+ pf->psFace = PRINT_PS_NONE;
+
+ if (page->vector) {
+ pf->psFace = printPsFaceOf(family, style);
+ pf->psSizeX = (double)decipoints * page->dpiX / DECIPOINTS_PER_INCH;
+ pf->psSizeY = (double)decipoints * page->dpiY / DECIPOINTS_PER_INCH;
+ return pf;
+ }
+
+ int32_t want = pf->dotsX > pf->dotsY ? pf->dotsX : pf->dotsY;
+ int32_t count = dvxFontSizes(family, style, sizes, FONT_SIZES_MAX);
+ int32_t pick = FONT_CHAR_HEIGHT;
+
+ count = count > FONT_SIZES_MAX ? FONT_SIZES_MAX : count;
+
+ for (int32_t i = 0; i < count; i++) {
+ pick = sizes[i];
+
+ if (pick >= want) {
+ break;
+ }
+ }
+
+ pf->font = dvxFontOpen(family, pick, style);
+
+ if (!pf->font) {
+ free(pf);
+ return NULL;
+ }
+
+ pf->size = pf->font->pixelSize > 0 ? pf->font->pixelSize : pick;
+ return pf;
+}
+
+
+// Each glyph's spans, then the lines the font's style asks for; nothing
+// when the line misses top .. bottom (or for a printer's face).
+void printFontSpans(const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx) {
+ const BitmapFontT *f = font->font;
+ int32_t pen = 0;
+
+ if (!f || !text || len <= 0 || y > bottom || y + printFontHeight(font) <= top) {
+ return;
+ }
+
+ for (int32_t i = 0; i < len; i++) {
+ GlyphT g;
+
+ if (!glyphOf(f, (uint8_t)text[i], &g)) {
+ pen += f->charWidth;
+ continue;
+ }
+
+ glyphSpans(font, &g, x, y, pen, top, bottom, fn, ctx);
+ pen += g.advance;
+ }
+
+ if (f->style & FONT_STYLE_UNDERLINE) {
+ lineSpans(font, x, y, f->underlineY, f->lineThick, pen, top, bottom, fn, ctx);
+ }
+
+ if (f->style & FONT_STYLE_STRIKE) {
+ lineSpans(font, x, y, f->strikeY, f->lineThick, pen, top, bottom, fn, ctx);
+ }
+}
+
+
+int32_t printFontWidth(const PrintFontT *font, const char *text, int32_t len) {
+ if (!text || len <= 0) {
+ return 0;
+ }
+
+ if (font->psFace != PRINT_PS_NONE) {
+ return (int32_t)floor(printPsWidth(font, text, len) + ROUND_HALF);
+ }
+
+ return scaleX(font, textPixels(font->font, text, len));
+}
+
+
+// Strike pixels across / down as dots.
+static int32_t scaleX(const PrintFontT *pf, int32_t px) {
+ return (int32_t)((int64_t)px * pf->dotsX / pf->size);
+}
+
+
+static int32_t scaleY(const PrintFontT *pf, int32_t px) {
+ return (int32_t)((int64_t)px * pf->dotsY / pf->size);
+}
+
+
+// The pen's movement over len characters of text, in strike pixels.
+static int32_t textPixels(const BitmapFontT *font, const char *text, int32_t len) {
+ int32_t pen = 0;
+
+ for (int32_t i = 0; i < len; i++) {
+ GlyphT g;
+
+ pen += glyphOf(font, (uint8_t)text[i], &g) ? g.advance : font->charWidth;
+ }
+
+ return pen;
+}
diff --git a/src/libs/kpunch/print/printInt.h b/src/libs/kpunch/print/printInt.h
index 857361e7..3e3f8931 100644
--- a/src/libs/kpunch/print/printInt.h
+++ b/src/libs/kpunch/print/printInt.h
@@ -38,4 +38,201 @@ extern const PrintPortOpsT printSysPortOps;
// Name of a destination for messages: "LPT1" or the file's base name.
void printDestName(PrintDestE dest, const char *filePath, char *out, int32_t outSize);
+// ============================================================
+// Pictures: inks, bands, and the drivers (printRaster.c: the pipeline;
+// printEscp.c and printPcl.c: the drivers)
+// ============================================================
+
+// A printer's inks, a plane of dots each
+typedef enum {
+ PrintInkBlackE,
+ PrintInkCyanE,
+ PrintInkMagentaE,
+ PrintInkYellowE,
+ PrintInkCountE
+} PrintInkE;
+
+#define PRINT_PLANES_MAX PrintInkCountE
+
+// Ordered dither matrix sizes: 4 x 4 (17 levels, for the coarse dot-
+// matrix printers) or 8 x 8 (65 levels, for 300 dpi)
+#define PRINT_DITHER_SMALL 4
+#define PRINT_DITHER_LARGE 8
+
+// The printable area every driver uses, in inches
+#define PRINT_PAGE_WIDTH_IN 8
+#define PRINT_PAGE_HEIGHT_IN 10
+
+// One band of a page as dots of ink: plane i holds ink[i]'s, rows top
+// first, rowBytes apart, the leftmost dot in the high bit of a row's
+// first byte.
+typedef struct {
+ int32_t count;
+ PrintInkE ink[PRINT_PLANES_MAX];
+ uint8_t *bits[PRINT_PLANES_MAX];
+ int32_t rowBytes;
+ int32_t rows;
+ int32_t widthDots;
+ int32_t y0; // the band's first row on the page
+} PrintPlanesT;
+
+// A printer model's picture driver. A document is docBegin, then each
+// page: pageBegin, its bands top down, pageEnd; then docEnd. The band
+// gets the planes of inks in that order, dithered from the page's colours.
+// Each call gets the driver itself, whose params are its family's own
+// settings (EpsonParamsT, PclParamsT).
+typedef struct PrintDriverS PrintDriverT;
+
+// A driver that prints text its own way (PostScript): the text layout's
+// pages and lines (each with its left margin, no line end) go to it in
+// place of plain text, a job at a time.
+typedef struct {
+ bool (*begin)(PrintJobT *job, int32_t columns, int32_t lines);
+ bool (*pageBegin)(PrintJobT *job);
+ bool (*line)(PrintJobT *job, const char *text, int32_t len);
+ bool (*pageEnd)(PrintJobT *job);
+ bool (*end)(PrintJobT *job);
+} PrintTextOpsT;
+
+struct PrintDriverS {
+ int32_t dpiX;
+ int32_t dpiY;
+ int32_t pageWidthDots; // the printable area
+ int32_t pageHeightDots;
+ int32_t bandRows; // rows in a band (the last may have fewer)
+ int32_t ditherSize; // PRINT_DITHER_SMALL or PRINT_DITHER_LARGE
+ int32_t inkCount;
+ const PrintInkE *inks; // the planes band gets, in its order
+ const char *textBegin; // sent before printed text ("" = nothing)
+ const char *textEnd; // sent after printed text
+ int32_t linesPerPage; // the usual page length for text
+ const PrintTextOpsT *textOps; // NULL: text is plain text, between textBegin and textEnd
+ const void *params;
+ bool (*docBegin)(const PrintDriverT *drv, PrintJobT *job);
+ bool (*pageBegin)(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots);
+ bool (*band)(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes);
+ bool (*pageEnd)(const PrintDriverT *drv, PrintJobT *job);
+ bool (*docEnd)(const PrintDriverT *drv, PrintJobT *job);
+ bool vector; // pages may be drawn as PostScript (printVec*)
+};
+
+// A document of picture pages (print.h)
+struct PrintDocS {
+ PrintJobT *job;
+ const PrintDriverT *drv;
+ PrintModelE model;
+ bool ok; // false once a page failed
+ bool vecPage; // between printVecBegin and printVecEnd
+ bool vecDashed; // the vector page's pen: dashed (square ends) or solid (round)
+ uint16_t psFaces; // printPsFaces made Latin-1 in the job so far, a bit each
+ uint32_t vecColor; // ... the pen's colour
+ int32_t vecWidth; // ... and width in dots
+ int32_t vecFace; // the vector page's font: its face (PRINT_PS_NONE: none yet)
+ double vecSizeX; // ... and size in dots
+ double vecSizeY;
+};
+
+// A print font (print.h): a DVX font scaled to the page's dots, or on a
+// vector page a PostScript printer's own face
+struct PrintFontS {
+ BitmapFontT *font; // NULL for a printer's face
+ int32_t size; // its strike's pixel size (a strike pixel is dotsX / size dots across)
+ int32_t dotsX; // the size in dots across
+ int32_t dotsY; // ... and down
+ int32_t psFace; // printPsFaces index of a printer's face, else -1
+ uint32_t style; // FONT_STYLE_* (a printer's face draws the lines itself)
+ double psSizeX; // a printer's face: the size in dots across (not rounded)
+ double psSizeY; // ... and down
+};
+
+// The PostScript printers' own text faces (printPsAfm.c, made from
+// Adobe's AFM files by src/tools/afmWidths.py): Helvetica, Times, and
+// Courier, each upright, bold, slanted, and both (PRINT_PS_BOLD and
+// PRINT_PS_SLANT added to a family's first); the ascender, descender,
+// x-height, underline position (its centre: negative below the baseline)
+// and thickness, and each character's width from PRINT_PS_FIRST_CHAR in
+// ISOLatin1Encoding order, all in thousandths of the size. A line of
+// text is PRINT_PS_LEADING thousandths tall, the space beyond the
+// ascender and descender shared above and below them.
+#define PRINT_PS_FACES 12
+#define PRINT_PS_FIRST_CHAR 32
+#define PRINT_PS_CHARS (256 - PRINT_PS_FIRST_CHAR)
+#define PRINT_PS_SANS 0
+#define PRINT_PS_SERIF 4
+#define PRINT_PS_MONO 8
+#define PRINT_PS_BOLD 1
+#define PRINT_PS_SLANT 2
+#define PRINT_PS_UNITS 1000
+#define PRINT_PS_LEADING 1200
+#define PRINT_PS_NONE (-1) // a font that is not a printer's face
+
+typedef struct {
+ const char *name;
+ int16_t ascender;
+ int16_t descender;
+ int16_t xHeight;
+ int16_t underlinePos;
+ int16_t underlineThick;
+ uint16_t widths[PRINT_PS_CHARS];
+} PrintPsFaceT;
+
+extern const PrintPsFaceT printPsFaces[PRINT_PS_FACES];
+
+// The face of printPsFaces a family and style (FONT_STYLE_BOLD, _ITALIC)
+// print in: Courier for a fixed-pitch family (or one named Mono or
+// Courier), Times for a serif one (its name says so), else Helvetica
+// (printPs.c).
+int32_t printPsFaceOf(const char *family, uint32_t style);
+
+// A CP437 character as ISOLatin1Encoding has it: Latin-1 letters and
+// signs as themselves, lines as - | +, blocks and shades as #, the rest
+// as ? (printPs.c); always PRINT_PS_FIRST_CHAR or more.
+int32_t printPsLatin1(uint8_t ch);
+
+// A printer's face (PrintFontT.psFace): dots from the top of a line to
+// its baseline, and across len characters of text, not rounded
+// (printPs.c).
+double printPsAscent(const PrintFontT *font);
+double printPsWidth(const PrintFontT *font, const char *text, int32_t len);
+
+// The one ink of a black-and-white printer, and the four of a printer
+// with black (printRaster.c)
+extern const PrintInkE printInksBlack[1];
+extern const PrintInkE printInksKcmy[PRINT_PLANES_MAX];
+
+// The drivers (printEscp.c, printPcl.c)
+extern const PrintDriverT printDrvDeskJet500C;
+extern const PrintDriverT printDrvDeskJetColor;
+extern const PrintDriverT printDrvEpson9;
+extern const PrintDriverT printDrvEpson9Color;
+extern const PrintDriverT printDrvEpson24;
+extern const PrintDriverT printDrvEpson24Color;
+extern const PrintDriverT printDrvImageWriter;
+extern const PrintDriverT printDrvImageWriterColor;
+extern const PrintDriverT printDrvLaserJet;
+extern const PrintDriverT printDrvLaserJet300;
+extern const PrintDriverT printDrvPostScript;
+extern const PrintDriverT printDrvPostScriptColor;
+
+// The picture driver of a model, NULL for PrintModelTextE.
+const PrintDriverT *printDriver(PrintModelE model);
+
+// One page through drv, widthDots x heightDots: fn fills each band in RGB
+// at the driver's resolution, which is turned into the driver's inks.
+// False when the job failed or was cancelled, or out of memory.
+bool printPageRun(PrintJobT *job, const PrintDriverT *drv, int32_t widthDots, int32_t heightDots, PrintBandFnT fn, void *ctx);
+
+// n bytes as TIFF PackBits (PCL compression mode 2, PostScript's
+// RunLengthDecode) into out, which holds PRINT_PACK_MAX(n); returns the
+// bytes written.
+#define PACK_RUN_MAX 128
+#define PRINT_PACK_MAX(n) ((n) + (n) / PACK_RUN_MAX + 1)
+int32_t printPackBits(const uint8_t *in, int32_t n, uint8_t *out);
+
+// The brightness (0 .. 255) of an R, G, B pixel.
+int32_t printLuma(const uint8_t *px);
+
+// True when plane has a dot at (x, row) of the band.
+bool printPlaneDot(const PrintPlanesT *pl, int32_t plane, int32_t row, int32_t x);
+
#endif
diff --git a/src/libs/kpunch/print/printIw.c b/src/libs/kpunch/print/printIw.c
new file mode 100644
index 00000000..9b08cf1a
--- /dev/null
+++ b/src/libs/kpunch/print/printIw.c
@@ -0,0 +1,242 @@
+// 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.
+
+// printIw.c -- Printing library: the Apple ImageWriter II drivers (see
+// printInt.h; the codes are the ImageWriter II Technical Reference
+// Manual's, 1986)
+//
+// ImageWriter II 144 x 144 dpi: 144 dots across (ESC p), and 144
+// down by two passes of the 8 graphics wires (72
+// dpi) 1/144 inch apart -- the even rows of a
+// 16-row band, a 1/144-inch line feed (ESC T 01),
+// the odd rows, then the rest of the band (ESC T
+// 15).
+// ImageWriter II color the same with the four-color ribbon: a line for
+// each ink with its ribbon band chosen by ESC K n,
+// yellow first and black last, CR without a line
+// feed between them.
+//
+// A line of graphics is ESC G nnnn (four ASCII digits) and the columns,
+// one byte each, bit 0 the top dot; it starts at the first column with
+// ink (ESC F nnnn places the head there), stops after the last, and is
+// not sent when it has none. The document starts by resetting the
+// printer (ESC c), then sets its software switches whatever its DIP
+// switches say: no line feed after CR, 8-bit data (ESC Z 80 20), no
+// perforation skip (ESC D 40 04); then 144 dpi and unidirectional
+// printing (ESC >), which keeps the two passes and the inks in line. It
+// ends with ESC c again.
+
+#include "printInt.h"
+
+#include
+#include
+#include
+#include
+
+#define ESC "\x1B"
+#define CR_CHAR '\r'
+#define FORM_FEED "\f"
+
+#define IW_DPI 144
+#define IW_PINS 8 // graphics wires, 1/72 inch apart
+#define IW_PASSES 2 // passes a band: its even rows, then its odd rows
+#define IW_BAND_ROWS (IW_PINS * IW_PASSES)
+#define IW_RESET ESC "c"
+#define IW_SWITCHES ESC "Z\x80\x20" ESC "D\x40\x04" // no LF after CR, 8-bit data; no perforation skip
+#define IW_SETUP IW_SWITCHES ESC "p" ESC ">" // 144 dpi, unidirectional
+#define IW_GRAPHICS_FMT ESC "G%04" PRId32
+#define IW_PLACE_FMT ESC "F%04" PRId32
+#define IW_FEED_FMT ESC "T%02" PRId32
+#define IW_FEED_MAX 99 // largest ESC T nn
+#define IW_RIBBON 'K' // ESC K n: the ribbon band
+#define IW_RIBBON_BLACK '0'
+#define IW_RIBBON_YELLOW '1'
+#define IW_RIBBON_MAGENTA '2'
+#define IW_RIBBON_CYAN '3'
+#define IW_CMD_MAX 16
+
+// A color ribbon's lines, lightest first
+static const PrintInkE sInksRibbon[PRINT_PLANES_MAX] = { PrintInkYellowE, PrintInkMagentaE, PrintInkCyanE, PrintInkBlackE };
+
+// ESC K n for each ink, indexed by PrintInkE
+static const char sRibbonOf[PrintInkCountE] = { IW_RIBBON_BLACK, IW_RIBBON_CYAN, IW_RIBBON_MAGENTA, IW_RIBBON_YELLOW };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool iwBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes);
+static uint8_t iwColumn(const PrintPlanesT *planes, int32_t plane, int32_t pass, int32_t x);
+static bool iwDocBegin(const PrintDriverT *drv, PrintJobT *job);
+static bool iwDocEnd(const PrintDriverT *drv, PrintJobT *job);
+static bool iwFeed(PrintJobT *job, int32_t rows);
+static bool iwLine(PrintJobT *job, const PrintPlanesT *planes, int32_t plane, int32_t pass);
+static bool iwPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots);
+static bool iwPageEnd(const PrintDriverT *drv, PrintJobT *job);
+
+const PrintDriverT printDrvImageWriter = {
+ IW_DPI, IW_DPI, IW_DPI * PRINT_PAGE_WIDTH_IN, IW_DPI * PRINT_PAGE_HEIGHT_IN, IW_BAND_ROWS,
+ PRINT_DITHER_SMALL, 1, printInksBlack, IW_RESET IW_SWITCHES, IW_RESET, PRINT_LINES_STANDARD, NULL, NULL,
+ iwDocBegin, iwPageBegin, iwBand, iwPageEnd, iwDocEnd, false
+};
+
+const PrintDriverT printDrvImageWriterColor = {
+ IW_DPI, IW_DPI, IW_DPI * PRINT_PAGE_WIDTH_IN, IW_DPI * PRINT_PAGE_HEIGHT_IN, IW_BAND_ROWS,
+ PRINT_DITHER_SMALL, PRINT_PLANES_MAX, sInksRibbon, IW_RESET IW_SWITCHES, IW_RESET, PRINT_LINES_STANDARD, NULL, NULL,
+ iwDocBegin, iwPageBegin, iwBand, iwPageEnd, iwDocEnd, false
+};
+
+
+// Each pass: a line of each ink, CR between them; then the feed to the
+// next pass (1/144 inch) or past the band.
+static bool iwBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes) {
+ (void)drv;
+
+ for (int32_t pass = 0; pass < IW_PASSES; pass++) {
+ for (int32_t p = 0; p < planes->count; p++) {
+ if (!iwLine(job, planes, p, pass)) {
+ return false;
+ }
+ }
+
+ if (!iwFeed(job, pass + 1 < IW_PASSES ? 1 : planes->rows - pass)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+// Column x of a pass as a graphics byte: the band's rows pass, pass + 2,
+// ... from bit 0 (the top wire) down.
+static uint8_t iwColumn(const PrintPlanesT *planes, int32_t plane, int32_t pass, int32_t x) {
+ uint8_t v = 0;
+
+ for (int32_t bit = 0; bit < IW_PINS; bit++) {
+ int32_t row = pass + bit * IW_PASSES;
+
+ if (row < planes->rows && printPlaneDot(planes, plane, row, x)) {
+ v |= (uint8_t)(1u << bit);
+ }
+ }
+
+ return v;
+}
+
+
+static bool iwDocBegin(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, IW_RESET IW_SETUP);
+}
+
+
+static bool iwDocEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, IW_RESET);
+}
+
+
+// Paper fed rows / 144 inch: ESC T nn and a line feed, as many as it takes.
+static bool iwFeed(PrintJobT *job, int32_t rows) {
+ char cmd[IW_CMD_MAX];
+
+ while (rows > 0) {
+ int32_t n = rows > IW_FEED_MAX ? IW_FEED_MAX : rows;
+
+ snprintf(cmd, sizeof(cmd), IW_FEED_FMT "\n", n);
+
+ if (!printJobPuts(job, cmd)) {
+ return false;
+ }
+
+ rows -= n;
+ }
+
+ return true;
+}
+
+
+// One ink's line of a pass: on a color printer ESC K n, the head placed
+// at the first column with ink (ESC F nnnn), ESC G nnnn and the columns
+// to the last one with ink, then CR. Nothing when the pass has no ink.
+static bool iwLine(PrintJobT *job, const PrintPlanesT *planes, int32_t plane, int32_t pass) {
+ int32_t first = 0;
+ int32_t last = planes->widthDots - 1;
+
+ while (first < planes->widthDots && iwColumn(planes, plane, pass, first) == 0) {
+ first++;
+ }
+
+ if (first == planes->widthDots) {
+ return true;
+ }
+
+ while (iwColumn(planes, plane, pass, last) == 0) {
+ last--;
+ }
+
+ int32_t count = last - first + 1;
+ uint8_t *data = (uint8_t *)malloc((size_t)count);
+ char cmd[IW_CMD_MAX * 2];
+ int32_t n = 0;
+ bool ok;
+
+ if (!data) {
+ return false;
+ }
+
+ if (planes->count > 1) {
+ n += snprintf(cmd + n, sizeof(cmd) - (size_t)n, ESC "%c%c", IW_RIBBON, sRibbonOf[planes->ink[plane]]);
+ }
+
+ if (first > 0) {
+ n += snprintf(cmd + n, sizeof(cmd) - (size_t)n, IW_PLACE_FMT, first);
+ }
+
+ snprintf(cmd + n, sizeof(cmd) - (size_t)n, IW_GRAPHICS_FMT, count);
+
+ for (int32_t x = 0; x < count; x++) {
+ data[x] = iwColumn(planes, plane, pass, first + x);
+ }
+
+ uint8_t cr = CR_CHAR;
+
+ ok = printJobPuts(job, cmd) && printJobWrite(job, data, count) && printJobWrite(job, &cr, 1);
+ free(data);
+ return ok;
+}
+
+
+static bool iwPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots) {
+ (void)drv;
+ (void)job;
+ (void)widthDots;
+ (void)heightDots;
+ return true;
+}
+
+
+static bool iwPageEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, FORM_FEED);
+}
diff --git a/src/libs/kpunch/print/printJob.c b/src/libs/kpunch/print/printJob.c
index 8ebd6bbe..d80d36fa 100644
--- a/src/libs/kpunch/print/printJob.c
+++ b/src/libs/kpunch/print/printJob.c
@@ -28,7 +28,9 @@
// is busy the job calls the idle hook and checks again, and a printer
// that stays busy for PRINT_BUSY_TIMEOUT_MS is reported like any other
// problem. A problem is put to the ask hook; Retry tries the same byte
-// again, Cancel ends the job.
+// again, Cancel ends the job. A serial port is opened when its job
+// starts and, at the end, closed once the bytes it holds have gone (the
+// printer holding them back counts as busy).
#include "printInt.h"
#include "dvxPlat.h"
@@ -54,6 +56,8 @@ DVX_ABI_STAMP(print);
#define INI_WRAP "wrap"
#define INI_HEADER "header"
#define INI_FOOTER "footer"
+#define INI_BAUD "baud"
+#define INI_HANDSHAKE "handshake"
// Bytes written to a file between two calls of the idle hook
#define PRINT_FILE_YIELD_BYTES 4096
@@ -74,14 +78,36 @@ const PrefsChoiceT printDestChoices[PrintDestCountE] = {
{ "LPT1", "LPT1", PrintDestLpt1E },
{ "LPT2", "LPT2", PrintDestLpt2E },
{ "LPT3", "LPT3", PrintDestLpt3E },
+ { "COM1", "COM1", PrintDestCom1E },
+ { "COM2", "COM2", PrintDestCom2E },
+ { "COM3", "COM3", PrintDestCom3E },
+ { "COM4", "COM4", PrintDestCom4E },
{ "File", "FILE", PrintDestFileE }
};
+const PrefsChoiceT printHandshakeChoices[PrintHandshakeCountE] = {
+ { "None", "none", PrintHandshakeNoneE },
+ { "XON / XOFF", "xonxoff", PrintHandshakeXonXoffE },
+ { "Hardware (CTS)", "rtscts", PrintHandshakeRtsCtsE },
+ { "Hardware (DSR, Apple)", "dtrdsr", PrintHandshakeDtrDsrE }
+};
+
+static const int32_t sBaudRates[PRINT_BAUD_COUNT] = { 300, 1200, 2400, 4800, 9600, 19200, 38400 };
+
const PrefsChoiceT printModelChoices[PrintModelCountE] = {
- { "Text only (any printer)", "text", PrintModelTextE },
- { "Epson 9-pin (ESC/P)", "epson9", PrintModelEpson9E },
- { "Epson 24-pin (ESC/P)", "epson24", PrintModelEpson24E },
- { "HP LaserJet (PCL)", "laserjet", PrintModelLaserJetE }
+ { "Text only (any printer)", "text", PrintModelTextE },
+ { "Epson 9-pin (ESC/P)", "epson9", PrintModelEpson9E },
+ { "Epson 24-pin (ESC/P)", "epson24", PrintModelEpson24E },
+ { "HP LaserJet (PCL)", "laserjet", PrintModelLaserJetE },
+ { "HP LaserJet III or DeskJet (300 dpi)", "laserjet300", PrintModelLaserJet300E },
+ { "Epson 9-pin color (ESC/P)", "epson9color", PrintModelEpson9ColorE },
+ { "Epson 24-pin color (ESC/P)", "epson24color", PrintModelEpson24ColorE },
+ { "HP DeskJet 500C (color)", "deskjet500c", PrintModelDeskJet500CE },
+ { "HP DeskJet 550C and later (color)", "deskjetcolor", PrintModelDeskJetColorE },
+ { "PostScript", "postscript", PrintModelPostScriptE },
+ { "PostScript (color)", "postscriptcolor", PrintModelPostScriptColorE },
+ { "Apple ImageWriter II", "imagewriter", PrintModelImageWriterE },
+ { "Apple ImageWriter II (color)", "imagewritercolor", PrintModelImageWriterColorE }
};
static const char sNotResponding[] = "is not responding. Check that it is switched on and connected, then choose Retry.";
@@ -93,11 +119,15 @@ static const PrintPortOpsT *sOps = &printSysPortOps;
// ============================================================
static bool askRetry(PrintJobT *job, const char *message);
+static int32_t baudOffered(int32_t baud);
static bool idle(PrintJobT *job);
+int32_t printBaudRate(int32_t index);
+bool printDestIsSerial(PrintDestE dest);
const char *printDestLabel(PrintDestE dest);
void printDestName(PrintDestE dest, const char *filePath, char *out, int32_t outSize);
const char *printDeviceProblem(PrintDestE dest, const char *filePath);
void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const char *filePath);
+const char *printHandshakeLabel(PrintHandshakeE handshake);
uint32_t printJobBytes(const PrintJobT *job);
bool printJobCancelled(const PrintJobT *job);
bool printJobClose(PrintJobT *job);
@@ -109,6 +139,7 @@ int32_t printPageLines(const PrintPageSetupT *ps, PrintModelE model);
const char *printPageSetupProblem(const PrintPageSetupT *ps);
bool printPortPresent(int32_t port);
uint8_t printPortStatus(int32_t port);
+bool printSerialStore(PrefsHandleT *h, int32_t baud, PrintHandshakeE handshake);
void printSetPortOps(const PrintPortOpsT *ops);
void printSettingsDefaults(PrintSettingsT *s);
void printSettingsLoad(PrintSettingsT *s);
@@ -134,6 +165,18 @@ static bool askRetry(PrintJobT *job, const char *message) {
}
+// A speed printBaudRate offers, else the default.
+static int32_t baudOffered(int32_t baud) {
+ for (int32_t i = 0; i < PRINT_BAUD_COUNT; i++) {
+ if (sBaudRates[i] == baud) {
+ return baud;
+ }
+ }
+
+ return PRINT_BAUD_DEFAULT;
+}
+
+
// Let other programs run. False (job cancelled) when the hook says so.
static bool idle(PrintJobT *job) {
job->idled = true;
@@ -153,6 +196,16 @@ static bool idle(PrintJobT *job) {
}
+int32_t printBaudRate(int32_t index) {
+ return index >= 0 && index < PRINT_BAUD_COUNT ? sBaudRates[index] : 0;
+}
+
+
+bool printDestIsSerial(PrintDestE dest) {
+ return dest >= PrintDestCom1E && dest <= PrintDestCom4E;
+}
+
+
const char *printDestLabel(PrintDestE dest) {
return printDestChoices[dest].label;
}
@@ -162,7 +215,7 @@ void printDestName(PrintDestE dest, const char *filePath, char *out, int32_t out
if (dest == PrintDestFileE) {
snprintf(out, outSize, "%s", platformPathBaseName(filePath ? filePath : ""));
} else {
- snprintf(out, outSize, "LPT%d", (int)(dest + PRINT_PORT_NUMBER_BASE));
+ snprintf(out, outSize, "%s", printDestChoices[dest].label);
}
}
@@ -191,6 +244,11 @@ void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const
}
+const char *printHandshakeLabel(PrintHandshakeE handshake) {
+ return printHandshakeChoices[handshake].label;
+}
+
+
uint32_t printJobBytes(const PrintJobT *job) {
return job ? job->bytes : 0;
}
@@ -206,6 +264,35 @@ bool printJobClose(PrintJobT *job) {
return false;
}
+ // A port's bytes held back (a serial printer's buffer) go out first
+ if (!job->fp && sOps->pending) {
+ PlatformTicksT since = platformClock();
+ int32_t left = sOps->pending(sOps->ctx, job->port);
+
+ while (!job->cancelled && left > 0) {
+ int32_t now = sOps->pending(sOps->ctx, job->port);
+
+ if (now < left) {
+ left = now;
+ since = platformClock();
+ } else if (platformClock() - since >= PLATFORM_MS_TO_TICKS(PRINT_BUSY_TIMEOUT_MS)) {
+ char msg[PRINT_MSG_MAX];
+
+ snprintf(msg, sizeof(msg), "The printer on %s %s", job->where, sNotResponding);
+
+ if (askRetry(job, msg)) {
+ since = platformClock();
+ }
+ } else {
+ idle(job);
+ }
+ }
+ }
+
+ if (!job->fp && sOps->close) {
+ sOps->close(sOps->ctx, job->port);
+ }
+
bool ok = !job->cancelled;
if (job->fp && fclose(job->fp) != 0) {
@@ -250,7 +337,17 @@ PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT
job->idleEvery = PRINT_YIELD_BYTES;
if ((int32_t)dest < 0 || (int32_t)dest >= PRINT_PORT_COUNT || !printPortPresent(job->port)) {
- snprintf(err, errSize, "This computer has no printer port %s.", job->where);
+ snprintf(err, errSize, "This computer has no port %s.", job->where);
+ free(job);
+ return NULL;
+ }
+
+ PrintSettingsT s;
+
+ printSettingsLoad(&s);
+
+ if (sOps->open && !sOps->open(sOps->ctx, job->port, s.baud, s.handshake)) {
+ snprintf(err, errSize, "The port %s cannot be opened. Another program may be using it.", job->where);
free(job);
return NULL;
}
@@ -347,6 +444,22 @@ uint8_t printPortStatus(int32_t port) {
}
+// The serial settings stored with everything else as DVX.INI has it now.
+bool printSerialStore(PrefsHandleT *h, int32_t baud, PrintHandshakeE handshake) {
+ PrintSettingsT s;
+
+ if (baudOffered(baud) != baud || handshake < PrintHandshakeNoneE || handshake >= PrintHandshakeCountE) {
+ return false;
+ }
+
+ printSettingsLoad(&s);
+ s.baud = baud;
+ s.handshake = handshake;
+ printSettingsWrite(h, &s);
+ return true;
+}
+
+
void printSetPortOps(const PrintPortOpsT *ops) {
sOps = ops ? ops : &printSysPortOps;
}
@@ -356,6 +469,8 @@ void printSettingsDefaults(PrintSettingsT *s) {
memset(s, 0, sizeof(*s));
s->dest = PrintDestLpt1E;
s->model = PrintModelTextE;
+ s->baud = PRINT_BAUD_DEFAULT;
+ s->handshake = PrintHandshakeDtrDsrE;
s->page.columns = PRINT_COLUMNS_DEFAULT;
s->page.marginTop = PRINT_MARGIN_TB_DEFAULT;
s->page.marginBottom = PRINT_MARGIN_TB_DEFAULT;
@@ -402,6 +517,8 @@ void printSettingsRead(PrefsHandleT *h, PrintSettingsT *s) {
platformPathNative(s->filePath);
snprintf(s->page.header, sizeof(s->page.header), "%s", prefsGetString(h, PRINT_SECTION, INI_HEADER, def.page.header));
snprintf(s->page.footer, sizeof(s->page.footer), "%s", prefsGetString(h, PRINT_SECTION, INI_FOOTER, def.page.footer));
+ s->baud = baudOffered(prefsGetInt(h, PRINT_SECTION, INI_BAUD, def.baud));
+ s->handshake = (PrintHandshakeE)prefsGetChoice(h, PRINT_SECTION, INI_HANDSHAKE, printHandshakeChoices, PrintHandshakeCountE, printHandshakeChoices[def.handshake].iniName);
if (printPageSetupProblem(&s->page)) {
s->page = def.page;
@@ -439,6 +556,8 @@ void printSettingsWrite(PrefsHandleT *h, const PrintSettingsT *s) {
prefsSetBool(h, PRINT_SECTION, INI_WRAP, s->page.wrap);
prefsSetString(h, PRINT_SECTION, INI_HEADER, s->page.header);
prefsSetString(h, PRINT_SECTION, INI_FOOTER, s->page.footer);
+ prefsSetInt(h, PRINT_SECTION, INI_BAUD, s->baud);
+ prefsSetString(h, PRINT_SECTION, INI_HANDSHAKE, printHandshakeChoices[s->handshake].iniName);
}
diff --git a/src/libs/kpunch/print/printPcl.c b/src/libs/kpunch/print/printPcl.c
new file mode 100644
index 00000000..c2556d37
--- /dev/null
+++ b/src/libs/kpunch/print/printPcl.c
@@ -0,0 +1,239 @@
+// 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.
+
+// printPcl.c -- Printing library: the HP PCL picture drivers (see
+// printInt.h)
+//
+// LaserJet PCL, 150 dpi raster graphics (ESC * t150R), one
+// ESC * b n W transfer per row (uncompressed, which every
+// LaserJet understands), trailing white bytes left out.
+// LaserJet 300 PCL, 300 dpi (LaserJet III, DeskJet and later): rows in
+// compression mode 2 (TIFF PackBits, ESC * b 2 M), blank
+// rows skipped with ESC * b n Y instead of being sent.
+// DeskJet 500C As the 300 dpi one, in three planes (ESC * r -3 U:
+// cyan, magenta, yellow); black is the three together.
+// DeskJet color DeskJet 550C and later: four planes (ESC * r -4 U:
+// black, cyan, magenta, yellow).
+//
+// A page is a raster graphic: its resolution, its planes on a color
+// printer, and start (ESC * t, ESC * r U, the cursor to the top-left
+// corner, ESC * r 1 A), the rows, then the end of the graphic (ESC * r B)
+// and a form feed. A row of several planes is each plane's transfer in
+// the driver's ink order, ESC * b n V for all but the last, which is
+// ESC * b n W.
+
+#include "printInt.h"
+
+#include
+#include
+#include
+#include
+
+#define ESC "\x1B"
+#define FORM_FEED "\f"
+
+#define PCL_RESET ESC "E"
+#define PCL_DPI 150
+#define PCL_BAND_ROWS 8
+#define PCL300_DPI 300
+#define PCL300_BAND_ROWS 32
+#define PCL_DPI_FMT ESC "*t%" PRId32 "R"
+#define PCL_PLANES_FMT ESC "*r%" PRId32 "U"
+#define PCL_START ESC "*p0x0Y" ESC "*r1A"
+#define PCL_PACKBITS ESC "*b2M"
+#define PCL_END_RASTER ESC "*rB"
+#define PCL_TRANSFER_FMT ESC "*b%" PRId32 "%c"
+#define PCL_PLANE_NEXT 'V' // a row's plane, more to come
+#define PCL_PLANE_LAST 'W' // a row's last plane
+#define PCL_SKIP_FMT ESC "*b%" PRId32 "Y"
+#define CMY_INKS 3
+#define PCL_PLANES_CMY (-CMY_INKS) // ESC * r -3 U: cyan, magenta, yellow
+#define PCL_PLANES_KCMY (-PRINT_PLANES_MAX) // ESC * r -4 U: black and the three
+#define PCL_CMD_MAX 16
+
+// A PCL driver's own settings (PrintDriverT.params; NULL: the plain one)
+typedef struct {
+ bool packBits; // rows in mode 2, blank rows skipped
+ int32_t planes; // ESC * r n U (PCL_PLANES_*); 0: black only, not sent
+} PclParamsT;
+
+static const PclParamsT sPcl300Params = { true, 0 };
+static const PclParamsT sDeskJet500CParams = { true, PCL_PLANES_CMY };
+static const PclParamsT sDeskJetColorParams = { true, PCL_PLANES_KCMY };
+
+// The DeskJet 500C's planes, in the order ESC * r -3 U gives them (the
+// later ones' are printInksKcmy's)
+static const PrintInkE sInksCmy[CMY_INKS] = { PrintInkCyanE, PrintInkMagentaE, PrintInkYellowE };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static int32_t inkBytes(const uint8_t *row, int32_t n);
+static bool pclBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes);
+static bool pclDocBegin(const PrintDriverT *drv, PrintJobT *job);
+static bool pclDocEnd(const PrintDriverT *drv, PrintJobT *job);
+static bool pclPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots);
+static bool pclPageEnd(const PrintDriverT *drv, PrintJobT *job);
+static bool pclSkip(PrintJobT *job, int32_t *blank);
+
+const PrintDriverT printDrvLaserJet = {
+ PCL_DPI, PCL_DPI, PCL_DPI * PRINT_PAGE_WIDTH_IN, PCL_DPI * PRINT_PAGE_HEIGHT_IN, PCL_BAND_ROWS,
+ PRINT_DITHER_SMALL, 1, printInksBlack, PCL_RESET, PCL_RESET, PRINT_LINES_LASER, NULL, NULL,
+ pclDocBegin, pclPageBegin, pclBand, pclPageEnd, pclDocEnd, false
+};
+
+const PrintDriverT printDrvLaserJet300 = {
+ PCL300_DPI, PCL300_DPI, PCL300_DPI * PRINT_PAGE_WIDTH_IN, PCL300_DPI * PRINT_PAGE_HEIGHT_IN, PCL300_BAND_ROWS,
+ PRINT_DITHER_LARGE, 1, printInksBlack, PCL_RESET, PCL_RESET, PRINT_LINES_LASER, NULL, &sPcl300Params,
+ pclDocBegin, pclPageBegin, pclBand, pclPageEnd, pclDocEnd, false
+};
+
+const PrintDriverT printDrvDeskJet500C = {
+ PCL300_DPI, PCL300_DPI, PCL300_DPI * PRINT_PAGE_WIDTH_IN, PCL300_DPI * PRINT_PAGE_HEIGHT_IN, PCL300_BAND_ROWS,
+ PRINT_DITHER_LARGE, CMY_INKS, sInksCmy, PCL_RESET, PCL_RESET, PRINT_LINES_LASER, NULL, &sDeskJet500CParams,
+ pclDocBegin, pclPageBegin, pclBand, pclPageEnd, pclDocEnd, false
+};
+
+const PrintDriverT printDrvDeskJetColor = {
+ PCL300_DPI, PCL300_DPI, PCL300_DPI * PRINT_PAGE_WIDTH_IN, PCL300_DPI * PRINT_PAGE_HEIGHT_IN, PCL300_BAND_ROWS,
+ PRINT_DITHER_LARGE, PRINT_PLANES_MAX, printInksKcmy, PCL_RESET, PCL_RESET, PRINT_LINES_LASER, NULL, &sDeskJetColorParams,
+ pclDocBegin, pclPageBegin, pclBand, pclPageEnd, pclDocEnd, false
+};
+
+
+// How many of row's n bytes count: up to the last one with ink.
+static int32_t inkBytes(const uint8_t *row, int32_t n) {
+ while (n > 0 && row[n - 1] == 0) {
+ n--;
+ }
+
+ return n;
+}
+
+
+// Every row: each plane's transfer of its bytes up to the last one with
+// ink; packed, with the blank rows before it skipped, on a PackBits
+// printer.
+static bool pclBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes) {
+ const PclParamsT *pp = (const PclParamsT *)drv->params;
+ bool pack = pp && pp->packBits;
+ uint8_t *packed = pack ? (uint8_t *)malloc((size_t)PRINT_PACK_MAX(planes->rowBytes)) : NULL;
+ int32_t last = planes->count - 1;
+ int32_t blank = 0;
+ bool ok = !pack || packed;
+ char cmd[PCL_CMD_MAX];
+
+ for (int32_t r = 0; ok && r < planes->rows; r++) {
+ int32_t n[PRINT_PLANES_MAX];
+ bool ink = false;
+
+ for (int32_t p = 0; p < planes->count; p++) {
+ n[p] = inkBytes(planes->bits[p] + r * planes->rowBytes, planes->rowBytes);
+ ink = ink || n[p] > 0;
+ }
+
+ if (pack && !ink) {
+ blank++;
+ continue;
+ }
+
+ ok = pclSkip(job, &blank);
+
+ for (int32_t p = 0; ok && p < planes->count; p++) {
+ const uint8_t *row = planes->bits[p] + r * planes->rowBytes;
+ int32_t len = n[p];
+
+ if (pack) {
+ len = printPackBits(row, len, packed);
+ row = packed;
+ }
+
+ snprintf(cmd, sizeof(cmd), PCL_TRANSFER_FMT, len, p == last ? PCL_PLANE_LAST : PCL_PLANE_NEXT);
+ ok = printJobPuts(job, cmd) && printJobWrite(job, row, len);
+ }
+ }
+
+ // Blank rows at the end of the band: the next band starts below them
+ ok = ok && pclSkip(job, &blank);
+ free(packed);
+ return ok;
+}
+
+
+static bool pclDocBegin(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, PCL_RESET);
+}
+
+
+static bool pclDocEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, PCL_RESET);
+}
+
+
+// The resolution, the planes (color printers), the start of the graphic,
+// and PackBits rows (where the driver packs them).
+static bool pclPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots) {
+ const PclParamsT *pp = (const PclParamsT *)drv->params;
+ char cmd[PCL_CMD_MAX];
+ bool ok;
+
+ (void)widthDots;
+ (void)heightDots;
+ snprintf(cmd, sizeof(cmd), PCL_DPI_FMT, drv->dpiX);
+ ok = printJobPuts(job, cmd);
+
+ if (ok && pp && pp->planes != 0) {
+ snprintf(cmd, sizeof(cmd), PCL_PLANES_FMT, pp->planes);
+ ok = printJobPuts(job, cmd);
+ }
+
+ ok = ok && printJobPuts(job, PCL_START);
+
+ if (ok && pp && pp->packBits) {
+ ok = printJobPuts(job, PCL_PACKBITS);
+ }
+
+ return ok;
+}
+
+
+static bool pclPageEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, PCL_END_RASTER FORM_FEED);
+}
+
+
+// ESC * b n Y for the blank rows counted so far, and the count cleared.
+static bool pclSkip(PrintJobT *job, int32_t *blank) {
+ char cmd[PCL_CMD_MAX];
+
+ if (*blank == 0) {
+ return true;
+ }
+
+ snprintf(cmd, sizeof(cmd), PCL_SKIP_FMT, *blank);
+ *blank = 0;
+ return printJobPuts(job, cmd);
+}
diff --git a/src/libs/kpunch/print/printPs.c b/src/libs/kpunch/print/printPs.c
new file mode 100644
index 00000000..0d801f69
--- /dev/null
+++ b/src/libs/kpunch/print/printPs.c
@@ -0,0 +1,946 @@
+// 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.
+
+// printPs.c -- Printing library: the PostScript drivers (see printInt.h)
+//
+// PostScript Level 2, pictures at 300 dpi: each band of a page an
+// imagemask of its black dots.
+// PostScript color each band a colorimage of 4 bits a dot (cyan,
+// magenta, yellow, black: the dithered planes).
+//
+// Band data is packed (RunLengthDecode: the PackBits of PCL mode 2) and
+// sent as ASCII85, which any port carries; a band with no ink is not
+// sent. The 8 x 10 inch printable area is centred across the paper and
+// half an inch from its top, whatever size the printer has loaded
+// (currentpagedevice). A PostScript printer prints no plain text, so
+// text jobs are PostScript too: Courier (Latin-1, CP437 characters
+// mapped), sized so the page setup's columns fill the 8 inches and its
+// lines the 10, a show per line.
+//
+// Vector pages (printVec*) are drawn by the printer itself: lines, arcs,
+// and fills in the page's dots (y down, a dot's centre half a dot in from
+// its corner), hatches as lines every PRINT_HATCH_DOTS dots clipped to
+// the shape, text in the printer's Helvetica, Times, or Courier (made
+// Latin-1 as a page first uses each), and pictures as images the printer
+// scales (8 bits a colour, grey on the black-and-white model). The pen
+// (colour, width, dashes) and font are only sent when they change.
+
+#include "printInt.h"
+#include "dvxFontFmt.h"
+#include "dvxText.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define PS_DPI 300
+#define PS_BAND_ROWS 32
+#define PS_COLOR_BITS 4 // a dot of the color image: C, M, Y, K
+#define PS_DOTS_PER_BYTE 2 // ... two to a byte
+#define PS_LINE_MAX 75 // ASCII85 characters on a line
+#define PS_STRING_LINE 200 // string characters on a line (a longer string goes on with \ and a line end)
+#define PS_STRING_ESCAPE 4 // the most characters one character takes in a string (\ooo)
+#define PS_STRING_CONTINUE "\\\n"
+#define PS_CMD_MAX 64
+#define PS_VEC_CMD_MAX 320 // a vector command (a dash pattern of 16 runs, a hatch)
+#define RLE_EOD 128 // RunLengthDecode's end of data
+#define A85_GROUP 4
+#define A85_CHARS 5
+#define A85_BASE 85
+#define A85_FIRST '!'
+#define A85_ZERO 'z'
+#define A85_END "~>"
+#define POINTS_WIDTH (72 * PRINT_PAGE_WIDTH_IN)
+#define POINTS_HEIGHT (72 * PRINT_PAGE_HEIGHT_IN)
+#define HUNDREDTHS 100
+#define COURIER_ADVANCE 60 // Courier's advance, hundredths of its size
+#define LATIN1_FIRST 0xA0 // Latin-1 letters and signs PostScript's ISOLatin1Encoding has
+#define LATIN1_LAST 0xFF
+#define BOX_FIRST 0x2500 // Unicode box drawing (CP437 B3-DA)
+#define BOX_H 0x2500
+#define BOX_DOUBLE_H 0x2550
+#define BOX_V 0x2502
+#define BOX_DOUBLE_V 0x2551
+#define BLOCK_FIRST 0x2580 // blocks and shades (CP437 B0-B2, DB-DF)
+#define BLOCK_LAST 0x259F
+#define OCTAL_ESCAPE "\\%03o"
+#define DOT_CENTRE 0.5 // a dot's centre from its top-left corner
+#define DEGREES_PER_RADIAN (180.0 / 3.14159265358979323846)
+#define DASH_BITS 16 // a dash pattern: a bit a step along the line, from the top one
+#define DASH_TOP 0x8000u
+#define DASH_SOLID 0xFFFFu
+#define DASH_NONE 0x0000u
+#define TEXT_LINES_MAX 2 // an underline and a strike-through
+#define HATCH_MASK (PRINT_HATCH_DOTS - 1)
+#define RGB_BYTES 3
+#define GRAY_BYTES 1
+#define RGB_RED_SHIFT 16
+#define RGB_GREEN_SHIFT 8
+#define RGB_MASK 0xFF
+#define COLOR_LEVELS 255.0
+#define PS_BLACK 0x000000u // a page's colour at its start
+
+// The procedures every job starts with: dvxPage leaves the printable
+// area's top-left corner (points); dvxBegin starts a page of dots (y
+// down) at a resolution; dvxMask and dvxColor print a band (y, width,
+// height) from the data after them; dvxRe makes a Latin-1 copy of a font;
+// dvxTop and L start a page of text and show a line. For vector pages:
+// dvxL strokes a line (x0 y0 x1 y1); dvxEA is the path of an ellipse's
+// arc (cx cy rx ry from to, degrees counterclockwise on the page), dvxE a
+// whole one, dvxBox a box (x y w h); dvxD is a dot (x y radius); dvxHz,
+// dvxVt, dvxUp, and dvxDn are hatch lines (first step last from to);
+// dvxSF sets a font (name, size across and down); dvxT shows text (string
+// x y); dvxImg prints a picture (x y w h, its width height, 1 grey or 3
+// RGB) from the data after it.
+static const char sProlog[] =
+ "%!PS\n"
+ "%%Creator: DVX\n"
+ "%%LanguageLevel: 2\n"
+ "%%EndComments\n"
+ "/dvxPage { currentpagedevice /PageSize get aload pop 36 sub exch 576 sub 2 div exch } bind def\n"
+ "/dvxBegin { /dvxDpi exch def gsave dvxPage translate 72 dvxDpi div dup neg scale } bind def\n"
+ "/dvxData { /dvxA currentfile /ASCII85Decode filter def /dvxR dvxA /RunLengthDecode filter def } bind def\n"
+ "/dvxDone { dvxR flushfile dvxA flushfile } bind def\n"
+ "/dvxMask { /dvxH exch def /dvxW exch def gsave 0 exch translate dvxW dvxH scale dvxData dvxW dvxH true [dvxW 0 0 dvxH 0 0] dvxR imagemask dvxDone grestore } bind def\n"
+ "/dvxColor { /dvxH exch def /dvxW exch def gsave 0 exch translate dvxW dvxH scale dvxData dvxW dvxH 1 [dvxW 0 0 dvxH 0 0] dvxR false 4 colorimage dvxDone grestore } bind def\n"
+ "/dvxRe { findfont dup length dict begin { 1 index /FID ne { def } { pop pop } ifelse } forall /Encoding ISOLatin1Encoding def currentdict end definefont pop } bind def\n"
+ "/DvxCourier /Courier dvxRe /dvxF /DvxCourier findfont def\n"
+ "/dvxTop { dvxPage /dvxY exch def /dvxX exch def dvxF dvxSize scalefont setfont } bind def\n"
+ "/L { dvxX dvxY dvxLead sub dup /dvxY exch def moveto show } bind def\n"
+ "/dvxM matrix def\n"
+ "/dvxL { 4 2 roll moveto lineto stroke } bind def\n"
+ "/dvxEA { dvxM currentmatrix pop 6 4 roll translate 4 2 roll scale 0 0 1 5 -2 roll neg exch neg exch arcn dvxM setmatrix } bind def\n"
+ "/dvxE { 0 360 dvxEA closepath } bind def\n"
+ "/dvxBox { 4 2 roll moveto 1 index 0 rlineto 0 exch rlineto neg 0 rlineto closepath } bind def\n"
+ "/dvxD { 0 360 arc fill } bind def\n"
+ "/dvxHz { /dvxQ exch def /dvxP exch def { 0.5 add dvxP exch moveto dvxQ dvxP sub 0 rlineto } for } bind def\n"
+ "/dvxVt { /dvxQ exch def /dvxP exch def { 0.5 add dvxP moveto 0 dvxQ dvxP sub rlineto } for } bind def\n"
+ "/dvxUp { /dvxQ exch def /dvxP exch def { 1 add dup dvxP sub dvxP moveto dvxQ sub dvxQ lineto } for } bind def\n"
+ "/dvxDn { /dvxQ exch def /dvxP exch def { dup dvxP add dvxP moveto dvxQ add dvxQ lineto } for } bind def\n"
+ "/dvxSF { /dvxSy exch def /dvxSx exch def findfont [dvxSx 0 0 dvxSy neg 0 0] makefont setfont } bind def\n"
+ "/dvxT { moveto show } bind def\n"
+ "/dvxImg { /dvxC exch def /dvxH exch def /dvxW exch def gsave 4 2 roll translate scale dvxData dvxW dvxH 8 [dvxW 0 0 dvxH 0 0] dvxR dvxC 1 eq { image } { false 3 colorimage } ifelse dvxDone grestore } bind def\n"
+ "%%EndProlog\n";
+
+#define PS_BAND_FMT "%" PRId32 " %" PRId32 " %" PRId32 " %s\n"
+#define PS_PAGE_FMT "%" PRId32 " dvxBegin\n"
+#define PS_PAGE_END "grestore showpage\n"
+#define PS_SIZES_FMT "/dvxSize %" PRId32 ".%02" PRId32 " def /dvxLead %" PRId32 ".%02" PRId32 " def\n"
+#define PS_TEXT_PAGE "dvxTop\n"
+#define PS_TEXT_PAGE_END "showpage\n"
+#define PS_SHOW_LINE " L\n"
+#define PS_EOF "%%EOF\n"
+#define PS_VEC_PAGE_FMT "0 0 %" PRId32 " %" PRId32 " rectclip 1 setlinecap 1 setlinejoin\n"
+#define PS_HATCH_FMT "%" PRId32 " %d %" PRId32 " %" PRId32 " %" PRId32 " %s\n"
+
+// A PostScript driver's own settings (PrintDriverT.params)
+typedef struct {
+ const char *bandProc; // dvxMask or dvxColor
+ bool color; // 4 bits a dot from the four planes; vector pages in colour
+} PsParamsT;
+
+// Bytes on their way out as ASCII85
+typedef struct {
+ PrintJobT *job;
+ uint8_t group[A85_GROUP];
+ int32_t n; // bytes in group
+ char line[PS_LINE_MAX + A85_CHARS + 1];
+ int32_t col;
+ bool ok;
+} A85T;
+
+static const PsParamsT sPsMono = { "dvxMask", false };
+static const PsParamsT sPsColor = { "dvxColor", true };
+
+// The color image's bit for each ink, indexed by PrintInkE (C, M, Y, K
+// from the top bit of a dot's four)
+static const uint8_t sColorBit[PrintInkCountE] = { 0x1, 0x8, 0x4, 0x2 };
+
+// Words in a family's name that print it in Courier, and in Times (but
+// not with Sans)
+static const char *const sMonoWords[] = { "Mono", "Courier" };
+static const char *const sSerifWords[] = { "Serif", "Times", "Roman", "Tinos", "Ledger" };
+static const char *const sSansWords[] = { "Sans" };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void a85Byte(A85T *a, uint8_t b);
+static bool a85End(A85T *a);
+static void a85Group(A85T *a, int32_t count);
+static void a85Out(A85T *a, const char *text, int32_t len);
+static void a85Put(A85T *a, const uint8_t *data, int32_t len);
+static bool bandInk(const PrintPlanesT *planes);
+static void colorRow(const PrintPlanesT *planes, int32_t r, uint8_t *out);
+static bool dashOn(uint16_t dash, int32_t i);
+static int32_t hatchFirst(int32_t lo);
+static bool nameHas(const char *name, const char *const *words, int32_t count);
+double printPsAscent(const PrintFontT *font);
+int32_t printPsFaceOf(const char *family, uint32_t style);
+int32_t printPsLatin1(uint8_t ch);
+double printPsWidth(const PrintFontT *font, const char *text, int32_t len);
+bool printVecBegin(PrintDocT *doc);
+void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width);
+void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end);
+void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill);
+bool printVecEnd(PrintDocT *doc);
+void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh);
+void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash);
+void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill);
+void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color);
+static bool psBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes);
+static bool psDocBegin(const PrintDriverT *drv, PrintJobT *job);
+static bool psDocEnd(const PrintDriverT *drv, PrintJobT *job);
+static bool psPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots);
+static bool psPageEnd(const PrintDriverT *drv, PrintJobT *job);
+static bool psString(PrintJobT *job, const char *text, int32_t len);
+static bool psTextBegin(PrintJobT *job, int32_t columns, int32_t lines);
+static bool psTextEnd(PrintJobT *job);
+static bool psTextLine(PrintJobT *job, const char *text, int32_t len);
+static bool psTextPageBegin(PrintJobT *job);
+static bool psTextPageEnd(PrintJobT *job);
+static void vecColor(PrintDocT *doc, uint32_t color);
+static void vecDashes(PrintDocT *doc, uint16_t dash, double unit, double phase);
+static void vecFill(PrintDocT *doc, const char *shape, PrintFillE fill, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
+static void vecFont(PrintDocT *doc, const PrintFontT *font);
+static bool vecOn(const PrintDocT *doc);
+static void vecPen(PrintDocT *doc, int32_t width, bool dashed);
+static void vecPrintf(PrintDocT *doc, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+
+static const PrintTextOpsT sPsText = { psTextBegin, psTextPageBegin, psTextLine, psTextPageEnd, psTextEnd };
+
+const PrintDriverT printDrvPostScript = {
+ PS_DPI, PS_DPI, PS_DPI * PRINT_PAGE_WIDTH_IN, PS_DPI * PRINT_PAGE_HEIGHT_IN, PS_BAND_ROWS,
+ PRINT_DITHER_LARGE, 1, printInksBlack, "", "", PRINT_LINES_LASER, &sPsText, &sPsMono,
+ psDocBegin, psPageBegin, psBand, psPageEnd, psDocEnd, true
+};
+
+const PrintDriverT printDrvPostScriptColor = {
+ PS_DPI, PS_DPI, PS_DPI * PRINT_PAGE_WIDTH_IN, PS_DPI * PRINT_PAGE_HEIGHT_IN, PS_BAND_ROWS,
+ PRINT_DITHER_LARGE, PRINT_PLANES_MAX, printInksKcmy, "", "", PRINT_LINES_LASER, &sPsText, &sPsColor,
+ psDocBegin, psPageBegin, psBand, psPageEnd, psDocEnd, true
+};
+
+
+static void a85Byte(A85T *a, uint8_t b) {
+ a->group[a->n++] = b;
+
+ if (a->n == A85_GROUP) {
+ a85Group(a, A85_GROUP);
+ }
+}
+
+
+// The last, short group, the end mark, and a line end.
+static bool a85End(A85T *a) {
+ if (a->n > 0) {
+ int32_t count = a->n;
+
+ memset(a->group + count, 0, (size_t)(A85_GROUP - count));
+ a85Group(a, count);
+ }
+
+ a85Out(a, A85_END "\n", (int32_t)strlen(A85_END "\n"));
+ a->ok = a->ok && printJobWrite(a->job, a->line, a->col);
+ a->col = 0;
+ return a->ok;
+}
+
+
+// The group as five characters (z for four zero bytes); a short group of
+// count bytes as count + 1 characters.
+static void a85Group(A85T *a, int32_t count) {
+ uint32_t v = ((uint32_t)a->group[0] << 24) | ((uint32_t)a->group[1] << 16) | ((uint32_t)a->group[2] << 8) | a->group[3];
+ char c[A85_CHARS];
+
+ a->n = 0;
+
+ if (v == 0 && count == A85_GROUP) {
+ c[0] = A85_ZERO;
+ a85Out(a, c, 1);
+ return;
+ }
+
+ for (int32_t i = A85_CHARS - 1; i >= 0; i--) {
+ c[i] = (char)(A85_FIRST + v % A85_BASE);
+ v /= A85_BASE;
+ }
+
+ a85Out(a, c, count + 1);
+}
+
+
+// Characters into the line, which goes out (with a line end) when full.
+static void a85Out(A85T *a, const char *text, int32_t len) {
+ memcpy(a->line + a->col, text, (size_t)len);
+ a->col += len;
+
+ if (a->col >= PS_LINE_MAX) {
+ a->line[a->col++] = '\n';
+ a->ok = a->ok && printJobWrite(a->job, a->line, a->col);
+ a->col = 0;
+ }
+}
+
+
+static void a85Put(A85T *a, const uint8_t *data, int32_t len) {
+ for (int32_t i = 0; i < len; i++) {
+ a85Byte(a, data[i]);
+ }
+}
+
+
+// Whether any plane has a dot in the band.
+static bool bandInk(const PrintPlanesT *planes) {
+ for (int32_t p = 0; p < planes->count; p++) {
+ for (int32_t i = 0; i < planes->rowBytes * planes->rows; i++) {
+ if (planes->bits[p][i]) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+// Row r of the band as the color image's dots: four bits each, two to a
+// byte, from the planes.
+static void colorRow(const PrintPlanesT *planes, int32_t r, uint8_t *out) {
+ memset(out, 0, (size_t)((planes->widthDots + 1) / PS_DOTS_PER_BYTE));
+
+ for (int32_t p = 0; p < planes->count; p++) {
+ uint8_t bit = sColorBit[planes->ink[p]];
+
+ for (int32_t x = 0; x < planes->widthDots; x++) {
+ if (printPlaneDot(planes, p, r, x)) {
+ out[x / PS_DOTS_PER_BYTE] |= (uint8_t)(x % PS_DOTS_PER_BYTE ? bit : bit << PS_COLOR_BITS);
+ }
+ }
+ }
+}
+
+
+// Whether step i (any whole number: the pattern repeats) of a dash
+// pattern is drawn.
+static bool dashOn(uint16_t dash, int32_t i) {
+ return (dash & (DASH_TOP >> (((i % DASH_BITS) + DASH_BITS) % DASH_BITS))) != 0;
+}
+
+
+// The first hatch line at or after lo: a multiple of PRINT_HATCH_DOTS.
+static int32_t hatchFirst(int32_t lo) {
+ return lo + ((PRINT_HATCH_DOTS - (lo & HATCH_MASK)) & HATCH_MASK);
+}
+
+
+// Whether name holds any of words (case ignored).
+static bool nameHas(const char *name, const char *const *words, int32_t count) {
+ for (int32_t w = 0; w < count; w++) {
+ size_t n = strlen(words[w]);
+
+ for (const char *p = name; *p; p++) {
+ if (strncasecmp(p, words[w], n) == 0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+// The baseline below the top of a line: the ascender, and half the
+// leading beyond the ascender and descender.
+double printPsAscent(const PrintFontT *font) {
+ const PrintPsFaceT *face = &printPsFaces[font->psFace];
+ int32_t extra = PRINT_PS_LEADING - (face->ascender - face->descender);
+
+ return (face->ascender + extra / 2.0) * font->psSizeY / PRINT_PS_UNITS;
+}
+
+
+int32_t printPsFaceOf(const char *family, uint32_t style) {
+ const char *name = family && family[0] ? family : FONT_SYSTEM_FAMILY;
+ int32_t face = PRINT_PS_SANS;
+
+ if (dvxFontFamilyFixed(name) || nameHas(name, sMonoWords, DVX_ARRAY_LEN(sMonoWords))) {
+ face = PRINT_PS_MONO;
+ } else if (nameHas(name, sSerifWords, DVX_ARRAY_LEN(sSerifWords)) && !nameHas(name, sSansWords, DVX_ARRAY_LEN(sSansWords))) {
+ face = PRINT_PS_SERIF;
+ }
+
+ face += (style & FONT_STYLE_BOLD) ? PRINT_PS_BOLD : 0;
+ face += (style & FONT_STYLE_ITALIC) ? PRINT_PS_SLANT : 0;
+ return face;
+}
+
+
+int32_t printPsLatin1(uint8_t ch) {
+ uint32_t u = dfnCp437[ch];
+
+ if (ch >= PRINT_PS_FIRST_CHAR && ch < LATIN1_FIRST && u == ch) {
+ return ch;
+ }
+
+ if (u >= LATIN1_FIRST && u <= LATIN1_LAST) {
+ return (int32_t)u;
+ }
+
+ if (u == BOX_H || u == BOX_DOUBLE_H) {
+ return '-';
+ }
+
+ if (u == BOX_V || u == BOX_DOUBLE_V) {
+ return '|';
+ }
+
+ if (u >= BOX_FIRST && u < BLOCK_FIRST) {
+ return '+';
+ }
+
+ return (u >= BLOCK_FIRST && u <= BLOCK_LAST) ? '#' : '?';
+}
+
+
+// Dots across len characters of text in a printer's face (not rounded).
+double printPsWidth(const PrintFontT *font, const char *text, int32_t len) {
+ const PrintPsFaceT *face = &printPsFaces[font->psFace];
+ int32_t sum = 0;
+
+ for (int32_t i = 0; i < len; i++) {
+ sum += face->widths[printPsLatin1((uint8_t)text[i]) - PRINT_PS_FIRST_CHAR];
+ }
+
+ return sum * font->psSizeX / PRINT_PS_UNITS;
+}
+
+
+// The page started, clipped to the printable area, with round line ends
+// and corners; the pen and font as a page starts.
+bool printVecBegin(PrintDocT *doc) {
+ if (!doc || !doc->ok || !doc->drv->vector || doc->vecPage) {
+ return false;
+ }
+
+ const PrintDriverT *drv = doc->drv;
+
+ doc->ok = drv->pageBegin(drv, doc->job, drv->pageWidthDots, drv->pageHeightDots);
+ vecPrintf(doc, PS_VEC_PAGE_FMT, drv->pageWidthDots, drv->pageHeightDots);
+ doc->vecPage = doc->ok;
+ doc->vecColor = PS_BLACK;
+ doc->vecWidth = 1;
+ doc->vecDashed = false;
+ doc->vecFace = PRINT_PS_NONE;
+ return doc->vecPage;
+}
+
+
+void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width) {
+ if (!vecOn(doc)) {
+ return;
+ }
+
+ vecColor(doc, color);
+ vecPrintf(doc, "%.6g %.6g %.6g dvxD\n", x + DOT_CENTRE, y + DOT_CENTRE, (width < 1 ? 1 : width) / 2.0);
+}
+
+
+// The outline through the dots' centres, dashed from angle 0 (an arc's
+// path starts its start along the pattern); an ellipse with no width or
+// height is the line it has become.
+void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end) {
+ if (!vecOn(doc) || dash == DASH_NONE) {
+ return;
+ }
+
+ if (rx <= 0 || ry <= 0) {
+ printVecLine(doc, cx - rx, cy - ry, cx + rx, cy + ry, color, width, dash);
+ return;
+ }
+
+ vecColor(doc, color);
+ vecPen(doc, width, dash != DASH_SOLID);
+
+ if (dash != DASH_SOLID) {
+ vecDashes(doc, dash, 1.0, arc ? start * (rx + ry) / 2.0 : 0.0);
+ }
+
+ if (arc) {
+ vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " %.6g %.6g dvxEA stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry, start * DEGREES_PER_RADIAN, end * DEGREES_PER_RADIAN);
+ } else {
+ vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " dvxE stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry);
+ }
+}
+
+
+// Out to the edges of the dots rx and ry from the centre.
+void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill) {
+ char shape[PS_VEC_CMD_MAX];
+
+ if (!vecOn(doc)) {
+ return;
+ }
+
+ rx = rx < 0 ? -rx : rx;
+ ry = ry < 0 ? -ry : ry;
+ vecColor(doc, color);
+ snprintf(shape, sizeof(shape), "%.6g %.6g %.6g %.6g dvxE", cx + DOT_CENTRE, cy + DOT_CENTRE, rx + DOT_CENTRE, ry + DOT_CENTRE);
+ vecFill(doc, shape, fill, cx - rx, cy - ry, cx + rx, cy + ry);
+}
+
+
+bool printVecEnd(PrintDocT *doc) {
+ if (!doc || !doc->vecPage) {
+ return false;
+ }
+
+ doc->vecPage = false;
+ doc->ok = doc->ok && doc->drv->pageEnd(doc->drv, doc->job) && !printJobCancelled(doc->job);
+ return doc->ok;
+}
+
+
+// The part of the picture as an image of its own dots, grey on a
+// black-and-white printer, the rows packed and in ASCII85.
+void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) {
+ if (!vecOn(doc) || !rgb || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 || sx < 0 || sy < 0 || sx + sw > w || sy + sh > h) {
+ return;
+ }
+
+ const PsParamsT *pp = (const PsParamsT *)doc->drv->params;
+ int32_t comps = pp->color ? RGB_BYTES : GRAY_BYTES;
+ int32_t rowLen = sw * comps;
+ uint8_t *row = (uint8_t *)malloc((size_t)rowLen);
+ uint8_t *packed = (uint8_t *)malloc((size_t)PRINT_PACK_MAX(rowLen));
+ uint8_t eod = RLE_EOD;
+ A85T a;
+
+ if (!row || !packed) {
+ free(row);
+ free(packed);
+ doc->ok = false;
+ return;
+ }
+
+ memset(&a, 0, sizeof(a));
+ a.job = doc->job;
+ a.ok = true;
+ vecPrintf(doc, "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxImg\n", dx, dy, dw, dh, sw, sh, comps);
+
+ for (int32_t r = 0; a.ok && doc->ok && r < sh; r++) {
+ const uint8_t *src = rgb + ((int64_t)(sy + r) * w + sx) * RGB_BYTES;
+
+ if (pp->color) {
+ memcpy(row, src, (size_t)rowLen);
+ } else {
+ for (int32_t i = 0; i < sw; i++) {
+ row[i] = (uint8_t)printLuma(src + i * RGB_BYTES);
+ }
+ }
+
+ a85Put(&a, packed, printPackBits(row, rowLen, packed));
+ }
+
+ a85Put(&a, &eod, 1);
+ doc->ok = doc->ok && a.ok && a85End(&a);
+ free(row);
+ free(packed);
+}
+
+
+// A solid line through the dots' centres, round at the ends; a dashed
+// one in steps of its longer side in dots, a bit of dash each from the
+// top one, each step centred on its dot (the ends half a step out, as
+// dots), square at the ends.
+void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash) {
+ if (!vecOn(doc) || dash == DASH_NONE || !(fabs(x0) <= DBL_MAX && fabs(y0) <= DBL_MAX && fabs(x1) <= DBL_MAX && fabs(y1) <= DBL_MAX)) {
+ return;
+ }
+
+ x0 += DOT_CENTRE;
+ y0 += DOT_CENTRE;
+ x1 += DOT_CENTRE;
+ y1 += DOT_CENTRE;
+ vecColor(doc, color);
+
+ if (dash == DASH_SOLID) {
+ vecPen(doc, width, false);
+ vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0, y0, x1, y1);
+ return;
+ }
+
+ double ax = fabs(floor(x1) - floor(x0));
+ double ay = fabs(floor(y1) - floor(y0));
+ double steps = ax > ay ? ax : ay;
+
+ if (steps == 0.0) {
+ if (dash & DASH_TOP) {
+ printVecDot(doc, (int32_t)floor(x0), (int32_t)floor(y0), color, width);
+ }
+
+ return;
+ }
+
+ double len = sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
+ double unit = len / steps;
+ double ex = (x1 - x0) / len * unit * DOT_CENTRE;
+ double ey = (y1 - y0) / len * unit * DOT_CENTRE;
+
+ vecPen(doc, width, true);
+ vecDashes(doc, dash, unit, 0.0);
+ vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0 - ex, y0 - ey, x1 + ex, y1 + ey);
+}
+
+
+void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill) {
+ int32_t left = x0 < x1 ? x0 : x1;
+ int32_t right = x0 < x1 ? x1 : x0;
+ int32_t top = y0 < y1 ? y0 : y1;
+ int32_t bottom = y0 < y1 ? y1 : y0;
+ char shape[PS_VEC_CMD_MAX];
+
+ if (!vecOn(doc)) {
+ return;
+ }
+
+ vecColor(doc, color);
+ snprintf(shape, sizeof(shape), "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxBox", left, top, right - left + 1, bottom - top + 1);
+ vecFill(doc, shape, fill, left, top, right, bottom);
+}
+
+
+// The text at its baseline, then its underline and strike-through as
+// the face has them (the strike-through half the x-height up).
+void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color) {
+ if (!vecOn(doc) || !font || font->psFace == PRINT_PS_NONE || !text || len <= 0) {
+ return;
+ }
+
+ const PrintPsFaceT *face = &printPsFaces[font->psFace];
+ double base = y + printPsAscent(font);
+ double wide = printPsWidth(font, text, len);
+ double thick = face->underlineThick * font->psSizeY / PRINT_PS_UNITS;
+ double lines[TEXT_LINES_MAX];
+ int32_t count = 0;
+
+ vecColor(doc, color);
+ vecFont(doc, font);
+ doc->ok = doc->ok && psString(doc->job, text, len);
+ vecPrintf(doc, " %" PRId32 " %.6g dvxT\n", x, base);
+
+ if (font->style & FONT_STYLE_UNDERLINE) {
+ lines[count++] = base - face->underlinePos * font->psSizeY / PRINT_PS_UNITS;
+ }
+
+ if (font->style & FONT_STYLE_STRIKE) {
+ lines[count++] = base - face->xHeight / 2.0 * font->psSizeY / PRINT_PS_UNITS;
+ }
+
+ for (int32_t i = 0; i < count; i++) {
+ vecPrintf(doc, "%" PRId32 " %.6g %.6g %.6g rectfill\n", x, lines[i] - thick / 2.0, wide, thick);
+ }
+}
+
+
+// A band with ink: its place and size, then its rows packed and in
+// ASCII85 (the color image's rows built from the planes).
+static bool psBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes) {
+ const PsParamsT *pp = (const PsParamsT *)drv->params;
+
+ if (!bandInk(planes)) {
+ return true;
+ }
+
+ int32_t rowLen = pp->color ? (planes->widthDots + 1) / PS_DOTS_PER_BYTE : planes->rowBytes;
+ uint8_t *row = pp->color ? (uint8_t *)malloc((size_t)rowLen) : NULL;
+ uint8_t *packed = (uint8_t *)malloc((size_t)PRINT_PACK_MAX(rowLen));
+ char cmd[PS_CMD_MAX];
+ A85T a;
+
+ if (!packed || (pp->color && !row)) {
+ free(row);
+ free(packed);
+ return false;
+ }
+
+ memset(&a, 0, sizeof(a));
+ a.job = job;
+ snprintf(cmd, sizeof(cmd), PS_BAND_FMT, planes->y0, planes->widthDots, planes->rows, pp->bandProc);
+ a.ok = printJobPuts(job, cmd);
+
+ for (int32_t r = 0; a.ok && r < planes->rows; r++) {
+ const uint8_t *src = planes->bits[0] + r * planes->rowBytes;
+
+ if (pp->color) {
+ colorRow(planes, r, row);
+ src = row;
+ }
+
+ a85Put(&a, packed, printPackBits(src, rowLen, packed));
+ }
+
+ uint8_t eod = RLE_EOD;
+
+ a85Put(&a, &eod, 1);
+ free(row);
+ free(packed);
+ return a.ok && a85End(&a);
+}
+
+
+static bool psDocBegin(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, sProlog);
+}
+
+
+static bool psDocEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, PS_EOF);
+}
+
+
+static bool psPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots) {
+ char cmd[PS_CMD_MAX];
+
+ (void)widthDots;
+ (void)heightDots;
+ snprintf(cmd, sizeof(cmd), PS_PAGE_FMT, drv->dpiX);
+ return printJobPuts(job, cmd);
+}
+
+
+static bool psPageEnd(const PrintDriverT *drv, PrintJobT *job) {
+ (void)drv;
+ return printJobPuts(job, PS_PAGE_END);
+}
+
+
+// (text): ( ) and \ escaped, CP437 as Latin-1 (past ASCII in octal), a
+// long string going on over lines.
+static bool psString(PrintJobT *job, const char *text, int32_t len) {
+ char out[PS_STRING_LINE + PS_STRING_ESCAPE + 1];
+ int32_t n = 0;
+ bool ok = true;
+
+ out[n++] = '(';
+
+ for (int32_t i = 0; i < len; i++) {
+ int32_t c = printPsLatin1((uint8_t)text[i]);
+
+ if (c == '(' || c == ')' || c == '\\') {
+ out[n++] = '\\';
+ out[n++] = (char)c;
+ } else if (c >= LATIN1_FIRST) {
+ n += snprintf(out + n, sizeof(out) - (size_t)n, OCTAL_ESCAPE, (unsigned)c);
+ } else {
+ out[n++] = (char)c;
+ }
+
+ if (n >= PS_STRING_LINE && i < len - 1) {
+ ok = ok && printJobWrite(job, out, n) && printJobPuts(job, PS_STRING_CONTINUE);
+ n = 0;
+ }
+ }
+
+ out[n++] = ')';
+ return ok && printJobWrite(job, out, n);
+}
+
+
+// The prolog and the text's size: Courier as large as fills the columns
+// across and fits the lines down.
+static bool psTextBegin(PrintJobT *job, int32_t columns, int32_t lines) {
+ int32_t across = POINTS_WIDTH * HUNDREDTHS * HUNDREDTHS / (columns * COURIER_ADVANCE);
+ int32_t lead = POINTS_HEIGHT * HUNDREDTHS / lines;
+ int32_t size = across < lead ? across : lead;
+ char cmd[PS_CMD_MAX];
+
+ snprintf(cmd, sizeof(cmd), PS_SIZES_FMT, size / HUNDREDTHS, size % HUNDREDTHS, lead / HUNDREDTHS, lead % HUNDREDTHS);
+ return printJobPuts(job, sProlog) && printJobPuts(job, cmd);
+}
+
+
+static bool psTextEnd(PrintJobT *job) {
+ return printJobPuts(job, PS_EOF);
+}
+
+
+static bool psTextLine(PrintJobT *job, const char *text, int32_t len) {
+ return psString(job, text, len) && printJobPuts(job, PS_SHOW_LINE);
+}
+
+
+static bool psTextPageBegin(PrintJobT *job) {
+ return printJobPuts(job, PS_TEXT_PAGE);
+}
+
+
+static bool psTextPageEnd(PrintJobT *job) {
+ return printJobPuts(job, PS_TEXT_PAGE_END);
+}
+
+
+// The colour, sent when it changed: grey (its brightness) on a
+// black-and-white printer.
+static void vecColor(PrintDocT *doc, uint32_t color) {
+ const PsParamsT *pp = (const PsParamsT *)doc->drv->params;
+ uint8_t px[RGB_BYTES];
+
+ if (color == doc->vecColor) {
+ return;
+ }
+
+ px[0] = (uint8_t)((color >> RGB_RED_SHIFT) & RGB_MASK);
+ px[1] = (uint8_t)((color >> RGB_GREEN_SHIFT) & RGB_MASK);
+ px[2] = (uint8_t)(color & RGB_MASK);
+ doc->vecColor = color;
+
+ if (pp->color) {
+ vecPrintf(doc, "%.3g %.3g %.3g setrgbcolor\n", px[0] / COLOR_LEVELS, px[1] / COLOR_LEVELS, px[2] / COLOR_LEVELS);
+ } else {
+ vecPrintf(doc, "%.3g setgray\n", printLuma(px) / COLOR_LEVELS);
+ }
+}
+
+
+// A dash pattern (neither solid nor empty) as PostScript's: its runs from
+// the start of a run of dashes, unit long a step, the path starting phase
+// steps into the pattern.
+static void vecDashes(PrintDocT *doc, uint16_t dash, double unit, double phase) {
+ char cmd[PS_VEC_CMD_MAX];
+ int32_t from = 0;
+ int32_t run = 0;
+ int32_t n;
+ double offset;
+
+ while (!dashOn(dash, from) || dashOn(dash, from - 1)) {
+ from++;
+ }
+
+ n = snprintf(cmd, sizeof(cmd), "[");
+
+ for (int32_t i = 0; i < DASH_BITS; i++) {
+ run++;
+
+ if (dashOn(dash, from + i) != dashOn(dash, from + i + 1)) {
+ n += snprintf(cmd + n, sizeof(cmd) - (size_t)n, "%.6g ", run * unit);
+ run = 0;
+ }
+ }
+
+ offset = phase - from;
+ offset = offset - DASH_BITS * floor(offset / DASH_BITS);
+ vecPrintf(doc, "%s] %.6g setdash\n", cmd, offset * unit);
+}
+
+
+// A shape (the operands and procedure of its path) filled: solid, or
+// its hatch lines, one dot wide, over its box x0 .. x1, y0 .. y1 with
+// the shape as the clip.
+static void vecFill(PrintDocT *doc, const char *shape, PrintFillE fill, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
+ if (fill == PrintFillSolidE) {
+ vecPrintf(doc, "%s fill\n", shape);
+ return;
+ }
+
+ vecPrintf(doc, "gsave %s clip newpath\n", shape);
+
+ if (fill == PrintFillHorizontalE || fill == PrintFillCrossE) {
+ vecPrintf(doc, PS_HATCH_FMT, hatchFirst(y0), PRINT_HATCH_DOTS, y1, x0, x1 + 1, "dvxHz");
+ }
+
+ if (fill == PrintFillVerticalE || fill == PrintFillCrossE) {
+ vecPrintf(doc, PS_HATCH_FMT, hatchFirst(x0), PRINT_HATCH_DOTS, x1, y0, y1 + 1, "dvxVt");
+ }
+
+ if (fill == PrintFillUpDiagonalE || fill == PrintFillDiagonalCrossE) {
+ vecPrintf(doc, PS_HATCH_FMT, hatchFirst(x0 + y0), PRINT_HATCH_DOTS, x1 + y1, y0, y1 + 1, "dvxUp");
+ }
+
+ if (fill == PrintFillDownDiagonalE || fill == PrintFillDiagonalCrossE) {
+ vecPrintf(doc, PS_HATCH_FMT, hatchFirst(x0 - y1), PRINT_HATCH_DOTS, x1 - y0, y0, y1 + 1, "dvxDn");
+ }
+
+ vecPrintf(doc, "1 setlinewidth 0 setlinecap stroke grestore\n");
+}
+
+
+// The font's face made Latin-1 in the job (once), and the font sent
+// when it changed.
+static void vecFont(PrintDocT *doc, const PrintFontT *font) {
+ uint16_t bit = (uint16_t)(1u << font->psFace);
+
+ if (!(doc->psFaces & bit)) {
+ vecPrintf(doc, "/DvxF%" PRId32 " /%s dvxRe\n", font->psFace, printPsFaces[font->psFace].name);
+ doc->psFaces |= bit;
+ }
+
+ if (font->psFace != doc->vecFace || font->psSizeX != doc->vecSizeX || font->psSizeY != doc->vecSizeY) {
+ vecPrintf(doc, "/DvxF%" PRId32 " %.6g %.6g dvxSF\n", font->psFace, font->psSizeX, font->psSizeY);
+ doc->vecFace = font->psFace;
+ doc->vecSizeX = font->psSizeX;
+ doc->vecSizeY = font->psSizeY;
+ }
+}
+
+
+// Whether vector drawing goes out: on a vector page of a document that
+// has not failed.
+static bool vecOn(const PrintDocT *doc) {
+ return doc && doc->vecPage && doc->ok;
+}
+
+
+// The line width (at least a dot) and ends (round, or square for dashes,
+// whose pattern then each line sets), sent when they changed.
+static void vecPen(PrintDocT *doc, int32_t width, bool dashed) {
+ width = width < 1 ? 1 : width;
+
+ if (dashed != doc->vecDashed) {
+ vecPrintf(doc, dashed ? "0 setlinecap\n" : "[] 0 setdash 1 setlinecap\n");
+ doc->vecDashed = dashed;
+ }
+
+ if (width != doc->vecWidth) {
+ vecPrintf(doc, "%" PRId32 " setlinewidth\n", width);
+ doc->vecWidth = width;
+ }
+}
+
+
+static void vecPrintf(PrintDocT *doc, const char *fmt, ...) {
+ char buf[PS_VEC_CMD_MAX];
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(buf, sizeof(buf), fmt, ap);
+ va_end(ap);
+ doc->ok = doc->ok && printJobPuts(doc->job, buf);
+}
diff --git a/src/libs/kpunch/print/printPsAfm.c b/src/libs/kpunch/print/printPsAfm.c
new file mode 100644
index 00000000..fd429326
--- /dev/null
+++ b/src/libs/kpunch/print/printPsAfm.c
@@ -0,0 +1,264 @@
+// printPsAfm.c -- Printing library: the widths of the PostScript
+// printers' own text faces (printInt.h), made by src/tools/afmWidths.py
+// -- do not edit.
+//
+// Modified from Adobe's Core 14 AFM files: only the character widths (in
+// thousandths of the font size, in ISOLatin1Encoding order from code 32),
+// the ascender, descender, x-height, and underline of twelve faces are
+// kept, as C tables. The files' notices:
+//
+// Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.
+// All Rights Reserved. Helvetica is a trademark of Linotype-Hell AG
+// and/or its subsidiaries.
+//
+// Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved. Times is a trademark of
+// Linotype-Hell AG and/or its subsidiaries.
+//
+// Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems
+// Incorporated. All Rights Reserved.
+//
+// Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated.
+// All Rights Reserved.
+//
+// The licence they come with (MustRead.html):
+//
+// This file and the 14 PostScript(R) AFM files it accompanies may be used,
+// copied, and distributed for any purpose and without charge, with or
+// without modification, provided that all copyright notices are retained;
+// that the AFM files are not distributed without this file; that all
+// modifications to this file or any of the AFM files are prominently noted
+// in the modified file(s); and that this paragraph is not modified.
+
+#include "printInt.h"
+
+const PrintPsFaceT printPsFaces[PRINT_PS_FACES] = {
+ {
+ "Helvetica", 718, -207, 523, -100, 50,
+ {
+ 278, 278, 355, 556, 556, 889, 667, 222, 333, 333, 389, 584, 278, 584, 278, 278,
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
+ 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
+ 222, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
+ 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
+ 400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
+ 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
+ 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
+ 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
+ 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500
+ }
+ },
+ {
+ "Helvetica-Bold", 718, -207, 532, -100, 50,
+ {
+ 278, 333, 474, 556, 556, 889, 722, 278, 333, 333, 389, 584, 278, 584, 278, 278,
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
+ 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
+ 278, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
+ 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
+ 400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
+ 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
+ 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
+ 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
+ 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556
+ }
+ },
+ {
+ "Helvetica-Oblique", 718, -207, 523, -100, 50,
+ {
+ 278, 278, 355, 556, 556, 889, 667, 222, 333, 333, 389, 584, 278, 584, 278, 278,
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556,
+ 1015, 667, 667, 722, 722, 667, 611, 778, 722, 278, 500, 667, 556, 833, 722, 778,
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 278, 278, 278, 469, 556,
+ 222, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500, 222, 833, 556, 556,
+ 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 278, 333, 556, 556, 556, 556, 260, 556, 333, 737, 370, 556, 584, 333, 737, 333,
+ 400, 584, 333, 333, 333, 556, 537, 278, 333, 333, 365, 556, 834, 834, 834, 611,
+ 667, 667, 667, 667, 667, 667, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
+ 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
+ 556, 556, 556, 556, 556, 556, 889, 500, 556, 556, 556, 556, 278, 278, 278, 278,
+ 556, 556, 556, 556, 556, 556, 556, 584, 611, 556, 556, 556, 556, 500, 556, 500
+ }
+ },
+ {
+ "Helvetica-BoldOblique", 718, -207, 532, -100, 50,
+ {
+ 278, 333, 474, 556, 556, 889, 722, 278, 333, 333, 389, 584, 278, 584, 278, 278,
+ 556, 556, 556, 556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611,
+ 975, 722, 722, 722, 722, 667, 611, 778, 722, 278, 556, 722, 611, 833, 722, 778,
+ 667, 778, 722, 667, 611, 722, 667, 944, 667, 667, 611, 333, 278, 333, 584, 556,
+ 278, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556, 278, 889, 611, 611,
+ 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 278, 333, 556, 556, 556, 556, 280, 556, 333, 737, 370, 556, 584, 333, 737, 333,
+ 400, 584, 333, 333, 333, 611, 556, 278, 333, 333, 365, 556, 834, 834, 834, 611,
+ 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 278, 278, 278, 278,
+ 722, 722, 778, 778, 778, 778, 778, 584, 778, 722, 722, 722, 722, 667, 667, 611,
+ 556, 556, 556, 556, 556, 556, 889, 556, 556, 556, 556, 556, 278, 278, 278, 278,
+ 611, 611, 611, 611, 611, 611, 611, 584, 611, 611, 611, 611, 611, 556, 611, 556
+ }
+ },
+ {
+ "Times-Roman", 683, -217, 450, -100, 50,
+ {
+ 250, 333, 408, 500, 500, 833, 778, 333, 333, 333, 500, 564, 250, 564, 250, 278,
+ 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 278, 278, 564, 564, 564, 444,
+ 921, 722, 667, 667, 722, 611, 556, 722, 722, 333, 389, 722, 611, 889, 722, 722,
+ 556, 722, 667, 556, 611, 722, 722, 944, 722, 722, 611, 333, 278, 333, 469, 500,
+ 333, 444, 500, 444, 500, 444, 333, 500, 500, 278, 278, 500, 278, 778, 500, 500,
+ 500, 500, 333, 389, 278, 500, 500, 722, 500, 500, 444, 480, 200, 480, 541, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 250, 333, 500, 500, 500, 500, 200, 500, 333, 760, 276, 500, 564, 333, 760, 333,
+ 400, 564, 300, 300, 333, 500, 453, 250, 333, 300, 310, 500, 750, 750, 750, 444,
+ 722, 722, 722, 722, 722, 722, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
+ 722, 722, 722, 722, 722, 722, 722, 564, 722, 722, 722, 722, 722, 722, 556, 500,
+ 444, 444, 444, 444, 444, 444, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
+ 500, 500, 500, 500, 500, 500, 500, 564, 500, 500, 500, 500, 500, 500, 500, 500
+ }
+ },
+ {
+ "Times-Bold", 683, -217, 461, -100, 50,
+ {
+ 250, 333, 555, 500, 500, 1000, 833, 333, 333, 333, 500, 570, 250, 570, 250, 278,
+ 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
+ 930, 722, 667, 722, 722, 667, 611, 778, 778, 389, 500, 778, 667, 944, 722, 778,
+ 611, 778, 722, 556, 667, 722, 722, 1000, 722, 722, 667, 333, 278, 333, 581, 500,
+ 333, 500, 556, 444, 556, 444, 333, 500, 556, 278, 333, 556, 278, 833, 556, 500,
+ 556, 556, 444, 389, 333, 556, 500, 722, 500, 500, 444, 394, 220, 394, 520, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 250, 333, 500, 500, 500, 500, 220, 500, 333, 747, 300, 500, 570, 333, 747, 333,
+ 400, 570, 300, 300, 333, 556, 540, 250, 333, 300, 330, 500, 750, 750, 750, 500,
+ 722, 722, 722, 722, 722, 722, 1000, 722, 667, 667, 667, 667, 389, 389, 389, 389,
+ 722, 722, 778, 778, 778, 778, 778, 570, 778, 722, 722, 722, 722, 722, 611, 556,
+ 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
+ 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 500, 556, 500
+ }
+ },
+ {
+ "Times-Italic", 683, -217, 441, -100, 50,
+ {
+ 250, 333, 420, 500, 500, 833, 778, 333, 333, 333, 500, 675, 250, 675, 250, 278,
+ 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 675, 675, 675, 500,
+ 920, 611, 611, 667, 722, 611, 611, 722, 722, 333, 444, 667, 556, 833, 667, 722,
+ 611, 722, 611, 500, 556, 722, 611, 833, 611, 556, 556, 389, 278, 389, 422, 500,
+ 333, 500, 500, 444, 500, 444, 278, 500, 500, 278, 278, 444, 278, 722, 500, 500,
+ 500, 500, 389, 389, 278, 500, 444, 667, 444, 444, 389, 400, 275, 400, 541, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 250, 389, 500, 500, 500, 500, 275, 500, 333, 760, 276, 500, 675, 333, 760, 333,
+ 400, 675, 300, 300, 333, 500, 523, 250, 333, 300, 310, 500, 750, 750, 750, 500,
+ 611, 611, 611, 611, 611, 611, 889, 667, 611, 611, 611, 611, 333, 333, 333, 333,
+ 722, 667, 722, 722, 722, 722, 722, 675, 722, 722, 722, 722, 722, 556, 611, 500,
+ 500, 500, 500, 500, 500, 500, 667, 444, 444, 444, 444, 444, 278, 278, 278, 278,
+ 500, 500, 500, 500, 500, 500, 500, 675, 500, 500, 500, 500, 500, 444, 500, 444
+ }
+ },
+ {
+ "Times-BoldItalic", 683, -217, 462, -100, 50,
+ {
+ 250, 389, 555, 500, 500, 833, 778, 333, 333, 333, 500, 570, 250, 606, 250, 278,
+ 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 333, 333, 570, 570, 570, 500,
+ 832, 667, 667, 667, 722, 667, 667, 722, 778, 389, 500, 667, 611, 889, 722, 722,
+ 611, 722, 667, 556, 611, 722, 667, 889, 667, 611, 611, 333, 278, 333, 570, 500,
+ 333, 500, 500, 444, 500, 444, 333, 500, 556, 278, 278, 500, 278, 778, 556, 500,
+ 500, 500, 389, 389, 278, 556, 444, 667, 500, 444, 389, 348, 220, 348, 570, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 278, 333, 333, 333, 333, 333, 333, 333, 333, 0, 333, 333, 0, 333, 333, 333,
+ 250, 389, 500, 500, 500, 500, 220, 500, 333, 747, 266, 500, 606, 333, 747, 333,
+ 400, 570, 300, 300, 333, 576, 500, 250, 333, 300, 300, 500, 750, 750, 750, 500,
+ 667, 667, 667, 667, 667, 667, 944, 667, 667, 667, 667, 667, 389, 389, 389, 389,
+ 722, 722, 722, 722, 722, 722, 722, 570, 722, 722, 722, 722, 722, 611, 611, 500,
+ 500, 500, 500, 500, 500, 500, 722, 444, 444, 444, 444, 444, 278, 278, 278, 278,
+ 500, 556, 500, 500, 500, 500, 500, 570, 500, 556, 556, 556, 556, 444, 500, 444
+ }
+ },
+ {
+ "Courier", 629, -157, 426, -100, 50,
+ {
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 0, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600
+ }
+ },
+ {
+ "Courier-Bold", 629, -157, 439, -100, 50,
+ {
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 0, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600
+ }
+ },
+ {
+ "Courier-Oblique", 629, -157, 426, -100, 50,
+ {
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 0, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600
+ }
+ },
+ {
+ "Courier-BoldOblique", 629, -157, 439, -100, 50,
+ {
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 0, 600, 600, 0, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600,
+ 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600, 600
+ }
+ }
+};
diff --git a/src/libs/kpunch/print/printRaster.c b/src/libs/kpunch/print/printRaster.c
index d4b44a01..d5c00429 100644
--- a/src/libs/kpunch/print/printRaster.c
+++ b/src/libs/kpunch/print/printRaster.c
@@ -20,303 +20,287 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
-// printRaster.c -- Printing library: pictures and the printer drivers
-// (see print.h)
+// printRaster.c -- Printing library: documents of picture pages, and
+// the page pipeline every printer driver is fed by (see print.h,
+// printInt.h)
//
-// printRasterJob scales the picture (nearest pixel) to the largest size
-// that fits the driver's printable area, keeping its shape on printers
-// whose dots are not square, turns each pixel's brightness into black or
-// white with a 4x4 ordered (Bayer) dither, and hands the driver one band
-// of dot rows at a time, so only one band is ever in memory.
+// A page is asked for a band at a time in RGB at the printer's
+// resolution (a PrintBandFnT); each band is turned into the printer's
+// inks and dithered, plane by plane, and handed to the driver
+// (printEscp.c, printPcl.c), so only one band is ever in memory.
//
-// Drivers:
-// Epson 9-pin ESC/P, 120 x 72 dpi (ESC * 1: double density), bands of
-// 8 rows (one pass of the print head), paper advanced
-// with ESC J n (n/216 inch).
-// Epson 24-pin ESC/P, 180 x 180 dpi (ESC * 39), bands of 24 rows (3
-// bytes per column), paper advanced with ESC J n (n/180
-// inch).
-// LaserJet PCL, 150 dpi raster graphics (ESC * t150R), one
-// ESC * b n W transfer per row (uncompressed, which every
-// LaserJet understands), trailing white bytes left out.
-// Blank bands on the dot-matrix printers are skipped with a paper feed
-// only; columns after the last black dot of a band are not sent.
+// Inks Black only: a dot where the brightness (luma) is below the
+// dither threshold. Cyan, magenta and yellow: each plane from
+// its own primary (red, green and blue). With black as well,
+// black takes what the three have in common (full undercolour
+// removal) and each colour what is left.
+// Dither An ordered (Bayer) threshold, 4x4 on the dot-matrix
+// printers and 8x8 at 300 dpi, shifted for each plane so dots
+// of different inks do not land on each other in a regular
+// pattern. The threshold follows the row's place on the page,
+// so the pattern runs on from band to band.
+//
+// A document is the driver's start, its pages, and its end, on a job it
+// owns. A page is the whole printable area drawn by the program
+// (printDocPage), or a picture (printDocPicture) scaled (nearest pixel)
+// to the largest size that fits, keeping its shape on printers whose
+// dots are not square.
#include "printInt.h"
-#include
#include
#include
-#define ESC "\x1B"
-#define ESC_CHAR 0x1B
-#define CR_CHAR '\r'
-#define FORM_FEED "\f"
#define BITS_PER_BYTE 8
#define TOP_BIT 0x80
-#define PRINT_WIDTH_IN 8 // printable width, inches
-#define PRINT_HEIGHT_IN 10 // printable height, inches
-
-// Epson ESC/P
-#define EPSON_RESET ESC "@"
-#define EPSON_GRAPHICS '*'
-#define EPSON_FEED 'J'
-#define EPSON9_DPI_X 120
-#define EPSON9_DPI_Y 72
-#define EPSON9_MODE 1 // ESC * 1: double density
-#define EPSON9_PINS 8
-#define EPSON9_FEED_UNITS 216 // ESC J n feeds n/216 inch
-#define EPSON24_DPI 180
-#define EPSON24_MODE 39 // ESC * 39: 180 dpi, 24 pins
-#define EPSON24_PINS 24
-#define EPSON24_FEED_UNITS 180 // ESC J n feeds n/180 inch
-#define EPSON_FEED_MAX 255 // largest n of ESC J n
-#define EPSON_COUNT_SHIFT 8 // nL nH
-#define EPSON_COUNT_MASK 0xFF
-#define EPSON_HEADER_LEN 5 // ESC * m nL nH
-
-// HP PCL
-#define PCL_RESET ESC "E"
-#define PCL_DPI 150
-#define PCL_BAND_ROWS 8
-#define PCL_BEGIN_FMT ESC "*t%dR" ESC "*p0x0Y" ESC "*r1A"
-#define PCL_END_RASTER ESC "*rB"
-#define PCL_ROW_FMT ESC "*b%dW"
-#define PCL_CMD_MAX 16
-#define PCL_BEGIN_MAX 48
+#define RGB_BYTES 3
+#define WHITE 0xFF
+#define BRIGHTNESS_LEVELS 256
// Luma weights (ITU-R BT.601, per mille)
#define LUMA_R 299
#define LUMA_G 587
#define LUMA_B 114
#define LUMA_SCALE 1000
-#define RGB_BYTES 3
-// 4x4 ordered dither: a pixel is black when its brightness is below the
-// threshold for its position (brightness 0..255).
-#define DITHER_SIZE 4
-#define DITHER_MASK (DITHER_SIZE - 1)
-#define DITHER_LEVELS (DITHER_SIZE * DITHER_SIZE)
-#define BRIGHTNESS_LEVELS 256
-#define DITHER_STEP (BRIGHTNESS_LEVELS / DITHER_LEVELS)
-#define DITHER_HALF_STEP (DITHER_STEP / 2)
+// TIFF PackBits (printPackBits): a run of n equal bytes (2..PACK_RUN_MAX)
+// is (PACK_RUN_BASE - n) and the byte; a stretch of 1..PACK_RUN_MAX others
+// is (n - 1) and the bytes. Runs start at PACK_RUN_MIN bytes (a shorter
+// one would cost as much as it saves and split the stretch around it), so
+// n bytes pack into at most PRINT_PACK_MAX(n).
+#define PACK_RUN_MIN 3
+#define PACK_RUN_BASE 257
-static const uint8_t sBayer[DITHER_SIZE][DITHER_SIZE] = {
+// How a band's colours become a driver's inks
+typedef enum {
+ InkModeLumaE, // black only
+ InkModeCmyE, // cyan, magenta, yellow
+ InkModeKcmyE // black and the three
+} InkModeE;
+
+// A picture printDocPicture scales onto the page
+typedef struct {
+ const uint8_t *rgb;
+ int32_t w;
+ int32_t h;
+ int32_t pitch;
+ int32_t outW;
+ int32_t outH;
+} PictureT;
+
+const PrintInkE printInksBlack[1] = { PrintInkBlackE };
+const PrintInkE printInksKcmy[PRINT_PLANES_MAX] = { PrintInkBlackE, PrintInkCyanE, PrintInkMagentaE, PrintInkYellowE };
+
+static const uint8_t sBayer4[PRINT_DITHER_SMALL][PRINT_DITHER_SMALL] = {
{ 0, 8, 2, 10 },
{ 12, 4, 14, 6 },
{ 3, 11, 1, 9 },
{ 15, 7, 13, 5 }
};
+static const uint8_t sBayer8[PRINT_DITHER_LARGE][PRINT_DITHER_LARGE] = {
+ { 0, 32, 8, 40, 2, 34, 10, 42 },
+ { 48, 16, 56, 24, 50, 18, 58, 26 },
+ { 12, 44, 4, 36, 14, 46, 6, 38 },
+ { 60, 28, 52, 20, 62, 30, 54, 22 },
+ { 3, 35, 11, 43, 1, 33, 9, 41 },
+ { 51, 19, 59, 27, 49, 17, 57, 25 },
+ { 15, 47, 7, 39, 13, 45, 5, 37 },
+ { 63, 31, 55, 23, 61, 29, 53, 21 }
+};
+
+// Each plane's shift of the matrix (across, down), in quarters of it, so
+// the inks' dots fall in different places; the first plane is unshifted
+static const uint8_t sPlaneShift[PRINT_PLANES_MAX][2] = {
+ { 0, 0 },
+ { 2, 1 },
+ { 1, 3 },
+ { 3, 2 }
+};
+
// ============================================================
// Prototypes
// ============================================================
-static bool bitSet(const uint8_t *bits, int32_t rowBytes, int32_t row, int32_t x);
-static bool epson24Band(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots);
-static bool epson9Band(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots);
-static bool epsonBand(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots, int32_t mode, int32_t pins, int32_t feedPerRow);
-static bool epsonBegin(PrintJobT *job, int32_t widthDots, int32_t heightDots);
-static bool epsonEnd(PrintJobT *job);
-static bool epsonFeed(PrintJobT *job, int32_t units);
-static int32_t lastInkColumn(const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots);
-static bool pclBand(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots);
-static bool pclBegin(PrintJobT *job, int32_t widthDots, int32_t heightDots);
-static bool pclEnd(PrintJobT *job);
+static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl);
+static InkModeE inkModeOf(const PrintDriverT *drv);
+static int32_t inkValue(InkModeE mode, PrintInkE ink, const uint8_t *px);
+static bool pictureBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows);
+bool printDocCancelled(const PrintDocT *doc);
+bool printDocClose(PrintDocT *doc);
+void printDocInfo(const PrintDocT *doc, PrintPageInfoT *info);
+PrintDocT *printDocOpen(PrintJobT *job, PrintModelE model);
+bool printDocPage(PrintDocT *doc, PrintBandFnT fn, void *ctx);
+bool printDocPicture(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t pitch);
const PrintDriverT *printDriver(PrintModelE model);
-bool printRasterJob(PrintJobT *job, const PrintDriverT *drv, const uint8_t *rgb, int32_t w, int32_t h, int32_t pitch);
-
-static const PrintDriverT sEpson9 = {
- "Epson 9-pin (ESC/P)", EPSON9_DPI_X, EPSON9_DPI_Y, EPSON9_DPI_X * PRINT_WIDTH_IN, EPSON9_DPI_Y * PRINT_HEIGHT_IN, EPSON9_PINS,
- EPSON_RESET, "", PRINT_LINES_STANDARD, epsonBegin, epson9Band, epsonEnd
-};
-
-static const PrintDriverT sEpson24 = {
- "Epson 24-pin (ESC/P)", EPSON24_DPI, EPSON24_DPI, EPSON24_DPI * PRINT_WIDTH_IN, EPSON24_DPI * PRINT_HEIGHT_IN, EPSON24_PINS,
- EPSON_RESET, "", PRINT_LINES_STANDARD, epsonBegin, epson24Band, epsonEnd
-};
-
-static const PrintDriverT sLaserJet = {
- "HP LaserJet (PCL)", PCL_DPI, PCL_DPI, PCL_DPI * PRINT_WIDTH_IN, PCL_DPI * PRINT_HEIGHT_IN, PCL_BAND_ROWS,
- PCL_RESET, PCL_RESET, PRINT_LINES_LASER, pclBegin, pclBand, pclEnd
-};
+int32_t printLuma(const uint8_t *px);
+bool printModelPage(PrintModelE model, PrintPageInfoT *info);
+int32_t printPackBits(const uint8_t *in, int32_t n, uint8_t *out);
+bool printPageRun(PrintJobT *job, const PrintDriverT *drv, int32_t widthDots, int32_t heightDots, PrintBandFnT fn, void *ctx);
+bool printPlaneDot(const PrintPlanesT *pl, int32_t plane, int32_t row, int32_t x);
+static int32_t threshold(int32_t size, int32_t plane, int32_t x, int32_t y);
-static bool bitSet(const uint8_t *bits, int32_t rowBytes, int32_t row, int32_t x) {
- return (bits[row * rowBytes + x / BITS_PER_BYTE] & (TOP_BIT >> (x % BITS_PER_BYTE))) != 0;
-}
+// The band's colours as the driver's inks, dithered (the planes come
+// clear). Each row's thresholds are worked out once: the pattern repeats
+// every ditherSize dots across.
+static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl) {
+ int32_t mask = drv->ditherSize - 1;
+ for (int32_t r = 0; r < pl->rows; r++) {
+ uint8_t th[PRINT_PLANES_MAX][PRINT_DITHER_LARGE];
+ const uint8_t *src = rgb + (int64_t)r * pl->widthDots * RGB_BYTES;
-static bool epson24Band(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots) {
- return epsonBand(job, bits, rowBytes, rows, widthDots, EPSON24_MODE, EPSON24_PINS, EPSON24_FEED_UNITS / EPSON24_DPI);
-}
-
-
-static bool epson9Band(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots) {
- return epsonBand(job, bits, rowBytes, rows, widthDots, EPSON9_MODE, EPSON9_PINS, EPSON9_FEED_UNITS / EPSON9_DPI_Y);
-}
-
-
-// One pass of the print head: ESC * mode nL nH, then for every column
-// pins/8 bytes (top dot in the high bit), CR, and a paper feed of the
-// band's height. A band with no ink only feeds.
-static bool epsonBand(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots, int32_t mode, int32_t pins, int32_t feedPerRow) {
- int32_t cols = lastInkColumn(bits, rowBytes, rows, widthDots) + 1;
- int32_t bytesPerCol = pins / BITS_PER_BYTE;
-
- if (cols > 0) {
- uint8_t *line = (uint8_t *)malloc((size_t)(EPSON_HEADER_LEN + cols * bytesPerCol + 1));
-
- if (!line) {
- return false;
+ for (int32_t p = 0; p < pl->count; p++) {
+ for (int32_t i = 0; i < drv->ditherSize; i++) {
+ th[p][i] = (uint8_t)threshold(drv->ditherSize, p, i, y0 + r);
+ }
}
- int32_t n = 0;
+ for (int32_t x = 0; x < pl->widthDots; x++) {
+ const uint8_t *px = src + x * RGB_BYTES;
- line[n++] = ESC_CHAR;
- line[n++] = EPSON_GRAPHICS;
- line[n++] = (uint8_t)mode;
- line[n++] = (uint8_t)(cols & EPSON_COUNT_MASK);
- line[n++] = (uint8_t)(cols >> EPSON_COUNT_SHIFT);
-
- for (int32_t x = 0; x < cols; x++) {
- for (int32_t b = 0; b < bytesPerCol; b++) {
- uint8_t v = 0;
-
- for (int32_t bit = 0; bit < BITS_PER_BYTE; bit++) {
- int32_t row = b * BITS_PER_BYTE + bit;
-
- if (row < rows && bitSet(bits, rowBytes, row, x)) {
- v |= (uint8_t)(TOP_BIT >> bit);
- }
+ for (int32_t p = 0; p < pl->count; p++) {
+ if (inkValue(mode, pl->ink[p], px) < th[p][x & mask]) {
+ pl->bits[p][r * pl->rowBytes + x / BITS_PER_BYTE] |= (uint8_t)(TOP_BIT >> (x % BITS_PER_BYTE));
}
-
- line[n++] = v;
- }
- }
-
- line[n++] = CR_CHAR;
-
- bool ok = printJobWrite(job, line, n);
-
- free(line);
-
- if (!ok) {
- return false;
- }
- }
-
- return epsonFeed(job, rows * feedPerRow);
-}
-
-
-static bool epsonBegin(PrintJobT *job, int32_t widthDots, int32_t heightDots) {
- (void)widthDots;
- (void)heightDots;
- return printJobPuts(job, EPSON_RESET);
-}
-
-
-static bool epsonEnd(PrintJobT *job) {
- return printJobPuts(job, FORM_FEED EPSON_RESET);
-}
-
-
-// ESC J n, split when more than one command's worth.
-static bool epsonFeed(PrintJobT *job, int32_t units) {
- while (units > 0) {
- int32_t n = units > EPSON_FEED_MAX ? EPSON_FEED_MAX : units;
- uint8_t cmd[3] = { ESC_CHAR, EPSON_FEED, (uint8_t)n };
-
- if (!printJobWrite(job, cmd, (int32_t)sizeof(cmd))) {
- return false;
- }
-
- units -= n;
- }
-
- return true;
-}
-
-
-// Index of the last column with a black dot in the band, -1 when none.
-static int32_t lastInkColumn(const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots) {
- for (int32_t x = widthDots - 1; x >= 0; x--) {
- for (int32_t r = 0; r < rows; r++) {
- if (bitSet(bits, rowBytes, r, x)) {
- return x;
}
}
}
-
- return -1;
}
-// Every row: ESC * b n W and its bytes up to the last one with ink.
-static bool pclBand(PrintJobT *job, const uint8_t *bits, int32_t rowBytes, int32_t rows, int32_t widthDots) {
- char cmd[PCL_CMD_MAX];
+static InkModeE inkModeOf(const PrintDriverT *drv) {
+ bool black = false;
+ bool colour = false;
- (void)widthDots;
-
- for (int32_t r = 0; r < rows; r++) {
- const uint8_t *row = bits + r * rowBytes;
- int32_t n = rowBytes;
-
- while (n > 0 && row[n - 1] == 0) {
- n--;
- }
-
- snprintf(cmd, sizeof(cmd), PCL_ROW_FMT, (int)n);
-
- if (!printJobPuts(job, cmd) || !printJobWrite(job, row, n)) {
- return false;
- }
+ for (int32_t i = 0; i < drv->inkCount; i++) {
+ black = black || drv->inks[i] == PrintInkBlackE;
+ colour = colour || drv->inks[i] != PrintInkBlackE;
}
- return true;
+ if (!colour) {
+ return InkModeLumaE;
+ }
+
+ return black ? InkModeKcmyE : InkModeCmyE;
}
-static bool pclBegin(PrintJobT *job, int32_t widthDots, int32_t heightDots) {
- char cmd[PCL_BEGIN_MAX];
+// How light a pixel is in ink's terms (WHITE: none of it; a dot is made
+// where this is below the threshold).
+static int32_t inkValue(InkModeE mode, PrintInkE ink, const uint8_t *px) {
+ int32_t hi;
- (void)widthDots;
- (void)heightDots;
- snprintf(cmd, sizeof(cmd), PCL_BEGIN_FMT, PCL_DPI);
- return printJobPuts(job, PCL_RESET) && printJobPuts(job, cmd);
-}
+ if (mode == InkModeLumaE) {
+ return printLuma(px);
+ }
+ hi = px[0];
+ hi = px[1] > hi ? px[1] : hi;
+ hi = px[2] > hi ? px[2] : hi;
-static bool pclEnd(PrintJobT *job) {
- return printJobPuts(job, PCL_END_RASTER FORM_FEED PCL_RESET);
-}
+ switch (ink) {
+ case PrintInkBlackE:
+ return hi;
+ case PrintInkCyanE:
+ return mode == InkModeKcmyE ? WHITE - hi + px[0] : px[0];
-const PrintDriverT *printDriver(PrintModelE model) {
- switch (model) {
- case PrintModelEpson9E:
- return &sEpson9;
-
- case PrintModelEpson24E:
- return &sEpson24;
-
- case PrintModelLaserJetE:
- return &sLaserJet;
+ case PrintInkMagentaE:
+ return mode == InkModeKcmyE ? WHITE - hi + px[1] : px[1];
default:
- return NULL;
+ return mode == InkModeKcmyE ? WHITE - hi + px[2] : px[2];
}
}
-bool printRasterJob(PrintJobT *job, const PrintDriverT *drv, const uint8_t *rgb, int32_t w, int32_t h, int32_t pitch) {
- if (!job || !drv || !rgb || w <= 0 || h <= 0) {
+// A band of a picture scaled to outW x outH (nearest pixel).
+static bool pictureBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows) {
+ const PictureT *pic = (const PictureT *)ctx;
+
+ for (int32_t r = 0; r < rows; r++) {
+ const uint8_t *src = pic->rgb + (int64_t)((int64_t)(y0 + r) * pic->h / pic->outH) * pic->pitch;
+ uint8_t *dst = rgb + (int64_t)r * width * RGB_BYTES;
+
+ for (int32_t x = 0; x < width; x++) {
+ memcpy(dst + x * RGB_BYTES, src + (int64_t)x * pic->w / pic->outW * RGB_BYTES, RGB_BYTES);
+ }
+ }
+
+ return true;
+}
+
+
+bool printDocCancelled(const PrintDocT *doc) {
+ return printJobCancelled(doc->job);
+}
+
+
+// A vector page left open is ended first.
+bool printDocClose(PrintDocT *doc) {
+ if (!doc) {
+ return false;
+ }
+
+ printVecEnd(doc);
+
+ bool ok = doc->ok && doc->drv->docEnd(doc->drv, doc->job) && !printJobCancelled(doc->job);
+
+ ok = printJobClose(doc->job) && ok;
+ free(doc);
+ return ok;
+}
+
+
+void printDocInfo(const PrintDocT *doc, PrintPageInfoT *info) {
+ printModelPage(doc->model, info);
+}
+
+
+PrintDocT *printDocOpen(PrintJobT *job, PrintModelE model) {
+ const PrintDriverT *drv = printDriver(model);
+ PrintDocT *doc = drv && job ? (PrintDocT *)calloc(1, sizeof(PrintDocT)) : NULL;
+
+ if (!doc) {
+ printJobClose(job);
+ return NULL;
+ }
+
+ doc->job = job;
+ doc->drv = drv;
+ doc->model = model;
+ doc->ok = drv->docBegin(drv, job);
+ return doc;
+}
+
+
+bool printDocPage(PrintDocT *doc, PrintBandFnT fn, void *ctx) {
+ if (doc->vecPage) {
+ return false;
+ }
+
+ doc->ok = doc->ok && fn && printPageRun(doc->job, doc->drv, doc->drv->pageWidthDots, doc->drv->pageHeightDots, fn, ctx);
+ return doc->ok;
+}
+
+
+// Fit the width; if that is too tall, fit the height. Sizes in dots
+// keep the picture's shape: dots per inch may differ across and down.
+bool printDocPicture(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t pitch) {
+ const PrintDriverT *drv = doc->drv;
+ PictureT pic;
+
+ if (doc->vecPage) {
+ return false;
+ }
+
+ if (!rgb || w <= 0 || h <= 0) {
+ doc->ok = false;
return false;
}
- // Fit the width; if that is too tall, fit the height. Sizes in dots
- // keep the picture's shape: dots per inch may differ across and down.
int64_t outW = drv->pageWidthDots;
int64_t outH = outW * h * drv->dpiY / ((int64_t)w * drv->dpiX);
@@ -325,52 +309,184 @@ bool printRasterJob(PrintJobT *job, const PrintDriverT *drv, const uint8_t *rgb,
outW = outH * w * drv->dpiX / ((int64_t)h * drv->dpiY);
}
- if (outW < 1) {
- outW = 1;
+ pic.rgb = rgb;
+ pic.w = w;
+ pic.h = h;
+ pic.pitch = pitch;
+ pic.outW = outW < 1 ? 1 : (int32_t)outW;
+ pic.outH = outH < 1 ? 1 : (int32_t)outH;
+ doc->ok = doc->ok && printPageRun(doc->job, drv, pic.outW, pic.outH, pictureBand, &pic);
+ return doc->ok;
+}
+
+
+const PrintDriverT *printDriver(PrintModelE model) {
+ switch (model) {
+ case PrintModelEpson9E:
+ return &printDrvEpson9;
+
+ case PrintModelEpson24E:
+ return &printDrvEpson24;
+
+ case PrintModelLaserJetE:
+ return &printDrvLaserJet;
+
+ case PrintModelLaserJet300E:
+ return &printDrvLaserJet300;
+
+ case PrintModelEpson9ColorE:
+ return &printDrvEpson9Color;
+
+ case PrintModelEpson24ColorE:
+ return &printDrvEpson24Color;
+
+ case PrintModelDeskJet500CE:
+ return &printDrvDeskJet500C;
+
+ case PrintModelDeskJetColorE:
+ return &printDrvDeskJetColor;
+
+ case PrintModelPostScriptE:
+ return &printDrvPostScript;
+
+ case PrintModelPostScriptColorE:
+ return &printDrvPostScriptColor;
+
+ case PrintModelImageWriterE:
+ return &printDrvImageWriter;
+
+ case PrintModelImageWriterColorE:
+ return &printDrvImageWriterColor;
+
+ default:
+ return NULL;
}
+}
- if (outH < 1) {
- outH = 1;
- }
- int32_t rowBytes = (int32_t)((outW + BITS_PER_BYTE - 1) / BITS_PER_BYTE);
- size_t bandBytes = (size_t)rowBytes * (size_t)drv->bandRows;
- uint8_t *band = (uint8_t *)malloc(bandBytes);
+int32_t printLuma(const uint8_t *px) {
+ return (px[0] * LUMA_R + px[1] * LUMA_G + px[2] * LUMA_B) / LUMA_SCALE;
+}
- if (!band) {
+
+// The page model prints on; false for text only.
+bool printModelPage(PrintModelE model, PrintPageInfoT *info) {
+ const PrintDriverT *drv = printDriver(model);
+
+ if (!drv) {
return false;
}
- bool ok = drv->begin(job, (int32_t)outW, (int32_t)outH);
+ if (info) {
+ info->dpiX = drv->dpiX;
+ info->dpiY = drv->dpiY;
+ info->widthDots = drv->pageWidthDots;
+ info->heightDots = drv->pageHeightDots;
+ info->color = drv->inkCount > 1;
+ info->vector = drv->vector;
+ }
- for (int32_t y0 = 0; ok && y0 < outH; y0 += drv->bandRows) {
- int32_t rows = (int32_t)(outH - y0) < drv->bandRows ? (int32_t)(outH - y0) : drv->bandRows;
+ return true;
+}
- memset(band, 0, bandBytes);
- for (int32_t r = 0; r < rows; r++) {
- int32_t y = y0 + r;
- const uint8_t *src = rgb + (int64_t)(y * h / outH) * pitch;
- uint8_t *dst = band + r * rowBytes;
+int32_t printPackBits(const uint8_t *in, int32_t n, uint8_t *out) {
+ int32_t i = 0;
+ int32_t o = 0;
- for (int32_t x = 0; x < outW; x++) {
- const uint8_t *px = src + (int64_t)(x * w / outW) * RGB_BYTES;
- int32_t luma = (px[0] * LUMA_R + px[1] * LUMA_G + px[2] * LUMA_B) / LUMA_SCALE;
- int32_t threshold = sBayer[y & DITHER_MASK][x & DITHER_MASK] * DITHER_STEP + DITHER_HALF_STEP;
+ while (i < n) {
+ int32_t run = 1;
- if (luma < threshold) {
- dst[x / BITS_PER_BYTE] |= (uint8_t)(TOP_BIT >> (x % BITS_PER_BYTE));
- }
- }
+ while (i + run < n && run < PACK_RUN_MAX && in[i + run] == in[i]) {
+ run++;
}
- ok = drv->band(job, band, rowBytes, rows, (int32_t)outW);
+ if (run >= PACK_RUN_MIN) {
+ out[o++] = (uint8_t)(PACK_RUN_BASE - run);
+ out[o++] = in[i];
+ i += run;
+ continue;
+ }
+
+ int32_t start = i;
+
+ // The stretch ends where a run starts
+ do {
+ i++;
+ } while (i < n && i - start < PACK_RUN_MAX && !(i + 2 < n && in[i] == in[i + 1] && in[i] == in[i + 2]));
+
+ out[o++] = (uint8_t)(i - start - 1);
+ memcpy(out + o, in + start, (size_t)(i - start));
+ o += i - start;
}
- if (ok) {
- ok = drv->end(job);
+ return o;
+}
+
+
+bool printPageRun(PrintJobT *job, const PrintDriverT *drv, int32_t widthDots, int32_t heightDots, PrintBandFnT fn, void *ctx) {
+ PrintPlanesT pl;
+ InkModeE mode = inkModeOf(drv);
+ size_t rgbBytes = (size_t)widthDots * (size_t)drv->bandRows * RGB_BYTES;
+ uint8_t *rgb = (uint8_t *)malloc(rgbBytes);
+ size_t planeBytes;
+ bool ok = rgb != NULL;
+
+ memset(&pl, 0, sizeof(pl));
+ pl.count = drv->inkCount;
+ pl.rowBytes = (widthDots + BITS_PER_BYTE - 1) / BITS_PER_BYTE;
+ pl.widthDots = widthDots;
+ planeBytes = (size_t)pl.rowBytes * (size_t)drv->bandRows;
+
+ for (int32_t p = 0; p < pl.count; p++) {
+ pl.ink[p] = drv->inks[p];
+ pl.bits[p] = (uint8_t *)malloc(planeBytes);
+ ok = ok && pl.bits[p] != NULL;
}
- free(band);
+ ok = ok && drv->pageBegin(drv, job, widthDots, heightDots);
+
+ for (int32_t y0 = 0; ok && y0 < heightDots; y0 += drv->bandRows) {
+ pl.rows = heightDots - y0 < drv->bandRows ? heightDots - y0 : drv->bandRows;
+ pl.y0 = y0;
+ memset(rgb, WHITE, (size_t)widthDots * (size_t)pl.rows * RGB_BYTES);
+ ok = fn(ctx, rgb, widthDots, y0, pl.rows);
+
+ for (int32_t p = 0; ok && p < pl.count; p++) {
+ memset(pl.bits[p], 0, planeBytes);
+ }
+
+ if (ok) {
+ bandToPlanes(drv, mode, rgb, y0, &pl);
+ ok = drv->band(drv, job, &pl);
+ }
+ }
+
+ ok = ok && drv->pageEnd(drv, job);
+
+ for (int32_t p = 0; p < pl.count; p++) {
+ free(pl.bits[p]);
+ }
+
+ free(rgb);
return ok && !printJobCancelled(job);
}
+
+
+bool printPlaneDot(const PrintPlanesT *pl, int32_t plane, int32_t row, int32_t x) {
+ return (pl->bits[plane][row * pl->rowBytes + x / BITS_PER_BYTE] & (TOP_BIT >> (x % BITS_PER_BYTE))) != 0;
+}
+
+
+// The dither threshold (0..255) for plane at (x, y): its place in the
+// size x size Bayer matrix, shifted for the plane.
+static int32_t threshold(int32_t size, int32_t plane, int32_t x, int32_t y) {
+ int32_t mask = size - 1;
+ int32_t scale = size / PRINT_DITHER_SMALL;
+ int32_t sx = (x + sPlaneShift[plane][0] * scale) & mask;
+ int32_t sy = (y + sPlaneShift[plane][1] * scale) & mask;
+ int32_t step = BRIGHTNESS_LEVELS / (size * size);
+ int32_t level = size == PRINT_DITHER_LARGE ? sBayer8[sy][sx] : sBayer4[sy][sx];
+
+ return level * step + step / 2;
+}
diff --git a/src/libs/kpunch/print/printSys.c b/src/libs/kpunch/print/printSys.c
index 91822c28..9cb51f46 100644
--- a/src/libs/kpunch/print/printSys.c
+++ b/src/libs/kpunch/print/printSys.c
@@ -22,23 +22,35 @@
// printSys.c -- Printing library: the real printer ports
//
-// DOS: the BIOS printer service (INT 17h) through DPMI function 0300h
-// (simulate a real-mode interrupt) issued directly with INT 31h, which a
-// module can use without extra loader exports. A port exists when the
-// BIOS data area holds an I/O address for it (0040:0008, one word per
-// port).
+// DOS: the parallel ports through the BIOS printer service (INT 17h),
+// by DPMI function 0300h (simulate a real-mode interrupt) issued
+// directly with INT 31h, which a module can use without extra loader
+// exports; the serial ports through the serial library (rs232), whose
+// interrupt handler sends from its buffer as the printer's handshake
+// allows. A port exists when the BIOS data area holds an I/O address
+// for it (0040:0008 parallel, 0040:0000 serial, a word each). A serial
+// port with more than COM_BUSY_BYTES waiting is busy; it is closed once
+// they and the UART's FIFO have gone.
//
// Elsewhere: every port exists and is always ready; bytes are appended
-// to lpt1.prn .. lpt3.prn in the working directory (kept open, flushed
-// after every byte so a reader sees them at once).
+// to lpt1.prn .. lpt3.prn and com1.prn .. com4.prn in the working
+// directory (kept open, flushed after every byte so a reader sees them
+// at once).
#include "printInt.h"
+#include
#include
#include
+#define PORT_READY (PRINT_STATUS_NOT_BUSY | PRINT_STATUS_SELECTED)
+
#ifdef __DJGPP__
+#include "../serial/rs232/rs232.h"
+#include "dvxPlat.h"
+#include "taskSwch.h"
+
#include
#include
#include
@@ -48,14 +60,28 @@
#define PRN_FN_WRITE 0x00
#define PRN_FN_STATUS 0x02
#define BIOS_LPT_BASE_ADDR 0x0408
+#define BIOS_COM_BASE_ADDR 0x0400
#define BIOS_PORT_ENTRY_SHIFT 1
#define AH_SHIFT 8
+#define COM_DATA_BITS 8
+#define COM_PARITY 'N'
+#define COM_STOP_BITS 1
+#define COM_BUSY_BYTES 1024 // waiting in the send buffer: the port is busy
+#define COM_FIFO_BYTES 16 // a 16550A's transmit FIFO
+#define COM_BITS_PER_CHAR 10 // start, 8 data, stop
+#define MS_PER_SECOND 1000
+
+// The serial library's handshake for each PrintHandshakeE
+static const int32_t sRs232Handshake[PrintHandshakeCountE] = { RS232_HANDSHAKE_NONE, RS232_HANDSHAKE_XONXOFF, RS232_HANDSHAKE_RTSCTS, RS232_HANDSHAKE_DTRDSR };
+
+// Each serial port's speed while a job has it open
+static int32_t sComBaud[PRINT_COM_COUNT];
#else
-#define HOST_PORT_FILE "lpt%d.prn"
+#define HOST_LPT_FILE "lpt%" PRId32 ".prn"
+#define HOST_COM_FILE "com%" PRId32 ".prn"
#define HOST_PORT_NAME_MAX 16
-#define HOST_PORT_READY (PRINT_STATUS_NOT_BUSY | PRINT_STATUS_SELECTED)
static FILE *sHostPort[PRINT_PORT_COUNT];
@@ -68,11 +94,14 @@ static FILE *sHostPort[PRINT_PORT_COUNT];
#ifdef __DJGPP__
static uint8_t biosPrinter(uint8_t fn, int32_t port, uint8_t byte);
#endif
+static void sysClose(void *ctx, int32_t port);
+static bool sysOpen(void *ctx, int32_t port, int32_t baud, PrintHandshakeE handshake);
+static int32_t sysPending(void *ctx, int32_t port);
static bool sysPresent(void *ctx, int32_t port);
static uint8_t sysStatus(void *ctx, int32_t port);
static uint8_t sysWrite(void *ctx, int32_t port, uint8_t byte);
-const PrintPortOpsT printSysPortOps = { sysPresent, sysStatus, sysWrite, NULL };
+const PrintPortOpsT printSysPortOps = { sysPresent, sysStatus, sysWrite, NULL, sysOpen, sysPending, sysClose };
#ifdef __DJGPP__
@@ -101,10 +130,69 @@ static uint8_t biosPrinter(uint8_t fn, int32_t port, uint8_t byte) {
#endif
+// A serial port, once what it holds has gone and its FIFO had time to
+// empty.
+static void sysClose(void *ctx, int32_t port) {
+ (void)ctx;
+#ifdef __DJGPP__
+ if (port >= PRINT_PORT_COM1) {
+ int32_t com = port - PRINT_PORT_COM1;
+ PlatformTicksT since = platformClock();
+
+ while (platformClock() - since < PLATFORM_MS_TO_TICKS(COM_FIFO_BYTES * COM_BITS_PER_CHAR * MS_PER_SECOND / sComBaud[com] + 1)) {
+ tsYield();
+ }
+
+ rs232Close(com);
+ }
+#else
+ (void)port;
+#endif
+}
+
+
+// A serial port at baud, 8 data bits, no parity, 1 stop bit, with the
+// handshake; the parallel ports need nothing.
+static bool sysOpen(void *ctx, int32_t port, int32_t baud, PrintHandshakeE handshake) {
+ (void)ctx;
+#ifdef __DJGPP__
+ if (port < PRINT_PORT_COM1) {
+ return true;
+ }
+
+ int32_t com = port - PRINT_PORT_COM1;
+
+ sComBaud[com] = baud;
+ return rs232Open(com, baud, COM_DATA_BITS, COM_PARITY, COM_STOP_BITS, sRs232Handshake[handshake]) == RS232_SUCCESS;
+#else
+ (void)port;
+ (void)baud;
+ (void)handshake;
+ return true;
+#endif
+}
+
+
+// Bytes a serial port holds that the printer has not taken yet.
+static int32_t sysPending(void *ctx, int32_t port) {
+ (void)ctx;
+#ifdef __DJGPP__
+ int32_t n = port >= PRINT_PORT_COM1 ? rs232GetTxBuffered(port - PRINT_PORT_COM1) : 0;
+
+ return n > 0 ? n : 0;
+#else
+ (void)port;
+ return 0;
+#endif
+}
+
+
static bool sysPresent(void *ctx, int32_t port) {
(void)ctx;
#ifdef __DJGPP__
- return _farpeekw(_dos_ds, BIOS_LPT_BASE_ADDR + ((uint32_t)port << BIOS_PORT_ENTRY_SHIFT)) != 0;
+ uint32_t addr = port < PRINT_PORT_COM1 ? BIOS_LPT_BASE_ADDR + ((uint32_t)port << BIOS_PORT_ENTRY_SHIFT) : BIOS_COM_BASE_ADDR + ((uint32_t)(port - PRINT_PORT_COM1) << BIOS_PORT_ENTRY_SHIFT);
+
+ return _farpeekw(_dos_ds, addr) != 0;
#else
(void)port;
return true;
@@ -112,13 +200,25 @@ static bool sysPresent(void *ctx, int32_t port) {
}
+// A parallel port's BIOS status; a serial port is busy while its buffer
+// is well filled (the printer holding it back), else ready.
static uint8_t sysStatus(void *ctx, int32_t port) {
(void)ctx;
#ifdef __DJGPP__
+ if (port >= PRINT_PORT_COM1) {
+ int32_t n = rs232GetTxBuffered(port - PRINT_PORT_COM1);
+
+ if (n < 0) {
+ return PRINT_STATUS_SELECTED | PRINT_STATUS_IO_ERROR;
+ }
+
+ return n < COM_BUSY_BYTES ? PORT_READY : PRINT_STATUS_SELECTED;
+ }
+
return biosPrinter(PRN_FN_STATUS, port, 0);
#else
(void)port;
- return HOST_PORT_READY;
+ return PORT_READY;
#endif
}
@@ -126,12 +226,19 @@ static uint8_t sysStatus(void *ctx, int32_t port) {
static uint8_t sysWrite(void *ctx, int32_t port, uint8_t byte) {
(void)ctx;
#ifdef __DJGPP__
+ if (port >= PRINT_PORT_COM1) {
+ char c = (char)byte;
+
+ return rs232WriteBuf(port - PRINT_PORT_COM1, &c, 1) == 1 ? PORT_READY : PRINT_STATUS_SELECTED | PRINT_STATUS_IO_ERROR;
+ }
+
return biosPrinter(PRN_FN_WRITE, port, byte);
#else
if (!sHostPort[port]) {
char name[HOST_PORT_NAME_MAX];
+ bool com = port >= PRINT_PORT_COM1;
- snprintf(name, sizeof(name), HOST_PORT_FILE, (int)(port + PRINT_PORT_NUMBER_BASE));
+ snprintf(name, sizeof(name), com ? HOST_COM_FILE : HOST_LPT_FILE, (com ? port - PRINT_PORT_COM1 : port) + PRINT_PORT_NUMBER_BASE);
sHostPort[port] = fopen(name, "ab");
if (!sHostPort[port]) {
@@ -141,6 +248,6 @@ static uint8_t sysWrite(void *ctx, int32_t port, uint8_t byte) {
fputc(byte, sHostPort[port]);
fflush(sHostPort[port]);
- return HOST_PORT_READY;
+ return PORT_READY;
#endif
}
diff --git a/src/libs/kpunch/print/printText.c b/src/libs/kpunch/print/printText.c
index fdac8584..3e0aaad3 100644
--- a/src/libs/kpunch/print/printText.c
+++ b/src/libs/kpunch/print/printText.c
@@ -29,7 +29,9 @@
// between may be columns - marginLeft - marginRight characters wide.
//
// The same layout pass runs twice: first without a job to count the
-// pages (for &n and the page range), then once per copy to print.
+// pages (for &n and the page range), then once per copy to print. A
+// driver with text ops of its own (PostScript) is handed the pages and
+// lines instead of the plain text.
#include "printInt.h"
@@ -57,6 +59,7 @@
// One layout pass
typedef struct {
PrintJobT *job; // NULL: count the pages only
+ const PrintTextOpsT *ops; // the driver's own text output; NULL: plain text
const PrintPageSetupT *ps;
const PrintTextInfoT *info;
int32_t pageLines;
@@ -234,7 +237,8 @@ static void outHeader(TextOutT *o, const char *fmt) {
}
-// Left margin, text, CR LF (a blank line gets no margin).
+// Left margin, text, CR LF (a blank line gets no margin); to the
+// driver's text ops without the line end.
static void outLine(TextOutT *o, const char *text, int32_t len) {
char buf[PRINT_COLUMNS_MAX + LINE_BUF];
int32_t n = len > 0 ? o->ps->marginLeft : 0;
@@ -242,6 +246,12 @@ static void outLine(TextOutT *o, const char *text, int32_t len) {
memset(buf, ASCII_SPACE, (size_t)n);
memcpy(buf + n, text, (size_t)len);
n += len;
+
+ if (o->ops) {
+ o->ok = o->ok && (!o->job || !o->emit || o->ops->line(o->job, buf, n));
+ return;
+ }
+
memcpy(buf + n, LINE_END, LINE_END_LEN);
outBytes(o, buf, n + LINE_END_LEN);
}
@@ -254,6 +264,10 @@ static void pageBegin(TextOutT *o) {
o->page++;
o->emit = (o->info->firstPage <= 0 || o->page >= o->info->firstPage) && (o->info->lastPage <= 0 || o->page <= o->info->lastPage);
+ if (o->ops && o->job && o->emit) {
+ o->ok = o->ok && o->ops->pageBegin(o->job);
+ }
+
for (int32_t row = 0; row < ps->marginTop; row++) {
if (row == hdrRow) {
outHeader(o, ps->header);
@@ -281,14 +295,22 @@ static void pageEnd(TextOutT *o) {
outHeader(o, ps->footer);
}
- outBytes(o, &ff, 1);
+ if (o->ops) {
+ o->ok = o->ok && (!o->job || !o->emit || o->ops->pageEnd(o->job));
+ } else {
+ outBytes(o, &ff, 1);
+ }
+
o->line = 0;
}
static void passInit(TextOutT *o, PrintJobT *job, PrintModelE model, const PrintPageSetupT *ps, const PrintTextInfoT *info) {
+ const PrintDriverT *drv = printDriver(model);
+
memset(o, 0, sizeof(*o));
o->job = job;
+ o->ops = drv ? drv->textOps : NULL;
o->ps = ps;
o->info = info ? info : &sNoInfo;
o->pageLines = printPageLines(ps, model);
@@ -403,7 +425,9 @@ bool printTextJob(PrintJobT *job, PrintModelE model, const char *text, int32_t l
bool ok = true;
TextOutT o;
- if (drv) {
+ if (drv && drv->textOps) {
+ ok = drv->textOps->begin(job, ps->columns, printPageLines(ps, model));
+ } else if (drv) {
ok = printJobPuts(job, drv->textBegin);
}
@@ -414,7 +438,9 @@ bool printTextJob(PrintJobT *job, PrintModelE model, const char *text, int32_t l
ok = o.ok;
}
- if (ok && drv) {
+ if (ok && drv && drv->textOps) {
+ ok = drv->textOps->end(job);
+ } else if (ok && drv) {
ok = printJobPuts(job, drv->textEnd);
}
diff --git a/src/libs/kpunch/print/printUi.c b/src/libs/kpunch/print/printUi.c
index 2ffb4d1d..cc2ac227 100644
--- a/src/libs/kpunch/print/printUi.c
+++ b/src/libs/kpunch/print/printUi.c
@@ -113,12 +113,16 @@ typedef struct {
static WidgetT *addRow(WidgetT *parent, const char *label);
static WidgetT *addSpinner(WidgetT *parent, const char *name, const char *label, int32_t minVal, int32_t maxVal, int32_t value);
static void browseFile(WidgetT *w);
+static void choiceOf(const PrintSettingsT *s, PrintChoiceT *ch);
static void destChanged(WidgetT *w);
+static bool docClose(AppContextT *ctx, PrintDocT *doc, const char *failed);
static PrintJobT *openJob(AppContextT *ctx, PrintDestE dest, const char *filePath, const char *what, bool keepWindow, char *err, int32_t errSize);
bool printDialog(AppContextT *ctx, const char *title, int32_t pageCount, PrintChoiceT *choice);
static bool printDlgOk(WindowT *dlg, void *userCtx);
bool printPageSetupDialog(AppContextT *ctx);
static bool printRgb(AppContextT *ctx, const uint8_t *rgb, int32_t w, int32_t h, const char *title, bool askFirst);
+bool printUiDocClose(AppContextT *ctx, PrintDocT *doc);
+PrintDocT *printUiDocOpen(AppContextT *ctx, const char *title, const PrintChoiceT *choice);
bool printUiImageFile(AppContextT *ctx, const char *path, const char *title, bool askFirst);
PrintJobT *printUiOpenPort(AppContextT *ctx, int32_t port);
bool printUiText(AppContextT *ctx, const char *text, const char *title, bool askFirst);
@@ -129,6 +133,7 @@ static void progressClose(WindowT *win);
static void progressHide(UiRunT *run);
static void progressShow(UiRunT *run);
static bool setupDlgOk(WindowT *dlg, void *userCtx);
+static void textOnlyBox(AppContextT *ctx);
static uint8_t *toRgb(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch);
static PrintAnswerE uiAsk(void *ctx, const char *message);
static void uiFinished(void *ctx, bool closing);
@@ -142,7 +147,7 @@ static WidgetT *addRow(WidgetT *parent, const char *label) {
WidgetT *lbl = wgtLabel(row, label);
row->spacing = wgtPixels(UI_GAP);
- lbl->prefW = wgtPixels(UI_LABEL_W);
+ lbl->minW = wgtPixels(UI_LABEL_W);
return row;
}
@@ -151,7 +156,7 @@ static WidgetT *addRow(WidgetT *parent, const char *label) {
static WidgetT *addSpinner(WidgetT *parent, const char *name, const char *label, int32_t minVal, int32_t maxVal, int32_t value) {
WidgetT *spin = wgtSpinner(addRow(parent, label), minVal, maxVal, 1);
- spin->prefW = wgtChars(UI_SPIN_CHARS);
+ spin->minW = wgtChars(UI_SPIN_CHARS);
wgtSetName(spin, name);
wgtSpinnerSetValue(spin, value);
return spin;
@@ -173,6 +178,16 @@ static void browseFile(WidgetT *w) {
// The file name only matters when printing to a file.
+// A print choice from the settings: their destination, one copy, all
+// pages.
+static void choiceOf(const PrintSettingsT *s, PrintChoiceT *ch) {
+ memset(ch, 0, sizeof(*ch));
+ ch->dest = s->dest;
+ ch->copies = 1;
+ snprintf(ch->filePath, sizeof(ch->filePath), "%s", s->filePath);
+}
+
+
static void destChanged(WidgetT *w) {
PrintDlgT *d = (PrintDlgT *)w->userData;
bool toFile = wgtDropdownGetSelected(d->dest) == PrintDestFileE;
@@ -182,6 +197,23 @@ static void destChanged(WidgetT *w) {
}
+// Close a document; failed (unless cancelled) is told to the user.
+static bool docClose(AppContextT *ctx, PrintDocT *doc, const char *failed) {
+ if (!doc) {
+ return false;
+ }
+
+ bool cancelled = printDocCancelled(doc);
+ bool ok = printDocClose(doc);
+
+ if (!ok && !cancelled) {
+ dvxErrorBox(ctx, UI_TITLE, failed);
+ }
+
+ return ok;
+}
+
+
static PrintJobT *openJob(AppContextT *ctx, PrintDestE dest, const char *filePath, const char *what, bool keepWindow, char *err, int32_t errSize) {
UiRunT *run = (UiRunT *)calloc(1, sizeof(UiRunT));
PrintHooksT hooks;
@@ -244,7 +276,7 @@ bool printDialog(AppContextT *ctx, const char *title, int32_t pageCount, PrintCh
WidgetT *fileRow = addRow(root, "File name:");
d.file = wgtTextInput(fileRow, PRINT_PATH_MAX - 1);
- d.file->prefW = wgtChars(UI_FIELD_CHARS);
+ d.file->minW = wgtChars(UI_FIELD_CHARS);
d.browse = wgtButton(fileRow, "&Browse...");
d.browse->userData = &d;
d.browse->onClick = browseFile;
@@ -266,10 +298,10 @@ bool printDialog(AppContextT *ctx, const char *title, int32_t pageCount, PrintCh
row->spacing = wgtPixels(UI_GAP);
wgtLabel(row, "From");
d.from = wgtSpinner(row, 1, pageCount, 1);
- d.from->prefW = wgtChars(UI_SPIN_CHARS);
+ d.from->minW = wgtChars(UI_SPIN_CHARS);
wgtLabel(row, "to");
d.to = wgtSpinner(row, 1, pageCount, 1);
- d.to->prefW = wgtChars(UI_SPIN_CHARS);
+ d.to->minW = wgtChars(UI_SPIN_CHARS);
wgtSetName(d.pages, "printPages");
wgtSetName(d.from, "printFrom");
wgtSetName(d.to, "printTo");
@@ -339,8 +371,8 @@ bool printPageSetupDialog(AppContextT *ctx) {
d.footer = wgtTextInput(addRow(root, "Footer:"), PRINT_HF_MAX - 1);
d.wrap = wgtCheckbox(root, "Wrap long lines (otherwise they are cut off)");
d.page = s.page;
- d.header->prefW = wgtChars(UI_FIELD_CHARS);
- d.footer->prefW = wgtChars(UI_FIELD_CHARS);
+ d.header->minW = wgtChars(UI_FIELD_CHARS);
+ d.footer->minW = wgtChars(UI_FIELD_CHARS);
wgtSetName(d.header, "setupHeader");
wgtSetName(d.footer, "setupFooter");
wgtSetName(d.wrap, "setupWrap");
@@ -368,51 +400,75 @@ bool printPageSetupDialog(AppContextT *ctx) {
// Print a picture on the printer in the settings (it must print
// pictures), one page per copy.
+// Print a picture (R, G, B bytes) on a page of its own, copies times.
static bool printRgb(AppContextT *ctx, const uint8_t *rgb, int32_t w, int32_t h, const char *title, bool askFirst) {
- PrintSettingsT s;
- PrintChoiceT ch;
- char err[PRINT_MSG_MAX];
- const PrintDriverT *drv;
+ PrintSettingsT s;
+ PrintChoiceT ch;
printSettingsLoad(&s);
- drv = printDriver(s.model);
- if (!drv) {
- dvxMessageBox(ctx, UI_TITLE, "The printer model chosen in the Control Panel prints text only. To print pictures, choose your printer's model on the Control Panel's Printer page.", MB_OK | MB_ICONINFO);
+ if (!printModelPage(s.model, NULL)) {
+ textOnlyBox(ctx);
return false;
}
- memset(&ch, 0, sizeof(ch));
- ch.dest = s.dest;
- ch.copies = 1;
- snprintf(ch.filePath, sizeof(ch.filePath), "%s", s.filePath);
+ choiceOf(&s, &ch);
if (askFirst && !printDialog(ctx, UI_TITLE, 0, &ch)) {
return false;
}
- PrintJobT *job = openJob(ctx, ch.dest, ch.filePath, title, true, err, sizeof(err));
+ PrintDocT *doc = printUiDocOpen(ctx, title, &ch);
- if (!job) {
- dvxErrorBox(ctx, UI_TITLE, err);
+ if (!doc) {
return false;
}
- bool ok = true;
-
- for (int32_t c = 0; ok && c < ch.copies; c++) {
- ok = printRasterJob(job, drv, rgb, w, h, w * RGB_BYTES);
+ for (int32_t c = 0; c < ch.copies; c++) {
+ printDocPicture(doc, rgb, w, h, w * RGB_BYTES);
}
- bool cancelled = printJobCancelled(job);
+ return docClose(ctx, doc, "The picture could not be printed.");
+}
- ok = printJobClose(job) && ok;
- if (!ok && !cancelled) {
- dvxErrorBox(ctx, UI_TITLE, "The picture could not be printed.");
+bool printUiDocClose(AppContextT *ctx, PrintDocT *doc) {
+ return docClose(ctx, doc, "The document could not be printed.");
+}
+
+
+PrintDocT *printUiDocOpen(AppContextT *ctx, const char *title, const PrintChoiceT *choice) {
+ PrintSettingsT s;
+ PrintChoiceT ch;
+ char err[PRINT_MSG_MAX];
+
+ printSettingsLoad(&s);
+
+ if (!printModelPage(s.model, NULL)) {
+ textOnlyBox(ctx);
+ return NULL;
}
- return ok;
+ choiceOf(&s, &ch);
+
+ if (!choice) {
+ choice = &ch;
+ }
+
+ PrintJobT *job = openJob(ctx, choice->dest, choice->filePath, title, true, err, sizeof(err));
+
+ if (!job) {
+ dvxErrorBox(ctx, UI_TITLE, err);
+ return NULL;
+ }
+
+ PrintDocT *doc = printDocOpen(job, s.model);
+
+ if (!doc) {
+ dvxErrorBox(ctx, UI_TITLE, "Not enough memory to print.");
+ }
+
+ return doc;
}
@@ -468,10 +524,7 @@ bool printUiText(AppContextT *ctx, const char *text, const char *title, bool ask
PrintChoiceT ch;
printSettingsLoad(&s);
- memset(&ch, 0, sizeof(ch));
- ch.dest = s.dest;
- ch.copies = 1;
- snprintf(ch.filePath, sizeof(ch.filePath), "%s", s.filePath);
+ choiceOf(&s, &ch);
if (askFirst && !printDialog(ctx, UI_TITLE, printTextPageCount(text, (int32_t)strlen(text), &s.page, s.model), &ch)) {
return false;
@@ -623,6 +676,12 @@ static bool setupDlgOk(WindowT *dlg, void *userCtx) {
}
+// Pictures need a printer model that prints them.
+static void textOnlyBox(AppContextT *ctx) {
+ dvxMessageBox(ctx, UI_TITLE, "The printer model chosen in the Control Panel prints text only. To print pictures, choose your printer's model on the Control Panel's Printer page.", MB_OK | MB_ICONINFO);
+}
+
+
// The display-format pixels of a picture or window as R, G, B bytes
// (NULL when out of memory).
static uint8_t *toRgb(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch) {
diff --git a/src/libs/kpunch/taskmgr/shellTaskMgr.c b/src/libs/kpunch/taskmgr/shellTaskMgr.c
index 7d36613a..e301aa12 100644
--- a/src/libs/kpunch/taskmgr/shellTaskMgr.c
+++ b/src/libs/kpunch/taskmgr/shellTaskMgr.c
@@ -51,7 +51,6 @@ DVX_ABI_STAMP(taskmgr);
#define TM_COL_COUNT 6
#define TM_WIN_W 580
#define TM_WIN_H 280
-#define TM_LIST_PREF_H 160
#define TM_BTN_SPACING 8
#define TM_BTN_W 90
@@ -360,7 +359,6 @@ static void taskMgrCreate(AppContextT *ctx) {
sTmListView = wgtListView(root);
sTmListView->weight = WGT_WEIGHT_FILL;
- sTmListView->prefH = wgtPixels(TM_LIST_PREF_H);
sTmListView->onDblClick = onTmSwitchTo;
wgtListViewSetColumns(sTmListView, tmCols, TM_COL_COUNT);
@@ -372,15 +370,15 @@ static void taskMgrCreate(AppContextT *ctx) {
WidgetT *switchBtn = wgtButton(btnRow, "&Switch To");
switchBtn->onClick = onTmSwitchTo;
- switchBtn->prefW = wgtPixels(TM_BTN_W);
+ switchBtn->minW = wgtPixels(TM_BTN_W);
WidgetT *endBtn = wgtButton(btnRow, "&End Task");
endBtn->onClick = onTmEndTask;
- endBtn->prefW = wgtPixels(TM_BTN_W);
+ endBtn->minW = wgtPixels(TM_BTN_W);
WidgetT *runBtn = wgtButton(btnRow, "&Run...");
runBtn->onClick = onTmRun;
- runBtn->prefW = wgtPixels(TM_BTN_W);
+ runBtn->minW = wgtPixels(TM_BTN_W);
shellRegisterDesktopUpdate(shellTaskMgrRefresh);
shellRegisterIdle(onTmIdle, NULL);
diff --git a/src/savers/README.md b/src/savers/README.md
new file mode 100644
index 00000000..ce7e0dbe
--- /dev/null
+++ b/src/savers/README.md
@@ -0,0 +1,34 @@
+# DVX Screen Savers
+
+A screen saver is an ordinary DVX program (`.app`) kept in a SAVERS
+folder instead of APPS: `SHARED\SAVERS` (the ones DVX ships and add-ons
+install) or the user's `SAVERS`. Program Manager does not list them.
+DVX.INI `[desktop] saver` names the one chosen (Control Panel's Desktop
+page) and `saverWait` the minutes with no key or mouse input before the
+shell starts it (`dvxshell/shellSaver.c`).
+
+The shell starts a saver with the argument `/S`. The saver takes the
+whole screen (full-screen mode: `dvxExclusiveBegin` in C, `SCREEN` and
+`FULLSCREEN ON` in BASIC) and draws until its window is closed: at the
+next key or mouse input (after a second's grace) the shell asks it to
+close, and ends it if it is still there three seconds later. A saver
+needs no input handling of its own; the input that wakes the screen goes
+to the saver, never to the program underneath.
+
+The savers here are built by `kpunch/Makefile` into
+`bin/shared/savers/.app`; nothing lists them:
+
+* a directory `/` holding `.c` is a C saver (every `.c` in it
+ linked, `.res` appended by `dvxres`);
+* a directory `/` holding `.dbp` is a BASIC saver
+ (`bascomp`).
+
+| Saver | Language | What it draws |
+|-------|----------|---------------|
+| `blank/` | BASIC | A black screen |
+| `marquee/` | BASIC | DVX's name drifting across the screen in changing colours |
+| `mystify/` | BASIC | Two bouncing four-cornered shapes trailing their outlines |
+| `starfld/` | C | Stars flying out of the middle of the screen |
+
+The name Control Panel lists is the program's own: the `name` resource
+of a C saver, the project's Name in a BASIC one.
diff --git a/src/savers/kpunch/Makefile b/src/savers/kpunch/Makefile
new file mode 100644
index 00000000..50e70408
--- /dev/null
+++ b/src/savers/kpunch/Makefile
@@ -0,0 +1,82 @@
+# 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.
+
+# DVX screen savers Makefile -- builds the savers DVX ships into
+# bin/shared/savers (SHARED\SAVERS, DvxKindSaversE): programs the shell
+# starts after the wait with no input (dvxshell/shellSaver.c).
+#
+# One directory per saver. A directory / holding /.c
+# is a C saver (all its .c files linked, .res appended by dvxres);
+# one holding /.dbp is a BASIC saver (bascomp). Nothing
+# lists them.
+
+include ../../common.mk
+
+SAVER_INCLUDES = -I../../libs/kpunch -I../../libs/kpunch/libdvx -I../../libs/kpunch/libdvx/platform -I../../libs/kpunch/libdvx/thirdparty -I../../widgets/kpunch -I../../libs/kpunch/libtasks -I../../libs/kpunch/dvxshell
+CFLAGS = $(DOS_CFLAGS) $(SAVER_INCLUDES)
+
+OBJDIR = ../../../obj/savers
+BINDIR = ../../../bin/shared/savers
+DVXRES = ../../../bin/host/dvxres
+BASCOMP = ../../../bin/host/bascomp
+
+C_SAVERS = $(sort $(foreach f,$(wildcard */*.c),$(if $(filter $(notdir $(basename $(f))),$(patsubst %/,%,$(dir $(f)))),$(patsubst %/,%,$(dir $(f))))))
+BASIC_SAVERS = $(sort $(foreach f,$(wildcard */*.dbp),$(if $(filter $(notdir $(basename $(f))),$(patsubst %/,%,$(dir $(f)))),$(patsubst %/,%,$(dir $(f))))))
+
+saverSrcs = $(wildcard $(1)/*.c)
+saverObjs = $(patsubst $(1)/%.c,$(OBJDIR)/$(1)/%.o,$(call saverSrcs,$(1)))
+saverRes = $(wildcard $(1)/$(1).res)
+
+SAVER_OUTS = $(foreach s,$(C_SAVERS) $(BASIC_SAVERS),$(BINDIR)/$(s).app)
+
+.PHONY: all clean
+
+all: $(SAVER_OUTS)
+
+# $(1) = saver directory name.
+define C_SAVER_RULES
+$(BINDIR)/$(1).app: $(call saverObjs,$(1)) $(call saverRes,$(1))
+ @mkdir -p $$(dir $$@)
+ $$(DXE3GEN) -o $$@ -U $(call saverObjs,$(1))
+ $(if $(call saverRes,$(1)),cd $(1) && $$(abspath $$(DVXRES)) build $$(abspath $$@) $(1).res)
+
+$(OBJDIR)/$(1)/%.o: $(1)/%.c $$(DVX_MAKEFILES)
+ @mkdir -p $$(dir $$@)
+ $$(CC) $$(CFLAGS) -c -o $$@ $$<
+endef
+
+# $(1) = saver directory name.
+define BASIC_SAVER_RULES
+$(BINDIR)/$(1).app: $(wildcard $(1)/*.dbp $(1)/*.bas $(1)/*.frm) $$(BASCOMP)
+ @mkdir -p $$(dir $$@)
+ $$(BASCOMP) $(1)/$(1).dbp -o $$@ -release
+endef
+
+$(foreach s,$(C_SAVERS),$(eval $(call C_SAVER_RULES,$(s))))
+$(foreach s,$(BASIC_SAVERS),$(eval $(call BASIC_SAVER_RULES,$(s))))
+
+C_SAVER_OBJS = $(foreach s,$(C_SAVERS),$(call saverObjs,$(s)))
+
+-include $(C_SAVER_OBJS:.o=.d)
+
+clean:
+ rm -f $(C_SAVER_OBJS) $(C_SAVER_OBJS:.o=.d) $(SAVER_OUTS)
diff --git a/src/savers/kpunch/blank/blank.bas b/src/savers/kpunch/blank/blank.bas
new file mode 100644
index 00000000..cbadbd06
--- /dev/null
+++ b/src/savers/kpunch/blank/blank.bas
@@ -0,0 +1,32 @@
+' 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.
+
+' Blank Screen: a screen saver (DVX starts it with /S after the wait with
+' no input, and ends it at the next). The whole screen, black.
+
+SCREEN 13
+CLS
+FULLSCREEN ON
+
+DO
+ FRAMEWAIT 10
+LOOP
diff --git a/src/savers/kpunch/blank/blank.dbp b/src/savers/kpunch/blank/blank.dbp
new file mode 100644
index 00000000..acf260bb
--- /dev/null
+++ b/src/savers/kpunch/blank/blank.dbp
@@ -0,0 +1,11 @@
+[Project]
+Name = Blank Screen
+Author = Scott Duensing
+Publisher = Kangaroo Punch Studios
+Version = 1.00
+Copyright = Copyright 2026 Scott Duensing
+Description = Screen saver: a black screen
+
+[Modules]
+File0 = blank.bas
+Count = 1
diff --git a/src/savers/kpunch/marquee/marquee.bas b/src/savers/kpunch/marquee/marquee.bas
new file mode 100644
index 00000000..f20c43b5
--- /dev/null
+++ b/src/savers/kpunch/marquee/marquee.bas
@@ -0,0 +1,85 @@
+' 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.
+
+' Marquee: a screen saver (DVX starts it with /S after the wait with no
+' input, and ends it at the next). DVX's name drifts across the whole
+' screen, bouncing off its edges, in a colour that changes as it goes.
+
+OPTION EXPLICIT
+
+CONST MESSAGE = "The DOS Visual eXecutive"
+CONST COLS = 40 ' SCREEN 13's text grid
+CONST ROWS = 25
+CONST STEP_TICKS = 4 ' frames between moves
+CONST RATE = 30 ' frames a second
+CONST COLOR_MIN = 32 ' the bright part of the standard palette
+CONST COLOR_MAX = 103
+
+DIM row AS INTEGER
+DIM col AS INTEGER
+DIM dRow AS INTEGER
+DIM dCol AS INTEGER
+DIM hue AS INTEGER
+DIM tick AS INTEGER
+
+SCREEN 13
+CLS
+FULLSCREEN ON
+RANDOMIZE TIMER
+
+row = INT(RND * ROWS) + 1
+col = INT(RND * (COLS - LEN(MESSAGE))) + 1
+dRow = 1
+dCol = 1
+hue = COLOR_MIN
+
+DO
+ tick = tick + 1
+
+ IF tick >= STEP_TICKS THEN
+ tick = 0
+ LOCATE row, col
+ PRINT SPACE$(LEN(MESSAGE));
+
+ IF row + dRow < 1 OR row + dRow > ROWS THEN
+ dRow = -dRow
+ END IF
+
+ IF col + dCol < 1 OR col + dCol + LEN(MESSAGE) - 1 > COLS THEN
+ dCol = -dCol
+ END IF
+
+ row = row + dRow
+ col = col + dCol
+ hue = hue + 1
+
+ IF hue > COLOR_MAX THEN
+ hue = COLOR_MIN
+ END IF
+
+ COLOR hue
+ LOCATE row, col
+ PRINT MESSAGE;
+ END IF
+
+ FRAMEWAIT RATE
+LOOP
diff --git a/src/savers/kpunch/marquee/marquee.dbp b/src/savers/kpunch/marquee/marquee.dbp
new file mode 100644
index 00000000..79b30a93
--- /dev/null
+++ b/src/savers/kpunch/marquee/marquee.dbp
@@ -0,0 +1,11 @@
+[Project]
+Name = Marquee
+Author = Scott Duensing
+Publisher = Kangaroo Punch Studios
+Version = 1.00
+Copyright = Copyright 2026 Scott Duensing
+Description = Screen saver: DVX's name drifting across the screen in changing colours
+
+[Modules]
+File0 = marquee.bas
+Count = 1
diff --git a/src/savers/kpunch/mystify/mystify.bas b/src/savers/kpunch/mystify/mystify.bas
new file mode 100644
index 00000000..c635e566
--- /dev/null
+++ b/src/savers/kpunch/mystify/mystify.bas
@@ -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.
+
+' Mystify: a screen saver (DVX starts it with /S after the wait with no
+' input, and ends it at the next). Two outlines of four corners each
+' bounce around the whole screen; each leaves a trail of its last shapes,
+' the oldest wiped as the next is drawn, and changes colour now and then.
+
+OPTION EXPLICIT
+
+CONST SHAPES = 2
+CONST CORNERS = 4
+CONST TRAIL = 6 ' outlines each shape leaves behind
+CONST WIDTH_PX = 320 ' SCREEN 13
+CONST HEIGHT_PX = 200
+CONST SPEED = 4 ' most pixels a corner moves a frame
+CONST RATE = 30 ' frames a second
+CONST RECOLOR = 90 ' frames between colour changes
+CONST COLOR_MIN = 32 ' the bright part of the standard palette
+CONST COLOR_MAX = 103
+
+DIM SHARED x(SHAPES - 1, CORNERS - 1) AS INTEGER
+DIM SHARED y(SHAPES - 1, CORNERS - 1) AS INTEGER
+DIM SHARED dx(SHAPES - 1, CORNERS - 1) AS INTEGER
+DIM SHARED dy(SHAPES - 1, CORNERS - 1) AS INTEGER
+DIM SHARED trailX(SHAPES - 1, TRAIL - 1, CORNERS - 1) AS INTEGER
+DIM SHARED trailY(SHAPES - 1, TRAIL - 1, CORNERS - 1) AS INTEGER
+DIM hue(SHAPES - 1) AS INTEGER
+DIM slot AS INTEGER
+DIM frame AS LONG
+DIM s AS INTEGER
+DIM c AS INTEGER
+
+SCREEN 13
+CLS
+FULLSCREEN ON
+RANDOMIZE TIMER
+
+FOR s = 0 TO SHAPES - 1
+ hue(s) = COLOR_MIN + INT(RND * (COLOR_MAX - COLOR_MIN + 1))
+
+ FOR c = 0 TO CORNERS - 1
+ x(s, c) = INT(RND * WIDTH_PX)
+ y(s, c) = INT(RND * HEIGHT_PX)
+ dx(s, c) = RandomSpeed()
+ dy(s, c) = RandomSpeed()
+ NEXT c
+NEXT s
+
+DO
+ FOR s = 0 TO SHAPES - 1
+ ' The oldest outline of the trail goes; this frame's takes its place
+ Outline s, slot, 0
+
+ FOR c = 0 TO CORNERS - 1
+ MoveCorner s, c
+ trailX(s, slot, c) = x(s, c)
+ trailY(s, slot, c) = y(s, c)
+ NEXT c
+
+ IF frame MOD RECOLOR = 0 THEN
+ hue(s) = COLOR_MIN + INT(RND * (COLOR_MAX - COLOR_MIN + 1))
+ END IF
+
+ Outline s, slot, hue(s)
+ NEXT s
+
+ slot = (slot + 1) MOD TRAIL
+ frame = frame + 1
+ FRAMEWAIT RATE
+LOOP
+
+
+' A corner's next place: it turns back at an edge.
+SUB MoveCorner(BYVAL s AS INTEGER, BYVAL c AS INTEGER)
+ IF x(s, c) + dx(s, c) < 0 OR x(s, c) + dx(s, c) >= WIDTH_PX THEN
+ dx(s, c) = -dx(s, c)
+ END IF
+
+ IF y(s, c) + dy(s, c) < 0 OR y(s, c) + dy(s, c) >= HEIGHT_PX THEN
+ dy(s, c) = -dy(s, c)
+ END IF
+
+ x(s, c) = x(s, c) + dx(s, c)
+ y(s, c) = y(s, c) + dy(s, c)
+END SUB
+
+
+' One outline of the trail, drawn in colour shade (0: wiped).
+SUB Outline(BYVAL s AS INTEGER, BYVAL slot AS INTEGER, BYVAL shade AS INTEGER)
+ DIM c AS INTEGER
+ DIM n AS INTEGER
+
+ FOR c = 0 TO CORNERS - 1
+ n = (c + 1) MOD CORNERS
+ LINE (trailX(s, slot, c), trailY(s, slot, c))-(trailX(s, slot, n), trailY(s, slot, n)), shade
+ NEXT c
+END SUB
+
+
+' A speed between 1 and SPEED pixels a frame, either way.
+FUNCTION RandomSpeed() AS INTEGER
+ DIM v AS INTEGER
+
+ v = INT(RND * SPEED) + 1
+
+ IF RND < 0.5 THEN
+ v = -v
+ END IF
+
+ RandomSpeed = v
+END FUNCTION
diff --git a/src/savers/kpunch/mystify/mystify.dbp b/src/savers/kpunch/mystify/mystify.dbp
new file mode 100644
index 00000000..5e0c83bd
--- /dev/null
+++ b/src/savers/kpunch/mystify/mystify.dbp
@@ -0,0 +1,11 @@
+[Project]
+Name = Mystify
+Author = Scott Duensing
+Publisher = Kangaroo Punch Studios
+Version = 1.00
+Copyright = Copyright 2026 Scott Duensing
+Description = Screen saver: bouncing outlines trailing their past shapes
+
+[Modules]
+File0 = mystify.bas
+Count = 1
diff --git a/src/savers/kpunch/starfld/starfld.c b/src/savers/kpunch/starfld/starfld.c
new file mode 100644
index 00000000..a62c01e2
--- /dev/null
+++ b/src/savers/kpunch/starfld/starfld.c
@@ -0,0 +1,210 @@
+// 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.
+
+// starfld.c -- Starfield, a screen saver: stars fly out of the middle of
+// the screen as if the screen were moving through them, the nearer the
+// brighter and bigger.
+//
+// DVX starts it with /S after the wait with no input and ends it at the
+// next input by closing its window (the shell's screen saver, shellSaver.c),
+// so it does no input handling of its own: it takes the whole screen at
+// 320x200 in 256 colours (full-screen mode) and draws a frame every
+// FRAME_HZ-th of a second until its window is closed.
+
+#include "dvxApp.h"
+#include "dvxPlat.h"
+#include "dvxWm.h"
+#include "shellApp.h"
+#include "game/game.h"
+
+#include
+#include
+#include
+
+#define SAVER_TITLE "Starfield"
+#define FB_W 320
+#define FB_H 200
+#define FULL_BPP 8
+#define WIN_W 160 // the window while the screen is the desktop's
+#define WIN_H 100
+#define FRAME_HZ 30
+#define STARS 240
+#define DEPTH_FAR 1024 // where a star starts
+#define DEPTH_NEAR 16 // where it has passed the viewer
+#define STAR_SPEED 12 // depth units a frame
+#define FOCAL 128 // screen position = centre + x * FOCAL / depth
+#define SPREAD_X (FB_W * DEPTH_FAR / FOCAL) // a new star anywhere the far plane shows
+#define SPREAD_Y (FB_H * DEPTH_FAR / FOCAL)
+#define SHADES 16 // palette 1..SHADES: dark grey to white
+#define SHADE_LOW 112 // the far stars' brightness
+#define BIG_DEPTH (DEPTH_FAR / 4) // nearer than this: 2x2 pixels
+#define PAL_ENTRIES 256
+#define PAL_CHANNELS 3
+#define LCG_MUL 1103515245U
+#define LCG_ADD 12345U
+#define RAND_SHIFT 8
+
+// A star: where it is across the screen (x, y about the centre) and how far
+typedef struct {
+ int32_t x;
+ int32_t y;
+ int32_t z;
+} StarT;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void draw(void);
+static void onClose(WindowT *win);
+static void paletteInit(void);
+static uint32_t randomNext(void);
+static void starNew(StarT *s, bool anyDepth);
+int32_t appMain(DxeAppContextT *ctx);
+
+// ============================================================
+// App descriptor
+// ============================================================
+
+SHELL_APP_DESCRIPTOR(
+ .name = SAVER_TITLE,
+ .hasMainLoop = true,
+ .multiInstance = false,
+ .stackSize = SHELL_STACK_DEFAULT,
+ .priority = TS_PRIORITY_NORMAL
+);
+
+static AppContextT *sAc = NULL;
+static WindowT *sWin = NULL;
+static bool sQuit = false;
+static uint32_t sSeed = 1;
+static StarT sStars[STARS];
+static uint8_t sFb[FB_W * FB_H];
+static uint8_t sPal[PAL_ENTRIES * PAL_CHANNELS];
+
+
+// Each star a step nearer; one that passed the viewer or left the screen
+// starts again far away.
+static void draw(void) {
+ memset(sFb, 0, sizeof(sFb));
+
+ for (int32_t i = 0; i < STARS; i++) {
+ StarT *s = &sStars[i];
+
+ s->z -= STAR_SPEED;
+
+ if (s->z < DEPTH_NEAR) {
+ starNew(s, false);
+ }
+
+ int32_t sx = FB_W / 2 + s->x * FOCAL / s->z;
+ int32_t sy = FB_H / 2 + s->y * FOCAL / s->z;
+ int32_t size = s->z < BIG_DEPTH ? 2 : 1;
+
+ if (sx < 0 || sy < 0 || sx + size > FB_W || sy + size > FB_H) {
+ starNew(s, false);
+ continue;
+ }
+
+ uint8_t shade = (uint8_t)(1 + (DEPTH_FAR - s->z) * (SHADES - 1) / DEPTH_FAR);
+
+ for (int32_t dy = 0; dy < size; dy++) {
+ memset(&sFb[(sy + dy) * FB_W + sx], shade, (size_t)size);
+ }
+ }
+}
+
+
+static void onClose(WindowT *win) {
+ (void)win;
+ sQuit = true;
+}
+
+
+// Black, then SHADES greys from SHADE_LOW up to white.
+static void paletteInit(void) {
+ memset(sPal, 0, sizeof(sPal));
+
+ for (int32_t i = 1; i <= SHADES; i++) {
+ uint8_t level = (uint8_t)(SHADE_LOW + (UINT8_MAX - SHADE_LOW) * (i - 1) / (SHADES - 1));
+
+ memset(&sPal[i * PAL_CHANNELS], level, PAL_CHANNELS);
+ }
+}
+
+
+static uint32_t randomNext(void) {
+ sSeed = sSeed * LCG_MUL + LCG_ADD;
+ return sSeed >> RAND_SHIFT;
+}
+
+
+// Somewhere the far plane shows, far away (anyDepth: at any depth, so the
+// first frame is already full of stars).
+static void starNew(StarT *s, bool anyDepth) {
+ s->x = (int32_t)(randomNext() % SPREAD_X) - SPREAD_X / 2;
+ s->y = (int32_t)(randomNext() % SPREAD_Y) - SPREAD_Y / 2;
+ s->z = anyDepth ? DEPTH_NEAR + (int32_t)(randomNext() % (DEPTH_FAR - DEPTH_NEAR)) : DEPTH_FAR;
+}
+
+
+int32_t appMain(DxeAppContextT *ctx) {
+ GameFramePacerT pacer;
+
+ sAc = ctx->shellCtx;
+ sWin = dvxCreateWindowCentered(sAc, SAVER_TITLE, WIN_W, WIN_H, false);
+
+ if (!sWin) {
+ return -1;
+ }
+
+ sWin->gameInput = true;
+ sWin->onClose = onClose;
+ sQuit = false;
+ sSeed = (uint32_t)platformMicros() | 1;
+
+ if (dvxExclusiveBegin(sAc, sWin, FB_W, FB_H, FULL_BPP, NULL, NULL) != DVX_EXCL_OK) {
+ dvxLog("Starfield: no full-screen mode");
+ dvxDestroyWindow(sAc, sWin);
+ return -1;
+ }
+
+ paletteInit();
+ dvxExclusiveSetPalette(sAc, sPal, 0, PAL_ENTRIES);
+
+ for (int32_t i = 0; i < STARS; i++) {
+ starNew(&sStars[i], true);
+ }
+
+ gameFrameInit(&pacer, FRAME_HZ);
+
+ // Present8 does nothing while another window has the screen.
+ while (!sQuit) {
+ gameFrameWait(&pacer);
+ draw();
+ dvxExclusivePresent8(sAc, sFb, FB_W, FB_H, FB_W);
+ }
+
+ dvxDestroyWindow(sAc, sWin);
+ sWin = NULL;
+ return 0;
+}
diff --git a/src/savers/kpunch/starfld/starfld.res b/src/savers/kpunch/starfld/starfld.res
new file mode 100644
index 00000000..eb14c535
--- /dev/null
+++ b/src/savers/kpunch/starfld/starfld.res
@@ -0,0 +1,6 @@
+# starfld.res -- Resource manifest for the Starfield screen saver
+name text "Starfield"
+author text "Scott Duensing"
+copyright text "Copyright 2026 Scott Duensing"
+publisher text "Kangaroo Punch Studios"
+description text "Screen saver: stars flying out of the middle of the screen"
diff --git a/src/sdkHeaders.mk b/src/sdkHeaders.mk
index 6265fcd0..9b2d42c8 100644
--- a/src/sdkHeaders.mk
+++ b/src/sdkHeaders.mk
@@ -31,7 +31,7 @@
# with a trailing slash (empty at the top level).
SDK_CORE_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/libdvx/,dvxApp.h dvxTypes.h dvxWgt.h dvxWgtP.h dvxWm.h dvxDraw.h dvxVideo.h dvxComp.h dvxPrefs.h dvxDlg.h dvxRes.h dvxFont.h dvxText.h dvxFlow.h dvxCur.h dvxPal.h dvxMem.h dvxDrv.h platform/dvxPlat.h)
-SDK_SHELL_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/dvxshell/,shellApp.h shellAssoc.h shellInf.h shellProg.h shellRun.h pmModel.h) $(SDK_ROOT)src/libs/kpunch/taskmgr/shellTaskMgr.h
+SDK_SHELL_H = $(addprefix $(SDK_ROOT)src/libs/kpunch/dvxshell/,shellApp.h shellAssoc.h shellInf.h shellProg.h shellRun.h shellSaver.h pmModel.h) $(SDK_ROOT)src/libs/kpunch/taskmgr/shellTaskMgr.h
SDK_TASKS_H = $(SDK_ROOT)src/libs/kpunch/libtasks/taskSwch.h
SDK_SQL_H = $(SDK_ROOT)src/libs/kpunch/sql/dvxSql.h
SDK_TEXTHELP_H = $(SDK_ROOT)src/libs/kpunch/texthelp/textHelp.h
diff --git a/src/test/abi/Makefile b/src/test/abi/Makefile
index 8b048bca..68e7a6d6 100644
--- a/src/test/abi/Makefile
+++ b/src/test/abi/Makefile
@@ -29,6 +29,10 @@
# make update regenerate abiSizes.txt after a DVX_ABI_VERSION bump, or
# after adding a public struct (refuses a changed struct
# without a bump)
+# make update KEEP_VERSION=1
+# regenerate it at the version it has, changed structs too
+# (nothing built against DVX is released yet, so layouts
+# change without a bump)
#
# The sizes come from the DOS cross compiler (CC), so nothing is run and
# SAN/COV do not apply.
@@ -51,7 +55,7 @@ check:
./abiCheck.py check $(TABLE) $(OBJDIR) $(CC) $(ABI_FLAGS) -- $(HEADERS)
update:
- ./abiCheck.py update $(TABLE) $(OBJDIR) $(CC) $(ABI_FLAGS) -- $(HEADERS)
+ ./abiCheck.py $(if $(KEEP_VERSION),keep,update) $(TABLE) $(OBJDIR) $(CC) $(ABI_FLAGS) -- $(HEADERS)
clean:
rm -rf $(OBJDIR)
diff --git a/src/test/abi/abiCheck.py b/src/test/abi/abiCheck.py
index 021c49c6..008c70c8 100755
--- a/src/test/abi/abiCheck.py
+++ b/src/test/abi/abiCheck.py
@@ -23,7 +23,7 @@
# abiCheck.py -- public struct sizes against the committed ABI table.
#
-# Usage: abiCheck.py check|update TABLE OUTDIR CC [CFLAGS...] -- HEADERS...
+# Usage: abiCheck.py check|update|keep TABLE OUTDIR CC [CFLAGS...] -- HEADERS...
#
# Every struct typedef in the SDK headers (a line "} NameT;") is measured
# with the DOS cross compiler: a generated C file includes every header and
@@ -37,6 +37,9 @@
# another version, or missing a new struct, needs `make update`.
# update rewrites the table, but refuses to record a changed or removed
# struct without a DVX_ABI_VERSION bump.
+# keep rewrites the table at the version it has, changed and removed
+# structs too (listed): while nothing built against DVX is out in
+# the world, layouts change without a bump.
import os
import re
@@ -125,7 +128,7 @@ def writeTable(path, version, sizes):
def main():
if len(sys.argv) < 6 or '--' not in sys.argv:
- print('usage: abiCheck.py check|update TABLE OUTDIR CC [CFLAGS...] -- HEADERS...')
+ print('usage: abiCheck.py check|update|keep TABLE OUTDIR CC [CFLAGS...] -- HEADERS...')
sys.exit(2)
mode = sys.argv[1]
@@ -148,6 +151,13 @@ def main():
breaking.append('%s changed size %d -> %d' % (n, oldSizes[n], sizes[n]))
added = sorted(n for n in sizes if n not in oldSizes)
+ if mode == 'keep':
+ for b in breaking:
+ print('abiCheck: recorded at the same version: ' + b)
+ writeTable(table, version, sizes)
+ print('abiCheck: wrote %s (DVX_ABI_VERSION %d kept, %d structs)' % (table, version, len(sizes)))
+ return
+
if mode == 'update':
if breaking and version == oldVersion:
for b in breaking:
diff --git a/src/test/abi/abiSizes.txt b/src/test/abi/abiSizes.txt
index 73b6c13b..2d926e78 100644
--- a/src/test/abi/abiSizes.txt
+++ b/src/test/abi/abiSizes.txt
@@ -7,19 +7,22 @@ AccelEntryT 12
AccelTableT 12
AnsiTermApiT 36
AnsiTermExApiT 20
-AppContextT 3480
+AppContextT 3484
AppDescriptorT 80
-AudioDriverT 28
+AudioDriverT 36
AudioDrvConfigT 16
-AudioDrvInfoT 84
+AudioDrvInfoT 92
AudioFmInfoT 76
AudioFormatT 12
-AudioInfoT 100
+AudioInfoT 108
AudioLockItemT 8
-AudioMidiInfoT 66
+AudioMidiInfoT 68
+AudioMidiMsgT 8
AudioStatsT 24
-AudioSynthDriverT 36
-AudioSynthDrvInfoT 72
+AudioSynthDriverT 64
+AudioSynthDrvInfoT 84
+AudioWtInfoT 92
+AudioWtVoiceT 96
BevelStyleT 16
BitmapFontT 64
BlitOpsT 12
@@ -121,9 +124,10 @@ PopupStateT 52
PrefsChoiceT 12
PrintChoiceT 276
PrintHooksT 16
+PrintPageInfoT 20
PrintPageSetupT 192
-PrintPortOpsT 16
-PrintSettingsT 460
+PrintPortOpsT 28
+PrintSettingsT 468
PrintTextInfoT 24
ProgressBarApiT 32
RadioApiT 36
@@ -170,7 +174,7 @@ WgtPropDescT 20
WgtSigInfoT 7
WgtValueT 12
WidgetClassT 136
-WidgetT 244
+WidgetT 236
WindowStackT 60
WindowT 308
WrapBoxApiT 12
diff --git a/src/test/audio/Makefile b/src/test/audio/Makefile
index 81fdab77..36255599 100644
--- a/src/test/audio/Makefile
+++ b/src/test/audio/Makefile
@@ -42,24 +42,24 @@ WORKDIR = $(BINDIR)/work
DRVDIR = $(WORKDIR)/libs/kpunch/audio/drivers
GOLDEN = $(abspath golden/mixer.txt)
-AUDIO_SRCS = $(filter-out $(AUDIO_DIR)/audioShell.c,$(wildcard $(AUDIO_DIR)/*.c)) $(AUDIO_DIR)/drivers/sb/sbCalc.c
+AUDIO_SRCS = $(filter-out $(AUDIO_DIR)/audioShell.c,$(wildcard $(AUDIO_DIR)/*.c)) $(AUDIO_DIR)/drivers/sb/sbCalc.c $(AUDIO_DIR)/drivers/wss/wssCalc.c $(AUDIO_DIR)/codec/codecCalc.c $(AUDIO_DIR)/gus/gusCalc.c $(AUDIO_DIR)/drivers/awe/aweCalc.c $(AUDIO_DIR)/math/drvMath.c $(AUDIO_DIR)/isa/isaCalc.c
AUDIO_OBJS = $(patsubst $(AUDIO_DIR)/%.c,$(BINDIR)/lib/%.o,$(AUDIO_SRCS))
LIBS_A = $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A)
-CFLAGS = $(DVXTEST_CFLAGS) -I$(AUDIO_DIR) -I$(AUDIO_DIR)/drivers/sb -I$(DVXTEST_ROOT)/src/libs/kpunch -DAUDIO_GOLDEN='"$(GOLDEN)"' -DAUDIO_WORKDIR='"$(WORKDIR)"' -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"'
+CFLAGS = $(DVXTEST_CFLAGS) -I$(AUDIO_DIR) -I$(AUDIO_DIR)/drivers/sb -I$(AUDIO_DIR)/drivers/wss -I$(AUDIO_DIR)/codec -I$(AUDIO_DIR)/drivers/awe -I$(AUDIO_DIR)/gus -I$(AUDIO_DIR)/math -I$(DVXTEST_ROOT)/src/libs/kpunch -DAUDIO_GOLDEN='"$(GOLDEN)"' -DAUDIO_WORKDIR='"$(WORKDIR)"' -I$(DVXTEST_BASICDIR) -DBASIC_INCLUDE_DIR='"$(DVXTEST_ROOT)/src/include/basic"'
# The fake drivers: name, priority, open / play succeed, rate, bits,
# channels, DAC, tone generator, clocked
-FAKE_DEFS = -DFAKE_CLOCK=0 -DFAKE_TONE=0 -DFAKE_DAC=1 -DFAKE_RATE=22050 -DFAKE_BITS=16 -DFAKE_CHANNELS=2 -DFAKE_OPEN_OK=1 -DFAKE_PLAY_OK=1
+FAKE_DEFS = -DFAKE_REC_SOURCES=0 -DFAKE_CLOCK=0 -DFAKE_TONE=0 -DFAKE_DAC=1 -DFAKE_RATE=22050 -DFAKE_BITS=16 -DFAKE_CHANNELS=2 -DFAKE_OPEN_OK=1 -DFAKE_PLAY_OK=1
FAKE_alpha = -DFAKE_NAME='"alpha"' -DFAKE_PRIORITY=50 -UFAKE_OPEN_OK -DFAKE_OPEN_OK=0
FAKE_beta = -DFAKE_NAME='"beta"' -DFAKE_PRIORITY=40 -UFAKE_PLAY_OK -DFAKE_PLAY_OK=0
-FAKE_gamma = -DFAKE_NAME='"gamma"' -DFAKE_PRIORITY=30
+FAKE_gamma = -DFAKE_NAME='"gamma"' -DFAKE_PRIORITY=30 -UFAKE_REC_SOURCES -DFAKE_REC_SOURCES=3
FAKE_delta = -DFAKE_NAME='"delta"' -DFAKE_PRIORITY=20 -UFAKE_DAC -DFAKE_DAC=0 -UFAKE_TONE -DFAKE_TONE=1 -UFAKE_CLOCK -DFAKE_CLOCK=1 -UFAKE_RATE -DFAKE_RATE=11025 -UFAKE_BITS -DFAKE_BITS=8 -UFAKE_CHANNELS -DFAKE_CHANNELS=1
FAKE_mono8 = -DFAKE_NAME='"mono8"' -DFAKE_PRIORITY=10 -UFAKE_BITS -DFAKE_BITS=8 -UFAKE_CHANNELS -DFAKE_CHANNELS=1
FAKE_badabi = -DFAKE_NAME='"badabi"' -DFAKE_PRIORITY=90 -DFAKE_BAD_ABI
FAKE_nodesc = -DFAKE_NO_DESC
FAKES = alpha beta gamma delta mono8 badabi nodesc
-FAKE_DRVS = $(patsubst %,$(DRVDIR)/%.drv,$(FAKES)) $(DRVDIR)/fmfake.drv $(DRVDIR)/midifake.drv
+FAKE_DRVS = $(patsubst %,$(DRVDIR)/%.drv,$(FAKES)) $(DRVDIR)/fmfake.drv $(DRVDIR)/midifake.drv $(DRVDIR)/wtfake.drv
.PHONY: all run clean
@@ -73,7 +73,8 @@ $(DRVDIR)/%.drv: fakeDrv.c $(AUDIO_DIR)/audioDrv.h $(DVX_MAKEFILES)
@mkdir -p $(DRVDIR)
$(HOSTCC) $(CFLAGS) -shared $(FAKE_DEFS) $(FAKE_$*) -o $@ fakeDrv.c
-# The fake synthesizers (fakeSynth.c): an FM chip and a MIDI port
+# The fake synthesizers (fakeSynth.c): an FM chip, a MIDI port, and a
+# wavetable synthesizer
$(DRVDIR)/fmfake.drv: fakeSynth.c $(AUDIO_DIR)/audioDrv.h $(DVX_MAKEFILES)
@mkdir -p $(DRVDIR)
$(HOSTCC) $(CFLAGS) -shared -DFAKE_SYN_FM -o $@ fakeSynth.c
@@ -82,6 +83,10 @@ $(DRVDIR)/midifake.drv: fakeSynth.c $(AUDIO_DIR)/audioDrv.h $(DVX_MAKEFILES)
@mkdir -p $(DRVDIR)
$(HOSTCC) $(CFLAGS) -shared -o $@ fakeSynth.c
+$(DRVDIR)/wtfake.drv: fakeSynth.c $(AUDIO_DIR)/audioDrv.h $(DVX_MAKEFILES)
+ @mkdir -p $(DRVDIR)
+ $(HOSTCC) $(CFLAGS) -shared -DFAKE_SYN_WT -o $@ fakeSynth.c
+
$(AUDIO_TEST): audioTest.c $(AUDIO_OBJS) $(DVX_MAKEFILES) $(LIBS_A) | dvxtest-libs
@mkdir -p $(BINDIR)
$(HOSTCC) $(CFLAGS) -o $@ $< $(AUDIO_OBJS) $(DVXTEST_BASRT_A) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm
diff --git a/src/test/audio/audioTest.c b/src/test/audio/audioTest.c
index e994115a..273a19df 100644
--- a/src/test/audio/audioTest.c
+++ b/src/test/audio/audioTest.c
@@ -73,6 +73,12 @@
#include "dvxPlatIsr.h"
#include "audioInt.h"
#include "sbCalc.h"
+#include "codecCalc.h"
+#include "wssCalc.h"
+#include "gusCalc.h"
+#include "aweCalc.h"
+#include "drvMath.h"
+#include "isa/isaCalc.h"
#include "stb_ds.h"
#include "compiler/compiler.h"
#include "runtime/vm.h"
@@ -94,6 +100,8 @@
#define LINE_MAX_LEN 128
#define WAV_MAX 4096
#define PCM_FRAMES 64
+#define PAUSE_FRAMES 8 // a voice paused this far into the ramp
+#define MIX_HELD_FRAMES 2 // a voice's current and next frames (read ahead to interpolate)
#define FNV_OFFSET 0xcbf29ce484222325ULL
#define FNV_PRIME 0x100000001b3ULL
#define RATE_DEV 22050
@@ -117,6 +125,11 @@
#define FM_OPL3_VOICES 18
#define FM_TONE_FRAMES 256
#define MIDI_BYTES_MAX 64
+#define MIDI_IN_WAIT_MS 5 // the input timer's calls (1 ms each) to read what came
+#define WT_FAKE_MEMORY (1024 * 1024)
+#define WT_FAKE_VOICES_MAX 32
+#define WT_ALIGN 32
+#define WT_BANK 0x40000
#define NOTE_US 10000 // 220.5 frames at 22,050 Hz
#define NOTE_FRAMES 220 // the first note's frames (the half is carried)
#define NOTE_MHZ_LOW 1000000 // 1000 Hz
@@ -127,6 +140,28 @@
#define NOTE_SQ_FLIP 12 // a 1000 Hz square is negative by this frame
#define DESK_PCM_FRAMES 1103 // 0.1 s at 11,025 Hz
#define DESK_PCM_LEVEL 200
+#define REC_BLOCK_FRAMES 64 // one block "the card recorded"
+#define REC_LEVEL 16000 // the block's samples: +/- this
+#define REC_RATE_OFF 5 // fakeDrv FAKE_REC_RATE_OFF: the card's rate is this far above
+#define REC_RATE 11025
+#define REC_BAD_BITS 12
+#define REC_FILL_BLOCKS 1100 // more than the default buffer holds
+#define REC_FILE_BLOCKS 3
+#define REC_FILE "rec.wav"
+#define REC_BAD_FILE "nodir" DVX_PATH_SEP "rec.wav"
+#define REC_U8_LOW 64
+#define REC_U8_HIGH 192
+#define REC_HEAD_DATA 1000
+#define REC_HEAD_RATE 22050
+#define WAV_HEAD_RIFF_SIZE 4
+#define WAV_HEAD_WAVE 8 // "WAVEfmt "
+#define WAV_HEAD_WAVE_LEN 8
+#define WAV_HEAD_DATA_TAG 36 // "data"
+#define WAV_FOURCC_LEN 4
+#define WAV_HEAD_ALIGN 32
+#define WAV_HEAD_BITS 34
+#define WAV_HEAD_DATA_SIZE 40
+#define WAV_HEAD_RIFF_EXTRA 36
#define DESK_WAV_BYTES 2048
#define DESK_WAIT_MIN_US 90000
#define DESK_WAIT_MAX_US 1000000
@@ -137,6 +172,7 @@
#define DRIVERS_FAKE 5 // the fake drivers that load (alpha .. mono8)
#define MIDI_FM_INDEX 2
#define MIDI_MPU_INDEX 1
+#define MODS_SOFT_INDEX 1
#define PERCENT_FULL 100
#define PERCENT_HALF 50
#define PERCENT_INI 40
@@ -149,6 +185,7 @@
#define BAS_STREAM_FRAMES 100
#define BAS_RENDER_FRAMES 32
#define BAS_PITCH_DOUBLE 200
+#define BAS_WT_FRAMES 4 // WtUpload's samples in the BASIC test
#define BAS_FM_REG 0xB0 // FM_KEY_BLOCK
#define BAS_FM_RAW_REG 0xB3
#define BAS_MIDI_NOTE_ON 0x90
@@ -171,6 +208,8 @@ typedef struct {
} GoldenT;
static char sEvents[EVENT_MAX][EVENT_TEXT_MAX];
+static int32_t sSynthOpens = 0;
+static int32_t sSynthCloses = 0;
static int32_t sEventCount = 0;
static uint32_t sToneHz = 0;
static int32_t sToneCalls = 0;
@@ -188,21 +227,35 @@ static int16_t sFillValue = 0;
static uint8_t sFmRegs[FM_REGS];
static uint32_t sFmWrites = 0;
static uint8_t sMidiBytes[MIDI_BYTES_MAX];
+static uint8_t sMidiIn[MIDI_BYTES_MAX]; // what the fake port's input brings next
+static int32_t sMidiInHead = 0;
+static int32_t sMidiInTail = 0;
static int32_t sMidiCount = 0;
static bool sMidiReady = true;
+static uint8_t sWtMemory[WT_FAKE_MEMORY]; // the fake wavetable synthesizer's memory
+static AudioWtVoiceT sWtVoice[WT_FAKE_VOICES_MAX]; // the last note started or updated on each voice
+static uint32_t sWtStarts;
+static uint32_t sWtUpdates;
+static uint32_t sWtWhat; // the last update's AUDIO_WT_SET_* bits
+static uint32_t sWtReleased; // a bit per voice
+static uint32_t sWtStopped;
+static uint32_t sWtActive; // a bit per voice: still sounding
static char sCapture[BAS_CAPTURE_MAX];
// ============================================================
// Prototypes
// ============================================================
+static void aweTests(void);
static void basicLiveTests(void);
+static void basicRecordTests(void);
static void basicSampleTests(void);
static void basicStreamTests(void);
static void basicSynthTests(void);
static void basicTests(void);
static void captureOut(void *ctx, const char *text, bool newline);
static void checkGolden(const char *name, uint64_t hash);
+static void codecTests(void);
static void countCall(void *userData);
static void deskTests(void);
static void driverTests(void);
@@ -210,13 +263,22 @@ static void eventsClear(void);
static bool eventSeen(const char *text);
void fakeDrvEvent(const char *driver, const char *event);
void fakeDrvTone(uint32_t hz);
+void fakeSynthEvent(const char *synth, const char *event);
+bool fakeSynthGet(uint8_t *byte);
bool fakeSynthPut(uint8_t byte);
void fakeSynthWrite(uint32_t reg, uint8_t value);
+bool fakeWtActive(uint32_t voice);
+void fakeWtRelease(uint32_t voice);
+void fakeWtStart(uint32_t voice, const AudioWtVoiceT *v);
+void fakeWtStop(uint32_t voice);
+void fakeWtUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what);
+bool fakeWtUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
static void fileTests(void);
static uint32_t fillRamp(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
static void fmRawTests(void);
static void fmTests(void);
static void fmToneTests(void);
+static void gusTests(void);
static uint64_t hashOut(uint32_t frames, uint32_t frameBytes);
static void hookA(void);
static void hookB(void);
@@ -224,6 +286,8 @@ static void idleTests(void);
static void loadGoldens(void);
static uint32_t makeVoc(uint8_t *buf, uint16_t checksumDelta, const uint8_t *blocks, uint32_t blockBytes);
static uint32_t makeWav(uint8_t *buf, uint32_t tag, uint32_t channels, uint32_t rate, uint32_t bits, uint32_t align, const void *data, uint32_t dataBytes, bool listFirst);
+static void midiIn(const uint8_t *bytes, int32_t count);
+static void midiInTests(void);
static void midiTests(void);
static void mixerTests(void);
static void notesTests(void);
@@ -233,20 +297,80 @@ static void platformTests(void);
static void programTests(void);
static void put16(uint8_t *p, uint32_t v);
static void put32(uint8_t *p, uint32_t v);
+static void recordTests(void);
static void render(uint32_t frames);
static void saveGoldens(void);
static void sbTests(void);
static void setIni(const char *extra);
static void settingsBasicTests(void);
static void settingsTests(void);
+static void suspendTests(void);
static void toneTests(void);
static void voiceTests(void);
+static void wssTests(void);
+static void wtTests(void);
// prefs.bas (libdvx dvxPrefsBas.c)
void PrefsClose(int32_t handle);
int32_t PrefsLoad(const char *path);
+// The EMU8000's units (aweCalc.c) and the drivers' 2^x (drvMath.c).
+static void aweTests(void) {
+ tCase("awe: 2^x in 16.16");
+ TEST_EQ(drvPow2(0), 65536);
+ TEST_EQ(drvPow2(1200), 131072);
+ TEST_EQ(drvPow2(-1200), 32768);
+ TEST_EQ(drvPow2(100), 69433);
+ TEST_EQ(drvPow2(50), (65536 + 69433) / 2);
+ TEST_EQ(drvPow2(-2400), 16384);
+
+ tCase("awe: pitch, loudness, filter, pan, sends");
+ TEST_EQ(awePitch(0), AWE_IP_UNITY);
+ TEST_EQ(awePitch(1200), 0xF000);
+ TEST_EQ(awePitch(-1200), 0xD000);
+ TEST_EQ(awePitch(12000), 0xFFFF);
+ TEST_EQ(aweAtten(0), 0);
+ TEST_EQ(aweAtten(375), 100);
+ TEST_EQ(aweAtten(2000), 0xFF);
+ TEST_EQ(aweCutoff(4721), 0);
+ TEST_EQ(aweCutoff(4721 + 2250), 80);
+ TEST_EQ(aweCutoff(13500), 0xFF);
+ TEST_EQ(aweQ(96), 8);
+ TEST_EQ(aweQ(500), 15);
+ TEST_EQ(awePan(-500), 0xFF);
+ TEST_EQ(awePan(500), 0);
+ TEST_EQ(awePan(0), 127);
+ TEST_EQ(aweSend(1000), 0xFF);
+ TEST_EQ(aweSend(500), 127);
+
+ tCase("awe: envelope and LFO times");
+ TEST_EQ(aweDelay(-12000), AWE_DELAY_NONE);
+ TEST_EQ(aweDelay(0), AWE_DELAY_NONE - 1000 * 40 / 29);
+
+ // The guide's example: a 100 ms release is decay code 5Ch
+ TEST_EQ(aweDecay(-3986), 0x5C);
+ TEST_EQ(aweDecay(2400), 11);
+ TEST_EQ(aweAttack(-3986), 61);
+ TEST_EQ(aweAttack(0), 11);
+ TEST_EQ(aweAttack(-12000), 127);
+ TEST_EQ(aweHold(-12000), 0x7F);
+ TEST_EQ(aweHold(0), 0x7F - 1000 / 92);
+ TEST_EQ(aweVolSustain(0), 0x7F);
+ TEST_EQ(aweVolSustain(960), 0);
+ TEST_EQ(aweModSustain(1000), 0);
+ TEST_EQ(aweLfoFreq(0), 194);
+ TEST_EQ(aweLfoFreq(-1200), 97);
+ TEST_EQ(aweLfoFreq(2000), 0xFF);
+
+ tCase("awe: signed depths");
+ TEST_EQ(aweDepth(600, 75, 8), 64);
+ TEST_EQ(aweDepth(1200, 75, 8), 127);
+ TEST_EQ(aweDepth(-1200, 75, 8), 0x80);
+ TEST_EQ(aweDepth(-75, 75, 8), 0xF8);
+}
+
+
// sound.bas and music.bas compiled with a program whose DECLARE LIBRARY
// calls reach the library for real: BINARY STRING data with zero bytes
// arrives whole (its length shows in SoundLength and the bytes a stream
@@ -320,6 +444,64 @@ static void basicLiveTests(void) {
}
+// sound.bas: recording into a file (the gamma fake records; the test
+// pushes the blocks), the format, the level and length, one recording
+// at a time, and a program's recording that is not another's.
+static void basicRecordTests(void) {
+ int16_t block[REC_BLOCK_FRAMES];
+
+ for (int32_t i = 0; i < REC_BLOCK_FRAMES; i++) {
+ block[i] = (int16_t)((i & 1) ? REC_LEVEL : -REC_LEVEL);
+ }
+
+ tCase("basic: RecordStart / RecordStop into a WAV file");
+ setIni("driver = gamma\n");
+ sAppId = APP_A;
+ TEST_EQ(SoundCaps("record"), BAS_TRUE);
+ TEST_EQ(SoundCaps("RecordSources"), (1 << AUDIO_REC_MIC) | (1 << AUDIO_REC_LINE));
+ TEST_EQ(Recording(), BAS_FALSE);
+ TEST_EQ(RecordStop(), BAS_FALSE);
+ TEST_EQ(RecordStart(REC_FILE, -1, 0, 0, AUDIO_REC_MIC), BAS_FALSE);
+ TEST_STR_EQ(SoundError(), "Invalid argument");
+ TEST_EQ(RecordStart(REC_FILE, 0, 0, 0, AUDIO_REC_CD), BAS_FALSE);
+ TEST_STR_EQ(SoundError(), "The sound device cannot record from that input");
+ TEST_EQ(RecordStart(REC_FILE, 0, 0, 0, AUDIO_REC_MIC), BAS_TRUE);
+ TEST_EQ(Recording(), BAS_TRUE);
+ TEST_EQ(RecordRate(), AUDIO_RATE_DEFAULT + REC_RATE_OFF);
+ TEST_EQ(RecordBits(), AUDIO_REC_BITS_DEFAULT);
+ TEST_EQ(RecordChannels(), 1);
+ TEST_EQ(RecordStart(REC_FILE, 0, 0, 0, AUDIO_REC_MIC), BAS_FALSE);
+ audioRecPush(block, sizeof(block));
+ audioPump();
+ TEST_EQ(RecordLevel(), REC_LEVEL * PERCENT_FULL / INT16_MAX);
+ TEST_EQ(RecordLength(), REC_BLOCK_FRAMES * MS_PER_SEC / (AUDIO_RATE_DEFAULT + REC_RATE_OFF));
+ TEST_EQ(RecordLost(), 0);
+
+ // Another program sees no recording of its own.
+ sAppId = APP_B;
+ TEST_EQ(Recording(), BAS_FALSE);
+ TEST_EQ(RecordStop(), BAS_FALSE);
+ sAppId = APP_A;
+ TEST_EQ(RecordStop(), BAS_TRUE);
+ TEST_EQ(Recording(), BAS_FALSE);
+ TEST_EQ(RecordRate(), 0);
+
+ int32_t size = 0;
+ char *wav = platformReadFile(REC_FILE, &size);
+
+ TEST_EQ(size, AUDIO_WAV_HEADER_BYTES + (int32_t)sizeof(block));
+ free(wav);
+
+ tCase("basic: a program's end closes its recording");
+ TEST_EQ(RecordStart(REC_FILE, 0, 0, 0, AUDIO_REC_LINE), BAS_TRUE);
+ eventsClear();
+ audioAppEnded(APP_A);
+ TEST_TRUE(eventSeen("gamma recordstop"));
+ TEST_EQ(Recording(), BAS_FALSE);
+ audioAppEnded(APP_B);
+}
+
+
// sound.bas: loaded and created sounds, voices changed while playing,
// device facts.
static void basicSampleTests(void) {
@@ -358,6 +540,8 @@ static void basicSampleTests(void) {
TEST_STR_EQ(SoundError(), "Invalid argument");
SoundSetFrequency(tri, -1);
TEST_STR_EQ(SoundError(), "Invalid argument");
+ SoundPause(tri);
+ TEST_STR_EQ(SoundError(), "Invalid argument");
StopWave(tri);
SoundSetVolume(tri, PERCENT_HALF);
TEST_STR_EQ(SoundError(), audioErrorText(AUDIO_ERR_VOICE));
@@ -400,6 +584,14 @@ static void basicSampleTests(void) {
TEST_EQ(audioMixer.voices[(v - 1) % 256].step, MIX_FRAC_ONE * 2);
SoundSetPitch(v, PERCENT_FULL * PERCENT_FULL);
TEST_EQ(audioMixer.voices[(v - 1) % 256].step, MIX_FRAC_ONE * AUDIO_PITCH_MAX / AUDIO_PITCH_NORMAL);
+ SoundPause(v);
+ TEST_STR_EQ(SoundError(), "No error");
+ render(BAS_RENDER_FRAMES);
+ TEST_EQ(outPeak(BAS_RENDER_FRAMES), 0);
+ TEST_EQ(SoundPlaying(v), BAS_TRUE);
+ SoundResume(v);
+ render(BAS_RENDER_FRAMES);
+ TEST_TRUE(outPeak(BAS_RENDER_FRAMES) > 0);
SoundSetVolume(v, 0);
render(BAS_RENDER_FRAMES);
TEST_EQ(outPeak(BAS_RENDER_FRAMES), 0);
@@ -521,9 +713,15 @@ static void basicStreamTests(void) {
sAppId = APP_B;
TEST_EQ(SoundStreamQueued(h), 0);
TEST_EQ(SoundStreamVoice(h), 0);
+ SoundStreamFlush(h);
SoundStreamClose(h);
sAppId = APP_A;
TEST_TRUE(SoundStreamQueued(h) > 0);
+
+ // A flush drops what is queued; the stream stays open.
+ SoundStreamFlush(h);
+ TEST_EQ(SoundStreamQueued(h), 0);
+ TEST_TRUE(SoundStreamSpace(h) > 0);
SoundStreamClose(h);
TEST_EQ(SoundStreamQueued(h), 0);
TEST_EQ(SoundStreamWrite(h, (const uint8_t *)pcm, (int32_t)sizeof(pcm)), 0);
@@ -589,6 +787,93 @@ static void basicSynthTests(void) {
audioAppEnded(APP_A);
audioAppEnded(APP_B);
+ tCase("basic: wavetable voices and memory");
+ setIni("driver = gamma\nfakeWtVoices = 4\n");
+ sAppId = APP_A;
+ TEST_STR_EQ(WtInfo("description"), "Fake wavetable (4 voices)");
+ TEST_STR_EQ(WtInfo("Voices"), "4");
+ TEST_STR_EQ(WtInfo("free"), "4");
+ snprintf(text, sizeof(text), "%d", WT_FAKE_MEMORY);
+ TEST_STR_EQ(WtInfo("memory"), text);
+ TEST_STR_EQ(WtInfo("freememory"), text);
+ TEST_STR_EQ(WtInfo("level"), "0");
+ TEST_STR_EQ(WtInfo("envelopes"), "-1");
+ TEST_STR_EQ(WtInfo("8bit"), "0");
+ TEST_STR_EQ(WtInfo("rom"), "0");
+ TEST_STR_EQ(WtInfo("nonsense"), "");
+
+ static const int16_t wtPcm[BAS_WT_FRAMES] = { 0x0102, 0x0304, -2, 0x7F00 };
+ int32_t wtAddr = WtMemAlloc(BAS_WT_FRAMES * 2, 16);
+
+ TEST_EQ(wtAddr, 0);
+ TEST_EQ(WtMemAlloc(0, 16), -1);
+ TEST_STR_EQ(SoundError(), "Invalid argument");
+ TEST_EQ(WtUpload(wtAddr, (const uint8_t *)wtPcm, (int32_t)sizeof(wtPcm), 16), BAS_TRUE);
+ TEST_EQ(sWtMemory[1], 0x01);
+ TEST_EQ(WtUpload(wtAddr, (const uint8_t *)wtPcm, 1, 16), 0);
+
+ int32_t wv = WtAlloc();
+
+ TEST_TRUE(wv > 0);
+ TEST_STR_EQ(WtInfo("free"), "3");
+ TEST_EQ(WtStart(wv, wtAddr, 2, BAS_WT_FRAMES, 1, BAS_WT_FRAMES, AUDIO_WT_LOOP_ON, 16, 1200, 60, 100), BAS_TRUE);
+ TEST_EQ(sWtVoice[0].pitch, 1200);
+ TEST_EQ(sWtVoice[0].attenuation, 60);
+ TEST_EQ(sWtVoice[0].pan, AUDIO_WT_PAN_MAX);
+ TEST_EQ(sWtVoice[0].loop, AUDIO_WT_LOOP_ON);
+ TEST_EQ(sWtVoice[0].offset, 2);
+ TEST_EQ(WtStart(wv, wtAddr, BAS_WT_FRAMES + 1, BAS_WT_FRAMES, 0, 0, 0, 16, 0, 0, 0), 0);
+ TEST_EQ(sWtVoice[0].volEnv[AUDIO_WT_ENV_SUSTAIN], 0);
+ TEST_EQ(WtActive(wv), BAS_TRUE);
+ TEST_EQ(WtStart(wv, wtAddr, 0, BAS_WT_FRAMES, 3, 2, AUDIO_WT_LOOP_ON, 16, 0, 0, 0), 0);
+ TEST_EQ(WtStart(wv, wtAddr, 0, BAS_WT_FRAMES, 0, 0, 2, 16, 0, 0, 0), 0);
+ TEST_EQ(WtStart(wv, wtAddr, 0, BAS_WT_FRAMES, 0, 0, 0, 12, 0, 0, 0), 0);
+ TEST_EQ(WtSetPitch(wv, 1300), BAS_TRUE);
+ TEST_EQ(sWtVoice[0].pitch, 1300);
+ TEST_EQ(sWtWhat, AUDIO_WT_SET_PITCH);
+ TEST_EQ(WtSetAttenuation(wv, AUDIO_WT_ATTEN_MAX + 5), BAS_TRUE);
+ TEST_EQ(sWtVoice[0].attenuation, AUDIO_WT_ATTEN_MAX);
+ TEST_EQ(WtSetPan(wv, -100), BAS_TRUE);
+ TEST_EQ(sWtVoice[0].pan, -AUDIO_WT_PAN_MAX);
+ TEST_EQ(sWtVoice[0].pitch, 1300);
+ TEST_EQ(WtSetPitch(wv + 1, 0), 0);
+ sWtReleased = 0;
+ WtRelease(wv);
+ TEST_EQ(sWtReleased, 1u);
+ WtStop(wv);
+ TEST_EQ(WtActive(wv), 0);
+
+ // Another program cannot touch it
+ sAppId = APP_B;
+ TEST_EQ(WtSetPitch(wv, 0), 0);
+ WtFree(wv);
+ TEST_TRUE(strcmp(SoundError(), "No error") != 0);
+ sAppId = APP_A;
+ WtFree(wv);
+ TEST_STR_EQ(SoundError(), "No error");
+ TEST_STR_EQ(WtInfo("free"), "4");
+ TEST_EQ(WtSetPitch(wv, 0), 0);
+ WtMemFree(wtAddr);
+ TEST_STR_EQ(SoundError(), "No error");
+ WtMemFree(wtAddr);
+ TEST_TRUE(strcmp(SoundError(), "No error") != 0);
+
+ // A program's voices go back with its context (asked without opening
+ // one: the next case needs the device closed)
+ AudioWtInfoT wtInfo;
+
+ wv = WtAlloc();
+ TEST_EQ(WtStart(wv, 0, 0, BAS_WT_FRAMES, 0, 0, AUDIO_WT_LOOP_NONE, 16, 0, 0, 0), BAS_TRUE);
+ sAppId = APP_B;
+ TEST_EQ(WtAlloc() > 0, true);
+ audioWtGetInfo(&wtInfo);
+ TEST_EQ(wtInfo.freeVoices, 2);
+ audioAppEnded(APP_B);
+ audioWtGetInfo(&wtInfo);
+ TEST_EQ(wtInfo.freeVoices, 3);
+ audioAppEnded(APP_A);
+ sAppId = APP_A;
+
tCase("basic: the raw MIDI port (music.bas)");
setIni("driver = gamma\nmidi = mpu401\n");
TEST_STR_EQ(MidiPortInfo("description"), "Fake MIDI port");
@@ -697,6 +982,35 @@ static void checkGolden(const char *name, uint64_t hash) {
}
+// The AD1848 codec's rates and format byte (codecCalc.c), shared by the
+// Windows Sound System board and the UltraSound MAX.
+static void codecTests(void) {
+ uint32_t rate = 0;
+
+ tCase("codec: the rates");
+ TEST_EQ(codecRateCode(22050, &rate), 0x07);
+ TEST_EQ(rate, 22050);
+ TEST_EQ(codecRateCode(44100, &rate), 0x0B);
+ TEST_EQ(codecRateCode(48000, &rate), 0x0C);
+ TEST_EQ(codecRateCode(8000, &rate), 0x00);
+ TEST_EQ(codecRateCode(27000, &rate), 0x04);
+ TEST_EQ(rate, 27429);
+
+ // Nearest: 20,000 is 18,900 (1,100 off, not 2,050); out of range
+ // takes an end
+ codecRateCode(20000, &rate);
+ TEST_EQ(rate, 18900);
+ TEST_EQ(codecRateCode(1, &rate), 0x01);
+ TEST_EQ(rate, 5513);
+ codecRateCode(100000, &rate);
+ TEST_EQ(rate, 48000);
+
+ tCase("codec: the data format");
+ TEST_EQ(codecFormat(16, 2), CODEC_FMT_16BIT | CODEC_FMT_STEREO);
+ TEST_EQ(codecFormat(8, 1), CODEC_FMT_8BIT);
+}
+
+
static void countCall(void *userData) {
(*(uint32_t *)userData)++;
}
@@ -910,6 +1224,29 @@ void fakeDrvTone(uint32_t hz) {
}
+// The fake synthesizers' opens and closes (fakeSynth.c), counted.
+void fakeSynthEvent(const char *synth, const char *event) {
+ (void)synth;
+
+ if (strcmp(event, "open") == 0) {
+ sSynthOpens++;
+ } else {
+ sSynthCloses++;
+ }
+}
+
+
+// The fake MIDI port's input: the bytes midiIn queued.
+bool fakeSynthGet(uint8_t *byte) {
+ if (sMidiInTail == sMidiInHead) {
+ return false;
+ }
+
+ *byte = sMidiIn[sMidiInTail++];
+ return true;
+}
+
+
bool fakeSynthPut(uint8_t byte) {
if (!sMidiReady || sMidiCount >= MIDI_BYTES_MAX) {
return false;
@@ -928,6 +1265,61 @@ void fakeSynthWrite(uint32_t reg, uint8_t value) {
}
+bool fakeWtActive(uint32_t voice) {
+ return (sWtActive >> voice) & 1;
+}
+
+
+void fakeWtRelease(uint32_t voice) {
+ sWtReleased |= 1u << voice;
+}
+
+
+void fakeWtStart(uint32_t voice, const AudioWtVoiceT *v) {
+ sWtVoice[voice] = *v;
+ sWtActive |= 1u << voice;
+ sWtStarts++;
+}
+
+
+void fakeWtStop(uint32_t voice) {
+ sWtStopped |= 1u << voice;
+ sWtActive &= ~(1u << voice);
+}
+
+
+void fakeWtUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ sWtVoice[voice] = *v;
+ sWtWhat = what;
+ sWtUpdates++;
+}
+
+
+// Frames into the fake memory as 8-bit (signed, the top byte) or 16-bit
+// little-endian samples.
+bool fakeWtUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits) {
+ uint32_t bytes = bits / 8;
+
+ if (addr > WT_FAKE_MEMORY || count > (WT_FAKE_MEMORY - addr) / bytes) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ uint16_t v = (uint16_t)frames[i];
+
+ if (bits == 8) {
+ sWtMemory[addr + i] = (uint8_t)(v >> 8);
+ } else {
+ sWtMemory[addr + i * 2] = (uint8_t)v;
+ sWtMemory[addr + i * 2 + 1] = (uint8_t)(v >> 8);
+ }
+ }
+
+ return true;
+}
+
+
+
static void fileTests(void) {
static uint8_t wav[WAV_MAX];
uint8_t pcm8[PCM_FRAMES];
@@ -1390,6 +1782,109 @@ static void fmToneTests(void) {
}
+// The Gravis UltraSound's arithmetic (gusCalc.c): ULTRASND, the latch
+// codes, rates and frequency controls, addresses, volumes.
+static void gusTests(void) {
+ GusEnvT env;
+
+ tCase("gus: the ULTRASND variable");
+ TEST_TRUE(gusParseEnv("220,1,1,11,5", &env));
+ TEST_EQ(env.base, 0x220);
+ TEST_EQ(env.dma1, 1);
+ TEST_EQ(env.dma2, 1);
+ TEST_EQ(env.irq1, 11);
+ TEST_EQ(env.irq2, 5);
+ TEST_TRUE(gusParseEnv("240, 5, 6, 12, 7", &env));
+ TEST_EQ(env.base, 0x240);
+ TEST_EQ(env.dma2, 6);
+ TEST_EQ(env.irq2, 7);
+ TEST_TRUE(gusParseEnv("260", &env));
+ TEST_EQ(env.dma1, GUS_NONE);
+ TEST_TRUE(!gusParseEnv(NULL, &env));
+ TEST_TRUE(!gusParseEnv("", &env));
+ TEST_TRUE(!gusParseEnv(",1,1", &env));
+
+ tCase("gus: latch codes");
+ TEST_EQ(gusIrqCode(2), 1);
+ TEST_EQ(gusIrqCode(9), 1);
+ TEST_EQ(gusIrqCode(5), 2);
+ TEST_EQ(gusIrqCode(11), 5);
+ TEST_EQ(gusIrqCode(15), 7);
+ TEST_EQ(gusIrqCode(10), 0);
+ TEST_EQ(gusDmaCode(1), 1);
+ TEST_EQ(gusDmaCode(3), 2);
+ TEST_EQ(gusDmaCode(7), 5);
+ TEST_EQ(gusDmaCode(0), 0);
+
+ // Both channels latched: ULTRASND's second in bits 5:3, or recording
+ // put on the first (40h) when the second is the same or unusable; the
+ // channel recording uses
+ tCase("gus: the DMA latch and the record channel");
+ TEST_EQ(gusDmaLatch(1, 1), 0x01 | GUS_DMA_LATCH_COMBINE);
+ TEST_EQ(gusDmaLatch(1, 3), 0x01 | 0x02 << 3);
+ TEST_EQ(gusDmaLatch(5, 6), 0x03 | 0x04 << 3);
+ TEST_EQ(gusDmaLatch(1, GUS_NONE), 0x01 | GUS_DMA_LATCH_COMBINE);
+ TEST_EQ(gusDmaLatch(1, 0), 0x01 | GUS_DMA_LATCH_COMBINE);
+ TEST_EQ(gusRecordDma(1, 3), 3);
+ TEST_EQ(gusRecordDma(1, GUS_NONE), 1);
+ TEST_EQ(gusRecordDma(5, 2), 5);
+
+ // Sampling at 617,400 / (value + 2) Hz, clamped to 5,510..44,100
+ tCase("gus: the sampling rate");
+ uint32_t rate = 0;
+
+ TEST_EQ(gusSampleRate(22050, &rate), 26);
+ TEST_EQ(rate, 22050);
+ TEST_EQ(gusSampleRate(44100, &rate), 12);
+ TEST_EQ(rate, 44100);
+ TEST_EQ(gusSampleRate(96000, &rate), 12);
+ TEST_EQ(rate, GUS_SAMPLE_RATE_MAX);
+ TEST_EQ(gusSampleRate(8000, &rate), 75);
+ TEST_EQ(rate, 8018);
+ TEST_EQ(gusSampleRate(1, &rate), 110);
+ TEST_EQ(rate, 5513);
+
+ // A MAX reads version 10 or 11 (an ICS board 5..9, the ACE 30h, the
+ // Extreme 50h, an older board FFh); its control register: the base's
+ // bits 7-4 (the codec at base + 10Ch), the 16-bit channels, the codec
+ // on (40h)
+ tCase("gus: the MAX");
+ TEST_TRUE(gusIsMax(10));
+ TEST_TRUE(gusIsMax(11));
+ TEST_TRUE(!gusIsMax(9));
+ TEST_TRUE(!gusIsMax(0x30));
+ TEST_TRUE(!gusIsMax(0x50));
+ TEST_TRUE(!gusIsMax(0xFF));
+ TEST_EQ(gusMaxControl(0x220, 1, 1), 0x42);
+ TEST_EQ(gusMaxControl(0x240, 5, 6), 0x74);
+ TEST_EQ(gusMaxControl(0x260, 3, GUS_NONE), 0x46);
+
+ tCase("gus: output rates and frequency controls");
+ TEST_EQ(gusOutputRate(14), 44100);
+ TEST_EQ(gusOutputRate(28), 22050);
+ TEST_EQ(gusOutputRate(32), 19293);
+ TEST_EQ(gusOutputRate(2), 44100);
+ TEST_EQ(gusOutputRate(40), 19293);
+ TEST_EQ(gusFc(22050, 22050), 0x0400);
+ TEST_EQ(gusFc(44100, 22050), 0x0800);
+ TEST_EQ(gusFc(11025, 22050), 0x0200);
+
+ tCase("gus: addresses");
+ TEST_EQ(gusAddrHigh(0x12345), 0x0246);
+ TEST_EQ(gusAddrLow(0x12345), 0x45 << 9);
+ TEST_EQ(gusAddr16(0x00002), 0x00001);
+ TEST_EQ(gusAddr16(0x40000), 0x40000);
+ TEST_EQ(gusAddr16(0x7FFFE), 0x5FFFF);
+
+ tCase("gus: volumes");
+ TEST_EQ(gusVolume(0), 0);
+ TEST_EQ(gusVolume(65535), GUS_VOLUME_MAX);
+ TEST_EQ(gusVolume(32768), 0x0F00);
+ TEST_EQ(gusVolume(1), 0x0000);
+ TEST_EQ(gusVolume(3), 0x0180);
+}
+
+
// FNV-1a over the rendered bytes.
static uint64_t hashOut(uint32_t frames, uint32_t frameBytes) {
const uint8_t *p = (const uint8_t *)sOut;
@@ -1552,6 +2047,106 @@ static uint32_t makeWav(uint8_t *buf, uint32_t tag, uint32_t channels, uint32_t
}
+// Bytes for the fake port's input (from the start of the queue).
+static void midiIn(const uint8_t *bytes, int32_t count) {
+ sMidiInHead = 0;
+ sMidiInTail = 0;
+
+ for (int32_t i = 0; i < count && i < MIDI_BYTES_MAX; i++) {
+ sMidiIn[sMidiInHead++] = bytes[i];
+ }
+}
+
+
+// MIDI input through the fake port (midiIn = mpu401): one program reads
+// it; the timer takes the bytes with their times (and keeps the clock
+// they are on); messages come whole, running status filled in, clock
+// bytes dropped, system exclusive skipped; closing the context closes
+// it.
+static void midiInTests(void) {
+ static const uint8_t played[] = {
+ 0x90, 0x3C, 0x64, // note on, middle C
+ 0xF8, // clock: dropped
+ 0x40, 0x50, // running status: another note on
+ 0xF0, 0x7E, 0x01, 0xF7, // system exclusive: skipped
+ 0xC2, 0x05, // program change, one data byte
+ 0x45 // a stray data byte (running status C2h: another program change)
+ };
+ AudioMidiInfoT info;
+ AudioMidiMsgT msg;
+
+ tCase("midi in: none unless DVX.INI asks for it");
+ setIni("driver = gamma\nmidi = mpu401\n");
+
+ AudioCtxT *a = audioOpen();
+
+ audioMidiGetInfo(&info);
+ TEST_TRUE(info.present && !info.inPresent);
+ TEST_EQ(audioMidiInOpen(a), AUDIO_ERR_NO_SYNTH);
+ TEST_TRUE(!audioMidiInRead(a, &msg));
+ audioClose(a);
+
+ tCase("midi in: one program reads it; whole messages with their times");
+ setIni("driver = gamma\nmidiIn = mpu401\n");
+ a = audioOpen();
+
+ AudioCtxT *b = audioOpen();
+
+ audioMidiGetInfo(&info);
+ TEST_TRUE(info.inPresent && !info.inOwned);
+ TEST_EQ(audioMidiInOpen(a), AUDIO_OK);
+ TEST_EQ(audioMidiInOpen(a), AUDIO_OK);
+ TEST_EQ(audioMidiInOpen(b), AUDIO_ERR_BUSY);
+ audioMidiGetInfo(&info);
+ TEST_TRUE(info.inOwned);
+
+ midiIn(played, (int32_t)sizeof(played));
+ hostClockAdvance(MIDI_IN_WAIT_MS);
+ TEST_TRUE(audioMidiInRead(a, &msg));
+ TEST_EQ(msg.status, 0x90);
+ TEST_EQ(msg.data1, 0x3C);
+ TEST_EQ(msg.data2, 0x64);
+ TEST_EQ(msg.length, 3);
+ TEST_TRUE(msg.ms >= 1 && msg.ms <= MIDI_IN_WAIT_MS);
+ TEST_TRUE(audioMidiInRead(a, &msg));
+ TEST_EQ(msg.status, 0x90);
+ TEST_EQ(msg.data1, 0x40);
+ TEST_EQ(msg.data2, 0x50);
+ TEST_TRUE(audioMidiInRead(a, &msg));
+ TEST_EQ(msg.status, 0xC2);
+ TEST_EQ(msg.data1, 0x05);
+ TEST_EQ(msg.length, 2);
+ TEST_TRUE(audioMidiInRead(a, &msg));
+ TEST_EQ(msg.status, 0xC2);
+ TEST_EQ(msg.data1, 0x45);
+ TEST_TRUE(!audioMidiInRead(a, &msg));
+ TEST_TRUE(!audioMidiInRead(b, &msg));
+ TEST_EQ(audioMidiInLost(a), 0);
+
+ // The clock the times are on runs on; another program has none
+ uint32_t clock = audioMidiInClock(a);
+
+ TEST_TRUE(clock >= msg.ms && clock <= MIDI_IN_WAIT_MS);
+ hostClockAdvance(MIDI_IN_WAIT_MS);
+ TEST_TRUE(audioMidiInClock(a) > clock);
+ TEST_EQ(audioMidiInClock(b), 0);
+ TEST_EQ(audioMidiInClock(NULL), 0);
+
+ tCase("midi in: closing the reader lets another read");
+ audioClose(a);
+ audioMidiGetInfo(&info);
+ TEST_TRUE(!info.inOwned);
+ TEST_EQ(audioMidiInOpen(b), AUDIO_OK);
+ audioMidiInClose(b);
+ TEST_EQ(audioMidiInOpen(b), AUDIO_OK);
+ audioSuspend(true);
+ audioSuspend(false);
+ audioMidiGetInfo(&info);
+ TEST_TRUE(!info.inOwned);
+ audioClose(b);
+}
+
+
static void midiTests(void) {
AudioMidiInfoT info;
@@ -1777,6 +2372,46 @@ static void mixerTests(void) {
audioStreamClose(stream);
}
+ tCase("mixer: a paused voice holds its place, a flushed stream drops its frames");
+ {
+ AudioStreamT *stream = NULL;
+
+ v = audioPlay(s, FULL, AUDIO_PAN_CENTER, false);
+ render(PAUSE_FRAMES);
+ TEST_EQ(audioVoicePause(ctx, v, true), AUDIO_OK);
+ render(PAUSE_FRAMES);
+ TEST_EQ(outPeak(PAUSE_FRAMES), 0);
+ TEST_TRUE(audioVoiceIsPlaying(ctx, v));
+ TEST_EQ(audioVoicePause(ctx, v, false), AUDIO_OK);
+ render(1);
+ TEST_EQ(sOut[0], ramp[PAUSE_FRAMES]);
+ audioVoiceStop(ctx, v);
+ TEST_EQ(audioVoicePause(ctx, v, true), AUDIO_ERR_VOICE);
+ v = audioTone(ctx, AUDIO_WAVE_SQUARE, TONE_HZ, FULL, 0);
+ TEST_EQ(audioVoicePause(ctx, v, true), AUDIO_ERR_ARG);
+ audioVoiceStop(ctx, v);
+
+ TEST_EQ(audioStreamOpen(ctx, &mono16, 0, NULL, NULL, &stream), AUDIO_OK);
+ audioStreamWrite(stream, ramp, PCM_FRAMES);
+ render(PAUSE_FRAMES);
+ audioVoicePause(ctx, audioStreamVoice(stream), true);
+
+ uint32_t queued = audioStreamQueued(stream);
+
+ render(PAUSE_FRAMES);
+ TEST_EQ(outPeak(PAUSE_FRAMES), 0);
+ TEST_EQ(audioStreamQueued(stream), queued);
+ audioStreamFlush(stream);
+ audioStreamFlush(NULL);
+ TEST_EQ(audioStreamQueued(stream), 0);
+ audioVoicePause(ctx, audioStreamVoice(stream), false);
+ render(MIX_HELD_FRAMES);
+ render(PAUSE_FRAMES);
+ TEST_EQ(outPeak(PAUSE_FRAMES), 0);
+ audioStreamClose(stream);
+ audioPump();
+ }
+
tCase("mixer: several programs, streams, voices and tones at once");
{
AudioCtxT *other = NULL;
@@ -2047,6 +2682,173 @@ static void put32(uint8_t *p, uint32_t v) {
}
+// Recording with the gamma fake (it records from the microphone and the
+// line; the test pushes the blocks the card's interrupt would): the card
+// changes hands and comes back, the frames arrive in order with their
+// level, a full buffer drops whole blocks, a file is a WAV that reads
+// back, and a closing program and a DOS program end the recording.
+static void recordTests(void) {
+ AudioFormatT fmt;
+ AudioInfoT info;
+ AudioRecT *rec = NULL;
+ AudioRecT *second = NULL;
+ int16_t block[REC_BLOCK_FRAMES];
+ int16_t back[REC_BLOCK_FRAMES];
+ uint8_t block8[REC_BLOCK_FRAMES * 2];
+
+ for (int32_t i = 0; i < REC_BLOCK_FRAMES; i++) {
+ block[i] = (int16_t)((i & 1) ? REC_LEVEL : -REC_LEVEL);
+ block8[i * 2] = REC_U8_LOW;
+ block8[i * 2 + 1] = REC_U8_HIGH;
+ }
+
+ tCase("recording: what the device records from");
+ setIni("driver = gamma\n");
+
+ AudioCtxT *ctx = audioOpen();
+
+ audioGetInfo(&info);
+ TEST_TRUE(info.canRecord);
+ TEST_EQ(info.recSources, (1U << AUDIO_REC_MIC) | (1U << AUDIO_REC_LINE));
+
+ tCase("recording: bad arguments, a source the card lacks");
+ memset(&fmt, 0, sizeof(fmt));
+ TEST_EQ(audioRecOpen(ctx, &fmt, -1, 0, &rec), AUDIO_ERR_ARG);
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_SOURCES, 0, &rec), AUDIO_ERR_ARG);
+ fmt.bits = REC_BAD_BITS;
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_MIC, 0, &rec), AUDIO_ERR_ARG);
+ fmt.bits = 0;
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_CD, 0, &rec), AUDIO_ERR_NO_RECORD);
+ TEST_TRUE(rec == NULL);
+
+ tCase("recording: the card changes hands, the format, voices refused");
+ int32_t tone = audioTone(ctx, AUDIO_WAVE_SQUARE, TONE_HZ, FULL, 0);
+
+ TEST_TRUE(tone > 0);
+ eventsClear();
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_LINE, 0, &rec), AUDIO_OK);
+ TEST_TRUE(rec != NULL);
+ TEST_TRUE(eventSeen("gamma close"));
+ TEST_TRUE(eventSeen("gamma record 1"));
+ TEST_EQ(fmt.rate, AUDIO_RATE_DEFAULT + REC_RATE_OFF);
+ TEST_EQ(fmt.bits, AUDIO_REC_BITS_DEFAULT);
+ TEST_EQ(fmt.channels, 1);
+ TEST_EQ(audioTone(ctx, AUDIO_WAVE_SQUARE, TONE_HZ, FULL, TONE_MS), AUDIO_ERR_BUSY);
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_MIC, 0, &second), AUDIO_ERR_BUSY);
+
+ tCase("recording: frames arrive in order with their level");
+ audioRecPush(block, sizeof(block));
+ TEST_EQ(audioRecQueued(rec), REC_BLOCK_FRAMES);
+ TEST_EQ(audioRecRead(rec, back, REC_BLOCK_FRAMES), REC_BLOCK_FRAMES);
+ TEST_TRUE(memcmp(back, block, sizeof(block)) == 0);
+ TEST_EQ(audioRecFrames(rec), REC_BLOCK_FRAMES);
+ TEST_EQ(audioRecPeak(rec), REC_LEVEL * PERCENT_FULL / INT16_MAX);
+ TEST_EQ(audioRecPeak(rec), 0);
+ TEST_EQ(audioRecRead(rec, back, REC_BLOCK_FRAMES), 0);
+
+ tCase("recording: a full buffer drops whole blocks");
+ for (int32_t i = 0; i < REC_FILL_BLOCKS; i++) {
+ audioRecPush(block, sizeof(block));
+ }
+
+ TEST_TRUE(audioRecLost(rec) > 0);
+ TEST_EQ(audioRecQueued(rec) + audioRecLost(rec), REC_FILL_BLOCKS * REC_BLOCK_FRAMES);
+ TEST_EQ(audioRecQueued(rec) % REC_BLOCK_FRAMES, 0);
+
+ while (audioRecRead(rec, back, REC_BLOCK_FRAMES) > 0) {
+ TEST_TRUE(memcmp(back, block, sizeof(block)) == 0);
+ }
+
+ tCase("recording: closing gives the card back, the held tone plays on");
+ eventsClear();
+ TEST_EQ(audioRecClose(rec), AUDIO_OK);
+ TEST_TRUE(eventSeen("gamma recordstop"));
+ TEST_TRUE(eventSeen("gamma open"));
+ TEST_TRUE(eventSeen("gamma play"));
+ TEST_TRUE(audioVoiceIsPlaying(ctx, tone));
+ audioVoiceStop(ctx, tone);
+ TEST_TRUE(audioTone(ctx, AUDIO_WAVE_SQUARE, TONE_HZ, FULL, TONE_MS) > 0);
+ TEST_EQ(audioRecClose(NULL), AUDIO_OK);
+
+ tCase("recording into a WAV file that reads back");
+ AudioFormatT stereo8 = { REC_RATE, 8, 2 };
+
+ TEST_EQ(audioRecFile(ctx, REC_FILE, &stereo8, AUDIO_REC_MIC, &rec), AUDIO_OK);
+ TEST_EQ(stereo8.rate, REC_RATE + REC_RATE_OFF);
+ audioRecPush(block8, sizeof(block8));
+ audioRecPush(block8, sizeof(block8));
+ audioPump();
+ TEST_EQ(audioRecFrames(rec), 2 * REC_BLOCK_FRAMES);
+ TEST_EQ(audioRecRead(rec, back, REC_BLOCK_FRAMES), 0);
+ audioRecPush(block8, sizeof(block8));
+ TEST_EQ(audioRecClose(rec), AUDIO_OK);
+
+ int32_t size = 0;
+ uint8_t *data = (uint8_t *)platformReadFile(REC_FILE, &size);
+ int16_t *pcm = NULL;
+ uint32_t frames = 0;
+ AudioFormatT got;
+
+ TEST_EQ(size, AUDIO_WAV_HEADER_BYTES + REC_FILE_BLOCKS * (int32_t)sizeof(block8));
+ TEST_EQ(audioDecode(data, (uint32_t)size, &pcm, &frames, &got), AUDIO_OK);
+ TEST_EQ(frames, REC_FILE_BLOCKS * REC_BLOCK_FRAMES);
+ TEST_EQ(got.rate, REC_RATE + REC_RATE_OFF);
+ TEST_EQ(got.bits, 8);
+ TEST_EQ(got.channels, 2);
+ TEST_TRUE(pcm && pcm[0] < 0 && pcm[1] > 0);
+ free(pcm);
+ free(data);
+
+ tCase("recording into a file that cannot be made; a refused one leaves a file alone");
+ TEST_EQ(audioRecFile(ctx, REC_BAD_FILE, &fmt, AUDIO_REC_MIC, &rec), AUDIO_ERR_WRITE);
+ TEST_EQ(audioRecFile(ctx, REC_FILE, &fmt, AUDIO_REC_CD, &rec), AUDIO_ERR_NO_RECORD);
+ data = (uint8_t *)platformReadFile(REC_FILE, &size);
+ TEST_EQ(size, AUDIO_WAV_HEADER_BYTES + REC_FILE_BLOCKS * (int32_t)sizeof(block8));
+ free(data);
+
+ tCase("recording: a DOS program and the program's end close it");
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_MIC, 0, &rec), AUDIO_OK);
+ eventsClear();
+ audioSuspend(true);
+ TEST_TRUE(eventSeen("gamma recordstop"));
+ audioSuspend(false);
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_MIC, 0, &rec), AUDIO_OK);
+ eventsClear();
+ audioClose(ctx);
+ TEST_TRUE(eventSeen("gamma recordstop"));
+
+ tCase("recording: a card that cannot record");
+ setIni("driver = mono8\n");
+ ctx = audioOpen();
+ audioGetInfo(&info);
+ TEST_TRUE(!info.canRecord);
+ TEST_EQ(info.recSources, 0);
+ TEST_EQ(audioRecOpen(ctx, &fmt, AUDIO_REC_MIC, 0, &rec), AUDIO_ERR_NO_RECORD);
+ audioClose(ctx);
+
+ tCase("audioWavHeader: a header audioWavParse reads");
+ uint8_t head[AUDIO_WAV_HEADER_BYTES];
+ AudioFormatT hf = { REC_HEAD_RATE, 16, 2 };
+ AudioFormatT pf;
+ uint32_t at = 0;
+ uint32_t bytes = 0;
+
+ audioWavHeader(head, &hf, REC_HEAD_DATA);
+ TEST_TRUE(memcmp(head, "RIFF", WAV_FOURCC_LEN) == 0 && memcmp(head + WAV_HEAD_WAVE, "WAVEfmt ", WAV_HEAD_WAVE_LEN) == 0 && memcmp(head + WAV_HEAD_DATA_TAG, "data", WAV_FOURCC_LEN) == 0);
+ TEST_EQ(head[WAV_HEAD_RIFF_SIZE] | (head[WAV_HEAD_RIFF_SIZE + 1] << 8), REC_HEAD_DATA + WAV_HEAD_RIFF_EXTRA);
+ TEST_EQ(head[WAV_HEAD_ALIGN], hf.bits / 8 * hf.channels);
+ TEST_EQ(head[WAV_HEAD_BITS], hf.bits);
+ TEST_EQ(head[WAV_HEAD_DATA_SIZE] | (head[WAV_HEAD_DATA_SIZE + 1] << 8), REC_HEAD_DATA);
+ TEST_EQ(audioWavParse(head, sizeof(head), sizeof(head) + REC_HEAD_DATA, &pf, &at, &bytes), AUDIO_OK);
+ TEST_EQ(pf.rate, REC_HEAD_RATE);
+ TEST_EQ(pf.channels, 2);
+ TEST_EQ(at, AUDIO_WAV_HEADER_BYTES);
+ TEST_EQ(bytes, REC_HEAD_DATA);
+ TEST_STR_EQ(audioErrorText(AUDIO_ERR_NO_RECORD), "The sound device cannot record from that input");
+ TEST_STR_EQ(audioErrorText(AUDIO_ERR_WRITE), "The file could not be written");
+}
+
+
static void render(uint32_t frames) {
memset(sOut, 0, sizeof(sOut));
audioMixRender(sOut, frames);
@@ -2089,6 +2891,10 @@ static void sbTests(void) {
TEST_EQ(bl.dma8, 1);
TEST_EQ(bl.dma16, 5);
TEST_EQ(bl.type, 6);
+ TEST_EQ(bl.emu, SB_NONE);
+ TEST_TRUE(sbParseBlaster("A220 I5 D1 H5 P330 E620 T6", &bl));
+ TEST_EQ(bl.emu, 0x620);
+ TEST_EQ(bl.type, 6);
TEST_TRUE(sbParseBlaster("a240 i7 d3", &bl));
TEST_EQ(bl.base, 0x240);
TEST_EQ(bl.dma16, SB_NONE);
@@ -2134,13 +2940,41 @@ static void sbTests(void) {
TEST_STR_EQ(sbModelName(0x0201), "Sound Blaster 2.0");
TEST_STR_EQ(sbModelName(0x0105), "Sound Blaster");
- tCase("sb: DMA buffer placement");
- TEST_EQ(sbDmaPlace(0x20000, 8192, 4096, SB_DMA_BOUNDARY_8), 0);
- TEST_EQ(sbDmaPlace(0x2F800, 8192, 4096, SB_DMA_BOUNDARY_8), 0x800);
- TEST_EQ(sbDmaPlace(0x2F000, 8192, 4096, SB_DMA_BOUNDARY_8), 0);
- TEST_EQ(sbDmaPlace(0x2F001, 4096, 4096, SB_DMA_BOUNDARY_8), SB_NONE);
- TEST_EQ(sbDmaPlace(0x3F800, 8192, 4096, SB_DMA_BOUNDARY_16), 0x800);
- TEST_EQ(sbDmaPlace(0x2F800, 8192, 4096, SB_DMA_BOUNDARY_16), 0);
+ tCase("sb: recording plans by DSP version");
+ sbRecordPlan(0x0405, RATE_DEV, 16, 2, true, &plan);
+ TEST_EQ(plan.mode, SB_MODE_SB16);
+ TEST_EQ(plan.rate, RATE_DEV);
+ TEST_EQ(plan.bits, 16);
+ TEST_EQ(plan.channels, 2);
+ sbRecordPlan(0x0302, RATE_DEV, 16, 2, false, &plan);
+ TEST_EQ(plan.mode, SB_MODE_HIGH);
+ TEST_EQ(plan.bits, 8);
+ TEST_EQ(plan.channels, 2);
+ sbRecordPlan(0x0201, RATE_DEV, 16, 2, false, &plan);
+ TEST_EQ(plan.mode, SB_MODE_HIGH);
+ TEST_EQ(plan.bits, 8);
+ TEST_EQ(plan.channels, 1);
+ TEST_EQ(plan.timeConstant, 189);
+ TEST_EQ(plan.rate, 14925);
+ sbRecordPlan(0x0201, 8000, 8, 1, false, &plan);
+ TEST_EQ(plan.mode, SB_MODE_AUTO);
+ TEST_EQ(plan.rate, 8000);
+ sbRecordPlan(0x0200, RATE_DEV, 8, 1, false, &plan);
+ TEST_EQ(plan.mode, SB_MODE_AUTO);
+ TEST_EQ(plan.rate, 12987);
+ sbRecordPlan(0x0105, RATE_HALF, 8, 1, false, &plan);
+ TEST_EQ(plan.mode, SB_MODE_SINGLE);
+ TEST_EQ(plan.rate, 10989);
+
+ tCase("isa: DMA buffer placement");
+ TEST_EQ(isaDmaPlace(0x20000, 8192, 4096, ISA_DMA_BOUNDARY_8), 0);
+ TEST_EQ(isaDmaPlace(0x2F800, 8192, 4096, ISA_DMA_BOUNDARY_8), 0x800);
+ TEST_EQ(isaDmaPlace(0x2F000, 8192, 4096, ISA_DMA_BOUNDARY_8), 0);
+ TEST_EQ(isaDmaPlace(0x2F001, 4096, 4096, ISA_DMA_BOUNDARY_8), ISA_NONE);
+ TEST_EQ(isaDmaPlace(0x3F800, 8192, 4096, ISA_DMA_BOUNDARY_16), 0x800);
+ TEST_EQ(isaDmaPlace(0x2F800, 8192, 4096, ISA_DMA_BOUNDARY_16), 0);
+ TEST_EQ(isaDmaBoundary(1), ISA_DMA_BOUNDARY_8);
+ TEST_EQ(isaDmaBoundary(5), ISA_DMA_BOUNDARY_16);
}
@@ -2220,18 +3054,24 @@ static void settingsBasicTests(void) {
TEST_EQ(SoundMidiCount(), AUDIO_MIDI_CHOICE_COUNT);
TEST_STR_EQ(SoundMidiLabel(0), "Automatic");
TEST_STR_EQ(SoundMidiLabel(AUDIO_MIDI_CHOICE_COUNT), "");
+ TEST_EQ(SoundModulesCount(), AUDIO_MODULES_CHOICE_COUNT);
+ TEST_STR_EQ(SoundModulesLabel(MODS_SOFT_INDEX), "Mixed in software");
+ TEST_STR_EQ(SoundModulesLabel(-1), "");
tCase("basic: [audio] settings through a settings handle");
TEST_STR_EQ(SoundSettingDriver(h), "gamma");
TEST_EQ(SoundSettingMidi(h), MIDI_FM_INDEX);
+ TEST_EQ(SoundSettingModules(h), 0);
TEST_EQ(SoundSettingVolume(h), PERCENT_INI);
- SoundSettingsStore(h, "beta", MIDI_MPU_INDEX, PERCENT_STORED);
+ SoundSettingsStore(h, "beta", MIDI_MPU_INDEX, MODS_SOFT_INDEX, PERCENT_STORED);
TEST_STR_EQ(SoundSettingDriver(h), "beta");
TEST_EQ(SoundSettingMidi(h), MIDI_MPU_INDEX);
+ TEST_EQ(SoundSettingModules(h), MODS_SOFT_INDEX);
TEST_EQ(SoundSettingVolume(h), PERCENT_STORED);
- SoundSettingsStore(BAS_BAD_HANDLE, "beta", 0, 0);
+ SoundSettingsStore(BAS_BAD_HANDLE, "beta", 0, 0, 0);
TEST_STR_EQ(SoundSettingDriver(BAS_BAD_HANDLE), AUDIO_DRIVER_AUTO);
TEST_EQ(SoundSettingMidi(BAS_BAD_HANDLE), 0);
+ TEST_EQ(SoundSettingModules(BAS_BAD_HANDLE), 0);
TEST_EQ(SoundSettingVolume(BAS_BAD_HANDLE), PERCENT_FULL);
PrefsClose(h);
}
@@ -2257,13 +3097,16 @@ static void settingsTests(void) {
audioDeskSetFile(prefs, SoundCountE, "x.wav");
TEST_STR_EQ(audioDeskFile(prefs, SoundWarningE), "w.wav");
TEST_STR_EQ(prefsGetString(prefs, AUDIO_SOUNDS_SECTION, "question", "?"), "");
- audioSettingsStore(prefs, "", AUDIO_MIDI_CHOICE_COUNT, -1);
+ audioSettingsStore(prefs, "", AUDIO_MIDI_CHOICE_COUNT, AUDIO_MODULES_CHOICE_COUNT, -1);
TEST_STR_EQ(audioSettingDriver(prefs), AUDIO_DRIVER_AUTO);
TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, ""), AUDIO_MIDI_AUTO);
+ TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MODULES, ""), AUDIO_MODULES_WAVETABLE);
TEST_EQ(audioSettingVolume(prefs), 0);
- audioSettingsStore(prefs, "delta", MIDI_MPU_INDEX, PERCENT_HALF);
+ audioSettingsStore(prefs, "delta", MIDI_MPU_INDEX, MODS_SOFT_INDEX, PERCENT_HALF);
TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_DRIVER, ""), "delta");
TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, ""), AUDIO_MIDI_MPU401);
+ TEST_STR_EQ(prefsGetString(prefs, AUDIO_INI_SECTION, AUDIO_INI_MODULES, ""), AUDIO_MODULES_SOFTWARE);
+ TEST_EQ(audioSettingModules(prefs), MODS_SOFT_INDEX);
TEST_EQ(prefsGetInt(prefs, AUDIO_INI_SECTION, AUDIO_INI_VOLUME, 0), PERCENT_HALF);
prefsClose(prefs);
@@ -2287,6 +3130,59 @@ static void settingsTests(void) {
}
+// DVX steps aside for a DOS program (audioSuspend, the library's
+// platform suspend hook): the sound stops, the output and the
+// synthesizers close; back, the synthesizers open again, and the next
+// sound opens the output, as after a silence. With no device open it
+// does nothing.
+static void suspendTests(void) {
+ AudioSampleT *s = NULL;
+ int16_t pcm[PCM_FRAMES];
+ AudioFormatT mono16 = { RATE_DEV, 16, 1 };
+
+ for (int32_t i = 0; i < PCM_FRAMES; i++) {
+ pcm[i] = FULL;
+ }
+
+ tCase("suspend: the sound stops, the output and the synthesizers close and come back");
+ setIni("driver = gamma\n");
+
+ AudioCtxT *ctx = audioOpen();
+
+ audioSampleCreate(ctx, pcm, PCM_FRAMES, &mono16, &s);
+
+ int32_t v = audioPlay(s, FULL, 0, true);
+
+ TEST_TRUE(v > 0);
+ eventsClear();
+ sSynthOpens = 0;
+ sSynthCloses = 0;
+ audioSuspend(true);
+ TEST_TRUE(!audioVoiceIsPlaying(ctx, v));
+ TEST_TRUE(eventSeen("gamma close"));
+ TEST_EQ(sSynthCloses, 1);
+ TEST_EQ(sSynthOpens, 0);
+ audioSuspend(false);
+ TEST_EQ(sSynthOpens, 1);
+ TEST_TRUE(!eventSeen("gamma open"));
+ TEST_TRUE(audioPlay(s, FULL, 0, false) > 0);
+ TEST_TRUE(eventSeen("gamma open"));
+ TEST_TRUE(eventSeen("gamma play"));
+ render(PCM_FRAMES);
+ TEST_TRUE(outPeak(PCM_FRAMES) > 0);
+ audioSampleFree(s);
+ audioClose(ctx);
+
+ tCase("suspend: nothing open, nothing done");
+ eventsClear();
+ sSynthCloses = 0;
+ audioSuspend(true);
+ audioSuspend(false);
+ TEST_EQ(sEventCount, 0);
+ TEST_EQ(sSynthCloses, 0);
+}
+
+
static void toneTests(void) {
AudioCtxT *ctx;
@@ -2340,6 +3236,7 @@ static void toneTests(void) {
}
+
static void voiceTests(void) {
AudioCtxT *ctx;
AudioCtxT *other;
@@ -2422,6 +3319,149 @@ static void voiceTests(void) {
}
+// The Windows Sound System board's configuration register (wssCalc.c).
+static void wssTests(void) {
+ uint8_t value = 0;
+
+ tCase("wss: the configuration register");
+ TEST_TRUE(wssConfig(7, 1, &value));
+ TEST_EQ(value, 0x0A);
+ TEST_TRUE(wssConfig(7, 3, &value));
+ TEST_EQ(value, 0x0B);
+ TEST_TRUE(wssConfig(10, 1, &value));
+ TEST_EQ(value, 0x1A);
+ TEST_TRUE(wssConfig(11, 0, &value));
+ TEST_EQ(value, 0x21);
+ TEST_TRUE(!wssConfig(5, 1, &value));
+ TEST_TRUE(!wssConfig(7, 2, &value));
+}
+
+
+// The wavetable synthesizer: voices shared like FM ones, the calls passed
+// to the driver with its own voice numbers, sample memory first fit and
+// aligned (16-bit samples kept inside 256 KB where the chip asks), uploads
+// only into a block of the context's own, everything given back when the
+// context closes.
+static void wtTests(void) {
+ AudioWtInfoT info;
+ AudioWtVoiceT note;
+ int32_t ids[5];
+ uint32_t addr = 0;
+ uint32_t addr2 = 0;
+ int16_t ramp[4] = { 0x0102, 0x0304, -2, 0x7F00 };
+
+ tCase("wavetable: none unless the driver finds one");
+ setIni("driver = gamma\n");
+
+ AudioCtxT *a = audioOpen();
+
+ audioWtGetInfo(&info);
+ TEST_TRUE(!info.present);
+ TEST_EQ(audioWtAlloc(a), AUDIO_ERR_NO_SYNTH);
+ TEST_EQ(audioWtMemAlloc(a, 64, 16, &addr), AUDIO_ERR_NO_SYNTH);
+ audioClose(a);
+
+ tCase("wavetable: shared voices, least recently freed first");
+ setIni("driver = gamma\nfakeWtVoices = 4\n");
+ a = audioOpen();
+
+ AudioCtxT *b = audioOpen();
+
+ audioWtGetInfo(&info);
+ TEST_TRUE(info.present);
+ TEST_STR_EQ(info.description, "Fake wavetable (4 voices)");
+ TEST_EQ(info.voices, 4);
+ TEST_EQ(info.memory, WT_FAKE_MEMORY);
+ TEST_EQ(info.freeMemory, WT_FAKE_MEMORY);
+ TEST_EQ(info.flags, AUDIO_WT_ENVELOPES | AUDIO_WT_LFOS | AUDIO_WT_FILTER | AUDIO_WT_EFFECTS);
+
+ for (int32_t i = 0; i < 4; i++) {
+ ids[i] = audioWtAlloc(i < 2 ? a : b);
+ TEST_TRUE(ids[i] > 0);
+ }
+
+ ids[4] = audioWtAlloc(a);
+ TEST_EQ(ids[4], AUDIO_ERR_NO_VOICE);
+ audioWtGetInfo(&info);
+ TEST_EQ(info.freeVoices, 0);
+
+ memset(¬e, 0, sizeof(note));
+ note.pitch = 1200;
+ note.attenuation = 100;
+ note.bits = 16;
+ sWtStarts = 0;
+ TEST_EQ(audioWtStart(a, ids[1], ¬e), AUDIO_OK);
+ TEST_EQ(sWtStarts, 1);
+ TEST_EQ(sWtVoice[1].pitch, 1200);
+ TEST_TRUE(audioWtActive(a, ids[1]));
+ TEST_EQ(audioWtStart(b, ids[1], ¬e), AUDIO_ERR_VOICE);
+ TEST_TRUE(!audioWtActive(b, ids[1]));
+ note.pitch = 1300;
+ TEST_EQ(audioWtUpdate(a, ids[1], ¬e, AUDIO_WT_SET_PITCH), AUDIO_OK);
+ TEST_EQ(sWtVoice[1].pitch, 1300);
+ TEST_EQ(sWtWhat, AUDIO_WT_SET_PITCH);
+ sWtReleased = 0;
+ TEST_EQ(audioWtRelease(a, ids[1]), AUDIO_OK);
+ TEST_EQ(sWtReleased, 1u << 1);
+ sWtStopped = 0;
+ TEST_EQ(audioWtStop(a, ids[1]), AUDIO_OK);
+ TEST_EQ(sWtStopped, 1u << 1);
+ TEST_TRUE(!audioWtActive(a, ids[1]));
+
+ // Voice 0 freed first, then 2: 0 comes back first, with a new id
+ TEST_EQ(audioWtFree(a, ids[0]), AUDIO_OK);
+ TEST_EQ(audioWtFree(b, ids[2]), AUDIO_OK);
+ TEST_EQ(audioWtFree(b, ids[2]), AUDIO_ERR_VOICE);
+
+ int32_t again = audioWtAlloc(b);
+
+ TEST_EQ((again - 1) % 64, 0);
+ TEST_TRUE(again != ids[0]);
+ TEST_EQ(audioWtStart(a, ids[0], ¬e), AUDIO_ERR_VOICE);
+
+ tCase("wavetable: sample memory, first fit and aligned");
+ TEST_EQ(audioWtMemAlloc(a, 1000, 16, &addr), AUDIO_OK);
+ TEST_EQ(addr, 0);
+ TEST_EQ(audioWtMemAlloc(b, 1000, 16, &addr2), AUDIO_OK);
+ TEST_EQ(addr2, 1024);
+ TEST_EQ(audioWtMemAlloc(a, 64, 8, &addr2), AUDIO_ERR_ARG);
+ TEST_EQ(audioWtMemAlloc(a, WT_FAKE_MEMORY, 16, &addr2), AUDIO_ERR_NO_ROOM);
+ TEST_EQ(audioWtUpload(a, addr, ramp, 4, 16), AUDIO_OK);
+ TEST_EQ(sWtMemory[0], 0x02);
+ TEST_EQ(sWtMemory[1], 0x01);
+ TEST_EQ(sWtMemory[7], 0x7F);
+ TEST_EQ(audioWtUpload(b, addr, ramp, 4, 16), AUDIO_ERR_ARG);
+ TEST_EQ(audioWtUpload(a, addr + 998, ramp, 2, 16), AUDIO_ERR_ARG);
+ TEST_EQ(audioWtMemFree(b, addr), AUDIO_ERR_ARG);
+ TEST_EQ(audioWtMemFree(a, addr), AUDIO_OK);
+ TEST_EQ(audioWtMemAlloc(a, 500, 16, &addr), AUDIO_OK);
+ TEST_EQ(addr, 0);
+
+ tCase("wavetable: closing a context gives its voices and memory back");
+ audioClose(a);
+ audioWtGetInfo(&info);
+ TEST_EQ(info.freeVoices, 2);
+ TEST_EQ(audioWtMemAlloc(b, 1024, 16, &addr), AUDIO_OK);
+ TEST_EQ(addr, 0);
+ audioClose(b);
+
+ tCase("wavetable: 8-bit samples, and 16-bit ones inside 256 KB");
+ setIni("driver = gamma\nfakeWtVoices = 2\nfakeWtFlags = 48\n");
+ a = audioOpen();
+ TEST_EQ(audioWtMemAlloc(a, WT_BANK - 256, 8, &addr), AUDIO_OK);
+ TEST_EQ(addr, 0);
+ TEST_EQ(audioWtMemAlloc(a, 512, 16, &addr2), AUDIO_OK);
+ TEST_EQ(addr2, WT_BANK);
+ TEST_EQ(audioWtMemAlloc(a, 128, 8, &addr2), AUDIO_OK);
+ TEST_EQ(addr2, WT_BANK - 256);
+ TEST_EQ(audioWtMemAlloc(a, WT_BANK + 2, 16, &addr2), AUDIO_ERR_NO_ROOM);
+ TEST_EQ(audioWtUpload(a, addr, ramp, 4, 8), AUDIO_OK);
+ TEST_EQ(sWtMemory[0], 0x01);
+ TEST_EQ(sWtMemory[2], 0xFF);
+ audioClose(a);
+}
+
+
int main(void) {
tSuiteBegin("audio");
sUpdate = getenv("UPDATE_GOLDENS") && strcmp(getenv("UPDATE_GOLDENS"), "1") == 0;
@@ -2440,14 +3480,23 @@ int main(void) {
voiceTests();
programTests();
idleTests();
+ suspendTests();
+ recordTests();
toneTests();
fileTests();
sbTests();
+ codecTests();
+ wssTests();
+ gusTests();
+ aweTests();
basicTests();
basicSampleTests();
+ basicRecordTests();
basicStreamTests();
fmTests();
midiTests();
+ midiInTests();
+ wtTests();
basicSynthTests();
basicLiveTests();
notesTests();
diff --git a/src/test/audio/fakeDrv.c b/src/test/audio/fakeDrv.c
index 555b7a24..10207948 100644
--- a/src/test/audio/fakeDrv.c
+++ b/src/test/audio/fakeDrv.c
@@ -35,11 +35,24 @@
// FAKE_CLOCK play through audioClockStart
// FAKE_BAD_ABI descriptor built for another DVX version
// FAKE_NO_DESC no descriptor at all
+// FAKE_REC_SOURCES what it records from (bits 1 << AUDIO_REC_*;
+// 0, the default: it cannot record). It
+// records the format asked at FAKE_REC_RATE_OFF
+// Hz more (a card's nearest rate); the test
+// pushes the "recorded" blocks itself
+// (audioRecPush), as the card's interrupt would.
#include "audioDrv.h"
#include
+#ifndef FAKE_REC_SOURCES
+#define FAKE_REC_SOURCES 0
+#endif
+
+#define FAKE_REC_RATE_OFF 5
+#define FAKE_EVENT_MAX 32
+
// Test hooks (audioTest.c)
void fakeDrvEvent(const char *driver, const char *event);
void fakeDrvTone(uint32_t hz);
@@ -55,16 +68,20 @@ const uint32_t fakeNotADriver = 1;
static void fakeClose(void);
static bool fakeOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info);
static bool fakePlay(void);
+static bool fakeRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got);
+static void fakeRecordStop(void);
static void fakeTone(uint32_t hz);
#ifdef FAKE_BAD_ABI
extern const AudioDriverT audioDriver;
-const AudioDriverT audioDriver = { .hdr = { DVX_ABI_VERSION + 1, sizeof(AudioDriverT), FAKE_NAME, FAKE_PRIORITY }, .open = fakeOpen, .play = fakePlay, .close = fakeClose };
+const AudioDriverT audioDriver = { .hdr = { DVX_ABI_VERSION + 1, sizeof(AudioDriverT), FAKE_NAME, FAKE_PRIORITY }, .open = fakeOpen, .play = fakePlay, .close = fakeClose, .recordStart = FAKE_REC_SOURCES ? fakeRecordStart : NULL, .recordStop = FAKE_REC_SOURCES ? fakeRecordStop : NULL };
#else
AUDIO_DRIVER(FAKE_NAME, FAKE_PRIORITY,
- .open = fakeOpen,
- .play = fakePlay,
- .close = fakeClose
+ .open = fakeOpen,
+ .play = fakePlay,
+ .close = fakeClose,
+ .recordStart = FAKE_REC_SOURCES ? fakeRecordStart : NULL,
+ .recordStop = FAKE_REC_SOURCES ? fakeRecordStop : NULL
);
#endif
@@ -87,7 +104,8 @@ static bool fakeOpen(const AudioDrvConfigT *cfg, AudioDrvInfoT *info) {
info->bits = FAKE_BITS;
info->channels = FAKE_CHANNELS;
info->hasDac = FAKE_DAC;
- info->tone = FAKE_TONE ? fakeTone : NULL;
+ info->tone = FAKE_TONE ? fakeTone : NULL;
+ info->recSources = FAKE_REC_SOURCES;
return FAKE_OPEN_OK;
}
@@ -103,6 +121,23 @@ static bool fakePlay(void) {
}
+// Reports "record "; the card's nearest rate is a little above.
+static bool fakeRecordStart(const AudioFormatT *want, int32_t source, AudioFormatT *got) {
+ char text[FAKE_EVENT_MAX];
+
+ snprintf(text, sizeof(text), "record %ld", (long)source);
+ fakeDrvEvent(FAKE_NAME, text);
+ *got = *want;
+ got->rate += FAKE_REC_RATE_OFF;
+ return true;
+}
+
+
+static void fakeRecordStop(void) {
+ fakeDrvEvent(FAKE_NAME, "recordstop");
+}
+
+
static void fakeTone(uint32_t hz) {
fakeDrvTone(hz);
}
diff --git a/src/test/audio/fakeSynth.c b/src/test/audio/fakeSynth.c
index 7eea3d46..7d7a3955 100644
--- a/src/test/audio/fakeSynth.c
+++ b/src/test/audio/fakeSynth.c
@@ -23,12 +23,19 @@
// fakeSynth.c -- fake synthesizer drivers for the audio and music suites
//
// Built into .drv shared objects in a suite's work tree (FAKE_SYN_FM for
-// the FM chip, otherwise the MIDI port), where audio.lib discovers them
-// like real synth drivers. Register writes go to fakeSynthWrite and
-// bytes to fakeSynthPut in the test program. The FM fake has DVX.INI
-// [audio] fakeFmVoices voices (default 9; 0: no chip) and is stereo with
-// fakeFmStereo = true; the MIDI fake, like the real one, is there only
-// with [audio] midi = mpu401.
+// the FM chip, FAKE_SYN_WT for a wavetable synthesizer, otherwise the
+// MIDI port), where audio.lib discovers them like real synth drivers.
+// Register writes go to fakeSynthWrite and bytes to fakeSynthPut in the
+// test program, MIDI input comes from its fakeSynthGet, a wavetable's
+// calls go to the fakeWt* functions. The FM
+// fake has DVX.INI [audio] fakeFmVoices voices (default 9; 0: no chip)
+// and is stereo with fakeFmStereo = true; the MIDI fake, like the real
+// one, is there only with [audio] midi or midiIn = mpu401; the wavetable fake has
+// fakeWtVoices voices (default 0: not there), fakeWtMemory bytes
+// (default FAKE_WT_MEMORY), does what fakeWtFlags says (AUDIO_WT_*,
+// default FAKE_WT_FLAGS: an AWE), and reports fakeWtLevel as its level
+// (default 0). An open that finds the fake there and every close go to
+// fakeSynthEvent.
#include "audioDrv.h"
@@ -38,43 +45,95 @@
#define FAKE_FM_VOICES 9
#define INI_FAKE_VOICES "fakeFmVoices"
#define INI_FAKE_STEREO "fakeFmStereo"
+#define INI_WT_VOICES "fakeWtVoices"
+#define INI_WT_MEMORY "fakeWtMemory"
+#define INI_WT_FLAGS "fakeWtFlags"
+#define INI_WT_LEVEL "fakeWtLevel"
+#define FAKE_WT_MEMORY (1024 * 1024)
+#define FAKE_WT_FLAGS (AUDIO_WT_ENVELOPES | AUDIO_WT_LFOS | AUDIO_WT_FILTER | AUDIO_WT_EFFECTS)
+
+#if defined(FAKE_SYN_FM)
+#define FAKE_SYN_NAME "fmfake"
+#elif defined(FAKE_SYN_WT)
+#define FAKE_SYN_NAME "wtfake"
+#else
+#define FAKE_SYN_NAME "midifake"
+#endif
// Test hooks (the suite's test program)
+void fakeSynthEvent(const char *synth, const char *event);
+bool fakeSynthGet(uint8_t *byte);
bool fakeSynthPut(uint8_t byte);
void fakeSynthWrite(uint32_t reg, uint8_t value);
+bool fakeWtActive(uint32_t voice);
+void fakeWtRelease(uint32_t voice);
+void fakeWtStart(uint32_t voice, const AudioWtVoiceT *v);
+void fakeWtStop(uint32_t voice);
+void fakeWtUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what);
+bool fakeWtUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
static void fakeClose(void);
static bool fakeOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info);
-#ifdef FAKE_SYN_FM
-AUDIO_SYNTH_DRIVER("fmfake", AUDIO_PRIORITY_CARD,
+#if defined(FAKE_SYN_FM)
+AUDIO_SYNTH_DRIVER(FAKE_SYN_NAME, AUDIO_PRIORITY_CARD,
.kind = AUDIO_SYNTH_FM,
.open = fakeOpen,
.close = fakeClose,
.write = fakeSynthWrite
);
+#elif defined(FAKE_SYN_WT)
+AUDIO_SYNTH_DRIVER(FAKE_SYN_NAME, AUDIO_PRIORITY_CARD,
+ .kind = AUDIO_SYNTH_WAVETABLE,
+ .open = fakeOpen,
+ .close = fakeClose,
+ .upload = fakeWtUpload,
+ .start = fakeWtStart,
+ .update = fakeWtUpdate,
+ .release = fakeWtRelease,
+ .stop = fakeWtStop,
+ .active = fakeWtActive
+);
#else
-AUDIO_SYNTH_DRIVER("midifake", AUDIO_PRIORITY_CARD,
+AUDIO_SYNTH_DRIVER(FAKE_SYN_NAME, AUDIO_PRIORITY_CARD,
.kind = AUDIO_SYNTH_MIDI,
.open = fakeOpen,
.close = fakeClose,
- .put = fakeSynthPut
+ .put = fakeSynthPut,
+ .get = fakeSynthGet
);
#endif
static void fakeClose(void) {
+ fakeSynthEvent(FAKE_SYN_NAME, "close");
}
static bool fakeOpen(const AudioDrvConfigT *cfg, AudioSynthDrvInfoT *info) {
-#ifdef FAKE_SYN_FM
+#if defined(FAKE_SYN_FM)
info->voices = (uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, INI_FAKE_VOICES, FAKE_FM_VOICES);
info->stereo = prefsGetBool(cfg->prefs, AUDIO_INI_SECTION, INI_FAKE_STEREO, false);
snprintf(info->description, sizeof(info->description), "Fake FM (%u voices)", (unsigned)info->voices);
- return info->voices > 0;
+
+ bool there = info->voices > 0;
+#elif defined(FAKE_SYN_WT)
+ info->voices = (uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, INI_WT_VOICES, 0);
+ info->memory = (uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, INI_WT_MEMORY, FAKE_WT_MEMORY);
+ info->flags = (uint32_t)prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, INI_WT_FLAGS, FAKE_WT_FLAGS);
+ info->level = prefsGetInt(cfg->prefs, AUDIO_INI_SECTION, INI_WT_LEVEL, 0);
+ snprintf(info->description, sizeof(info->description), "Fake wavetable (%u voices)", (unsigned)info->voices);
+
+ bool there = info->voices > 0;
#else
snprintf(info->description, sizeof(info->description), "Fake MIDI port");
- return strcasecmp(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO), AUDIO_MIDI_MPU401) == 0;
+
+ bool there = strcasecmp(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI, AUDIO_MIDI_AUTO), AUDIO_MIDI_MPU401) == 0 || strcasecmp(prefsGetString(cfg->prefs, AUDIO_INI_SECTION, AUDIO_INI_MIDI_IN, AUDIO_MIDI_IN_NONE), AUDIO_MIDI_MPU401) == 0;
#endif
+
+ if (there) {
+ fakeSynthEvent(FAKE_SYN_NAME, "open");
+ }
+
+ return there;
}
diff --git a/src/test/basdev/testBasDev.c b/src/test/basdev/testBasDev.c
index 4735930d..25058d88 100644
--- a/src/test/basdev/testBasDev.c
+++ b/src/test/basdev/testBasDev.c
@@ -42,6 +42,7 @@
#include "dvxRes.h"
#include "dvxTypes.h"
#include "pmModel.h"
+#include "shellSaver.h"
#include "../../libs/kpunch/audio/audioDrv.h"
#include "../../libs/kpunch/joystick/joystick.h"
#include "../../libs/kpunch/fileops/fileOps.h"
@@ -644,6 +645,9 @@ static void caseIncludeConsts(void) {
{ "system.bas", "SYS_VIDEO_DEFAULT_WIDTH", DVX_DEFAULT_VIDEO_W },
{ "system.bas", "SYS_VIDEO_DEFAULT_HEIGHT", DVX_DEFAULT_VIDEO_H },
{ "system.bas", "SYS_VIDEO_DEFAULT_BPP", DVX_DEFAULT_VIDEO_BPP },
+ { "system.bas", "SCREEN_SAVER_WAIT_MIN", SHELL_SAVER_WAIT_MIN },
+ { "system.bas", "SCREEN_SAVER_WAIT_MAX", SHELL_SAVER_WAIT_MAX },
+ { "system.bas", "SCREEN_SAVER_WAIT_DEFAULT", SHELL_SAVER_WAIT_DEFAULT },
{ "shell.bas", "SHELL_VIEW_LARGE", PmViewLargeE },
{ "shell.bas", "SHELL_VIEW_SMALL", PmViewSmallE },
{ "shell.bas", "SHELL_VIEW_LIST", PmViewListE },
@@ -660,6 +664,7 @@ static void caseIncludeConsts(void) {
{ "fileops.bas", "FILEOPS_KIND_THEMES", DvxKindThemesE },
{ "fileops.bas", "FILEOPS_KIND_WPAPER", DvxKindWpaperE },
{ "fileops.bas", "FILEOPS_KIND_BANKS", DvxKindBanksE },
+ { "fileops.bas", "FILEOPS_KIND_SAVERS", DvxKindSaversE },
{ "pkg.bas", "PKG_FIELD_ID", PkgFieldIdE },
{ "pkg.bas", "PKG_FIELD_NAME", PkgFieldNameE },
{ "pkg.bas", "PKG_FIELD_VERSION", PkgFieldVersionE },
@@ -739,6 +744,10 @@ static void caseIncludeConsts(void) {
{ "sound.bas", "SOUND_STREAM_MS_MIN", AUDIO_STREAM_MS_MIN },
{ "sound.bas", "SOUND_STREAM_MS_DEFAULT", AUDIO_STREAM_MS_DEFAULT },
{ "sound.bas", "SOUND_STREAM_MS_MAX", AUDIO_STREAM_MS_MAX },
+ { "sound.bas", "RECORD_MIC", AUDIO_REC_MIC },
+ { "sound.bas", "RECORD_LINE", AUDIO_REC_LINE },
+ { "sound.bas", "RECORD_CD", AUDIO_REC_CD },
+ { "music.bas", "MIDI_NONE", AUDIO_MIDI_PACKED_NONE },
{ "sound.bas", "FM_VOICES_MAX", AUDIO_FM_VOICES_MAX },
{ "sound.bas", "FM_CHIP_HZ", AUDIO_FM_CHIP_HZ },
{ "sound.bas", "FM_FNUM_MAX", AUDIO_FM_FNUM_MAX },
diff --git a/src/test/dos/fixtures/dosrun/dosprog.bat b/src/test/dos/fixtures/dosrun/dosprog.bat
new file mode 100644
index 00000000..b8ca3ef0
--- /dev/null
+++ b/src/test/dos/fixtures/dosrun/dosprog.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+REM dosprog.scr: run by DVX from TEST\DOSRUN.APP, in its own folder (TEST)
+ECHO ran %1>DOSRAN.TXT
diff --git a/src/test/dos/fixtures/dosrun/dosrun.dbp b/src/test/dos/fixtures/dosrun/dosrun.dbp
new file mode 100644
index 00000000..fe60b1cf
--- /dev/null
+++ b/src/test/dos/fixtures/dosrun/dosrun.dbp
@@ -0,0 +1,18 @@
+[Project]
+Name = DOS Run
+Author = Scott Duensing
+Publisher = Kangaroo Punch Studios
+Version = 1.00
+Copyright = Copyright 2026 Scott Duensing
+Description = DOS harness fixture: asks DVX to run a DOS program (dosprog.scr)
+
+[Modules]
+File0 = ../../../../include/basic/shell.bas
+Count = 1
+
+[Forms]
+File0 = dosrun.frm
+Count = 1
+
+[Settings]
+StartupForm = DosRun
diff --git a/src/test/dos/fixtures/dosrun/dosrun.frm b/src/test/dos/fixtures/dosrun/dosrun.frm
new file mode 100644
index 00000000..84702674
--- /dev/null
+++ b/src/test/dos/fixtures/dosrun/dosrun.frm
@@ -0,0 +1,44 @@
+VERSION DVX 1.00
+
+Begin Form DosRun
+ Caption = "DOS Run"
+ Layout = VBox
+ AutoSize = False
+ Resizable = False
+ Width = 200
+ Height = 60
+
+ Begin Label lblNote
+ Caption = "Runs TEST\DOSPROG.BAT"
+ Weight = 1
+ End
+End
+
+' 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.
+
+' Asks for TEST\DOSPROG.BAT with "hello" (dosprog.scr): DVX runs it once
+' this procedure has returned, after asking this program to close.
+
+SUB DosRun_Load
+ ShellRunDos "TEST\DOSPROG.BAT", "hello", "", False
+END SUB
diff --git a/src/test/dos/fixtures/parcp/parcp.frm b/src/test/dos/fixtures/parcp/parcp.frm
index cc21f93e..123c6c26 100644
--- a/src/test/dos/fixtures/parcp/parcp.frm
+++ b/src/test/dos/fixtures/parcp/parcp.frm
@@ -410,12 +410,19 @@ FUNCTION CheckSound() AS STRING
END IF
NEXT i
+ FOR i = 0 TO SoundModulesCount() - 1
+ IF SoundModulesLabel$(i) = "" THEN
+ what = "modules choice " + LTRIM$(STR$(i))
+ GOTO SoundOut
+ END IF
+ NEXT i
+
' Settings into a handle of their own (DVX.INI is not touched)
saved = PrefsCreate()
SoundSetEventFile saved, 0, "PARCP.WAV"
- SoundSettingsStore saved, SoundDriverAuto$(), 0, TEST_SND_VOLUME
+ SoundSettingsStore saved, SoundDriverAuto$(), 0, 1, TEST_SND_VOLUME
- IF SoundEventFile$(saved, 0) <> "PARCP.WAV" OR SoundSettingVolume(saved) <> TEST_SND_VOLUME OR SoundSettingDriver$(saved) <> SoundDriverAuto$() OR SoundSettingMidi(saved) <> 0 THEN
+ IF SoundEventFile$(saved, 0) <> "PARCP.WAV" OR SoundSettingVolume(saved) <> TEST_SND_VOLUME OR SoundSettingDriver$(saved) <> SoundDriverAuto$() OR SoundSettingMidi(saved) <> 0 OR SoundSettingModules(saved) <> 1 THEN
what = "sound settings"
END IF
diff --git a/src/test/dos/fixtures/prdraw/prdraw.dbp b/src/test/dos/fixtures/prdraw/prdraw.dbp
new file mode 100644
index 00000000..443b33b7
--- /dev/null
+++ b/src/test/dos/fixtures/prdraw/prdraw.dbp
@@ -0,0 +1,14 @@
+[Project]
+Name = Printer Drawing
+Author = Scott Duensing
+Publisher = Kangaroo Punch Studios
+Version = 1.00
+Copyright = Copyright 2026 Scott Duensing
+Description = DOS harness fixture: a page drawn with the Printer object (lines, boxes, circles, hatches, dashes, text, and a picture) printed where DVX.INI says
+
+[Forms]
+File0 = prdraw.frm
+Count = 1
+
+[Settings]
+StartupForm = PrDraw
diff --git a/src/test/dos/fixtures/prdraw/prdraw.frm b/src/test/dos/fixtures/prdraw/prdraw.frm
new file mode 100644
index 00000000..806aed36
--- /dev/null
+++ b/src/test/dos/fixtures/prdraw/prdraw.frm
@@ -0,0 +1,66 @@
+VERSION DVX 1.00
+
+Begin Form PrDraw
+ Caption = "Printer starting"
+ Layout = VBox
+ AutoSize = False
+ Resizable = False
+ Width = 480
+ Height = 120
+
+ Begin PictureBox picSource
+ Width = 32
+ Height = 32
+ End
+
+ Begin Timer tmrStart
+ Interval = 50
+ Enabled = True
+ End
+End
+
+
+
+' One page on the printer DVX.INI names, drawn with the Printer object:
+' a title and a line of text in two families, a thick rule, a box filled
+' solid, a circle cross-hatched, a dashed line and arc, and a picture of
+' four colored squares (drawn in picSource) stretched to 2 inches.
+Sub tmrStart_Timer ()
+ tmrStart.Enabled = False
+ picSource.BackColor = RGB(255, 255, 255)
+ picSource.Cls
+ picSource.Line (0, 0)-(15, 15), RGB(255, 0, 0), BF
+ picSource.Line (16, 0)-(31, 15), RGB(0, 255, 0), BF
+ picSource.Line (0, 16)-(15, 31), RGB(0, 0, 255), BF
+ picSource.Line (16, 16)-(31, 31), RGB(0, 0, 0), BF
+
+ Printer.ScaleMode = vbInches
+ Printer.FontName = "DejaVu Serif"
+ Printer.FontSize = 18
+ Printer.FontBold = True
+ Printer.CurrentX = 1
+ Printer.CurrentY = 1
+ Printer.Print "Printer Object"
+ Printer.FontBold = False
+ Printer.FontName = "DejaVu Sans"
+ Printer.FontSize = 11
+ Printer.CurrentX = 1
+ Printer.Print "Shapes, text, and a picture on "; Printer.DeviceName
+ Printer.DrawWidth = 3
+ Printer.Line (1, 2)-(7, 2), RGB(0, 0, 128)
+ Printer.DrawWidth = 1
+ Printer.FillStyle = vbFSSolid
+ Printer.FillColor = RGB(255, 200, 0)
+ Printer.Line (1, 2.5)-(3, 4), RGB(0, 0, 0), B
+ Printer.FillStyle = vbCross
+ Printer.FillColor = RGB(0, 128, 0)
+ Printer.Circle (4.5, 3.25), 0.75, RGB(0, 128, 0)
+ Printer.FillStyle = vbFSTransparent
+ Printer.DrawStyle = vbDash
+ Printer.Line (1, 4.5)-(7, 4.5), RGB(128, 0, 128)
+ Printer.Circle (6.25, 3.25), 0.6, RGB(200, 0, 0), 0, 3
+ Printer.DrawStyle = vbSolid
+ Printer.PaintPicture picSource, 1, 5, 2, 2
+ Printer.EndDoc
+ PrDraw.Caption = "Printer done"
+End Sub
diff --git a/src/test/dos/golden/awemod/86box/audio.json b/src/test/dos/golden/awemod/86box/audio.json
new file mode 100644
index 00000000..c745137d
--- /dev/null
+++ b/src/test/dos/golden/awemod/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 200,
+ "level": 0.0247,
+ "loudest": 2000,
+ "spectrum": [
+ 0.03408,
+ 0.00015,
+ 0.00017,
+ 0.00036
+ ]
+ },
+ {
+ "startMs": 520,
+ "lengthMs": 360,
+ "level": 0.04542,
+ "loudest": 440,
+ "spectrum": [
+ 0.00059,
+ 0.06303,
+ 0.00432,
+ 0.001
+ ]
+ },
+ {
+ "startMs": 1240,
+ "lengthMs": 360,
+ "level": 0.04529,
+ "loudest": 659,
+ "spectrum": [
+ 0.00015,
+ 0.00327,
+ 0.06196,
+ 0.00419
+ ]
+ },
+ {
+ "startMs": 1960,
+ "lengthMs": 360,
+ "level": 0.0449,
+ "loudest": 880,
+ "spectrum": [
+ 0.0009,
+ 0.00224,
+ 0.00318,
+ 0.05981
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/awerom/86box/audio.json b/src/test/dos/golden/awerom/86box/audio.json
new file mode 100644
index 00000000..a64318fc
--- /dev/null
+++ b/src/test/dos/golden/awerom/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 200,
+ "level": 0.02626,
+ "loudest": 2000,
+ "spectrum": [
+ 0.03638,
+ 0.00017,
+ 0.00019,
+ 0.00033
+ ]
+ },
+ {
+ "startMs": 480,
+ "lengthMs": 420,
+ "level": 0.07811,
+ "loudest": 440,
+ "spectrum": [
+ 0.00152,
+ 0.07818,
+ 0.00605,
+ 0.06582
+ ]
+ },
+ {
+ "startMs": 1180,
+ "lengthMs": 420,
+ "level": 0.0785,
+ "loudest": 659,
+ "spectrum": [
+ 0.02357,
+ 0.00574,
+ 0.07915,
+ 0.00558
+ ]
+ },
+ {
+ "startMs": 1880,
+ "lengthMs": 420,
+ "level": 0.07718,
+ "loudest": 880,
+ "spectrum": [
+ 0.00362,
+ 0.00186,
+ 0.00579,
+ 0.07902
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/awewave/86box/audio.json b/src/test/dos/golden/awewave/86box/audio.json
new file mode 100644
index 00000000..7f8ecca7
--- /dev/null
+++ b/src/test/dos/golden/awewave/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 200,
+ "level": 0.02503,
+ "loudest": 2000,
+ "spectrum": [
+ 0.03475,
+ 0.00017,
+ 0.00019,
+ 0.00039
+ ]
+ },
+ {
+ "startMs": 1020,
+ "lengthMs": 500,
+ "level": 0.04321,
+ "loudest": 440,
+ "spectrum": [
+ 0.00074,
+ 0.05295,
+ 0.00358,
+ 0.00895
+ ]
+ },
+ {
+ "startMs": 1720,
+ "lengthMs": 480,
+ "level": 0.04809,
+ "loudest": 659,
+ "spectrum": [
+ 0.02669,
+ 0.00385,
+ 0.05703,
+ 0.00377
+ ]
+ },
+ {
+ "startMs": 2420,
+ "lengthMs": 460,
+ "level": 0.04926,
+ "loudest": 880,
+ "spectrum": [
+ 0.0009,
+ 0.00125,
+ 0.0039,
+ 0.05662
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/dosprog/dosran.txt b/src/test/dos/golden/dosprog/dosran.txt
new file mode 100644
index 00000000..79b5d71f
--- /dev/null
+++ b/src/test/dos/golden/dosprog/dosran.txt
@@ -0,0 +1 @@
+ran hello
diff --git a/src/test/dos/golden/gus/86box/audio.json b/src/test/dos/golden/gus/86box/audio.json
new file mode 100644
index 00000000..a1467bb3
--- /dev/null
+++ b/src/test/dos/golden/gus/86box/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.06775,
+ "loudest": 2000,
+ "spectrum": [
+ 0.09192,
+ 0.00099,
+ 0.00192,
+ 0.00073,
+ 0.0012,
+ 0.00159
+ ]
+ },
+ {
+ "startMs": 520,
+ "lengthMs": 420,
+ "level": 0.0735,
+ "loudest": 440,
+ "spectrum": [
+ 0.00121,
+ 0.09461,
+ 0.00248,
+ 0.00698,
+ 0.00893,
+ 0.03025
+ ]
+ },
+ {
+ "startMs": 1220,
+ "lengthMs": 420,
+ "level": 0.04284,
+ "loudest": 880,
+ "spectrum": [
+ 0.00085,
+ 0.0015,
+ 0.05922,
+ 0.00398,
+ 0.00061,
+ 0.00127
+ ]
+ },
+ {
+ "startMs": 1960,
+ "lengthMs": 420,
+ "level": 0.06624,
+ "loudest": 660,
+ "spectrum": [
+ 0.01866,
+ 0.00649,
+ 0.00592,
+ 0.08661,
+ 0.0042,
+ 0.00165
+ ]
+ },
+ {
+ "startMs": 2640,
+ "lengthMs": 420,
+ "level": 0.06825,
+ "loudest": 330,
+ "spectrum": [
+ 0.00118,
+ 0.00756,
+ 0.00251,
+ 0.00425,
+ 0.08805,
+ 0.00207
+ ]
+ },
+ {
+ "startMs": 3300,
+ "lengthMs": 420,
+ "level": 0.06127,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00199,
+ 0.00181,
+ 0.00217,
+ 0.00175,
+ 0.00131,
+ 0.08235
+ ]
+ },
+ {
+ "startMs": 4000,
+ "lengthMs": 320,
+ "level": 0.06814,
+ "loudest": 330,
+ "spectrum": [
+ 0.00607,
+ 0.01621,
+ 0.01331,
+ 0.01601,
+ 0.01745,
+ 0.00842
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/gus/dosbox/audio.json b/src/test/dos/golden/gus/dosbox/audio.json
new file mode 100644
index 00000000..3a64376f
--- /dev/null
+++ b/src/test/dos/golden/gus/dosbox/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.08856,
+ "loudest": 2000,
+ "spectrum": [
+ 0.11654,
+ 0.00155,
+ 0.00212,
+ 0.00176,
+ 0.00194,
+ 0.00364
+ ]
+ },
+ {
+ "startMs": 520,
+ "lengthMs": 440,
+ "level": 0.11074,
+ "loudest": 440,
+ "spectrum": [
+ 0.00155,
+ 0.14015,
+ 0.00435,
+ 0.00955,
+ 0.01395,
+ 0.04442
+ ]
+ },
+ {
+ "startMs": 1260,
+ "lengthMs": 440,
+ "level": 0.06494,
+ "loudest": 880,
+ "spectrum": [
+ 0.00139,
+ 0.00222,
+ 0.08783,
+ 0.00626,
+ 0.00118,
+ 0.00211
+ ]
+ },
+ {
+ "startMs": 2000,
+ "lengthMs": 420,
+ "level": 0.10305,
+ "loudest": 660,
+ "spectrum": [
+ 0.02862,
+ 0.00949,
+ 0.00916,
+ 0.13482,
+ 0.00675,
+ 0.00255
+ ]
+ },
+ {
+ "startMs": 2720,
+ "lengthMs": 440,
+ "level": 0.10514,
+ "loudest": 330,
+ "spectrum": [
+ 0.00206,
+ 0.01296,
+ 0.00485,
+ 0.00584,
+ 0.12971,
+ 0.0026
+ ]
+ },
+ {
+ "startMs": 3460,
+ "lengthMs": 440,
+ "level": 0.09226,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00292,
+ 0.0027,
+ 0.00275,
+ 0.00315,
+ 0.00184,
+ 0.12242
+ ]
+ },
+ {
+ "startMs": 4200,
+ "lengthMs": 340,
+ "level": 0.09964,
+ "loudest": 330,
+ "spectrum": [
+ 0.00814,
+ 0.02278,
+ 0.01584,
+ 0.0232,
+ 0.02338,
+ 0.01361
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/gusmod/86box/audio.json b/src/test/dos/golden/gusmod/86box/audio.json
new file mode 100644
index 00000000..a31b2574
--- /dev/null
+++ b/src/test/dos/golden/gusmod/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.06374,
+ "loudest": 2000,
+ "spectrum": [
+ 0.08554,
+ 0.00211,
+ 0.00084,
+ 0.00175
+ ]
+ },
+ {
+ "startMs": 480,
+ "lengthMs": 380,
+ "level": 0.02091,
+ "loudest": 440,
+ "spectrum": [
+ 0.00031,
+ 0.02846,
+ 0.0021,
+ 0.00045
+ ]
+ },
+ {
+ "startMs": 1200,
+ "lengthMs": 380,
+ "level": 0.02092,
+ "loudest": 659,
+ "spectrum": [
+ 0.00012,
+ 0.00144,
+ 0.02791,
+ 0.00202
+ ]
+ },
+ {
+ "startMs": 1920,
+ "lengthMs": 380,
+ "level": 0.02081,
+ "loudest": 880,
+ "spectrum": [
+ 0.00045,
+ 0.00108,
+ 0.00132,
+ 0.02679
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/gusmod/dosbox/audio.json b/src/test/dos/golden/gusmod/dosbox/audio.json
new file mode 100644
index 00000000..f7d2e789
--- /dev/null
+++ b/src/test/dos/golden/gusmod/dosbox/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.15614,
+ "loudest": 2000,
+ "spectrum": [
+ 0.20895,
+ 0.00242,
+ 0.00268,
+ 0.00401
+ ]
+ },
+ {
+ "startMs": 500,
+ "lengthMs": 380,
+ "level": 0.0702,
+ "loudest": 440,
+ "spectrum": [
+ 0.00091,
+ 0.09492,
+ 0.00678,
+ 0.00151
+ ]
+ },
+ {
+ "startMs": 1260,
+ "lengthMs": 380,
+ "level": 0.06989,
+ "loudest": 659,
+ "spectrum": [
+ 0.00027,
+ 0.005,
+ 0.09303,
+ 0.00665
+ ]
+ },
+ {
+ "startMs": 2000,
+ "lengthMs": 400,
+ "level": 0.0669,
+ "loudest": 880,
+ "spectrum": [
+ 0.00132,
+ 0.00344,
+ 0.00458,
+ 0.0821
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/guspat/86box/audio.json b/src/test/dos/golden/guspat/86box/audio.json
new file mode 100644
index 00000000..8876d2e4
--- /dev/null
+++ b/src/test/dos/golden/guspat/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.05491,
+ "loudest": 2000,
+ "spectrum": [
+ 0.07306,
+ 0.00074,
+ 0.00082,
+ 0.00195
+ ]
+ },
+ {
+ "startMs": 480,
+ "lengthMs": 400,
+ "level": 0.01578,
+ "loudest": 440,
+ "spectrum": [
+ 0.00016,
+ 0.02214,
+ 0.00145,
+ 0.00052
+ ]
+ },
+ {
+ "startMs": 1160,
+ "lengthMs": 420,
+ "level": 0.01532,
+ "loudest": 659,
+ "spectrum": [
+ 0.0002,
+ 0.0014,
+ 0.02125,
+ 0.00144
+ ]
+ },
+ {
+ "startMs": 1860,
+ "lengthMs": 420,
+ "level": 0.01519,
+ "loudest": 880,
+ "spectrum": [
+ 0.00032,
+ 0.00046,
+ 0.00141,
+ 0.02105
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/guspat/dosbox/audio.json b/src/test/dos/golden/guspat/dosbox/audio.json
new file mode 100644
index 00000000..5d71ffac
--- /dev/null
+++ b/src/test/dos/golden/guspat/dosbox/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 240,
+ "level": 0.13558,
+ "loudest": 2000,
+ "spectrum": [
+ 0.17834,
+ 0.00202,
+ 0.00265,
+ 0.00369
+ ]
+ },
+ {
+ "startMs": 480,
+ "lengthMs": 440,
+ "level": 0.05284,
+ "loudest": 440,
+ "spectrum": [
+ 0.0005,
+ 0.07101,
+ 0.00498,
+ 0.00183
+ ]
+ },
+ {
+ "startMs": 1220,
+ "lengthMs": 440,
+ "level": 0.05267,
+ "loudest": 659,
+ "spectrum": [
+ 0.00056,
+ 0.00489,
+ 0.07143,
+ 0.00482
+ ]
+ },
+ {
+ "startMs": 1960,
+ "lengthMs": 440,
+ "level": 0.05172,
+ "loudest": 880,
+ "spectrum": [
+ 0.00105,
+ 0.00194,
+ 0.00535,
+ 0.07062
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/guswave/86box/audio.json b/src/test/dos/golden/guswave/86box/audio.json
new file mode 100644
index 00000000..f3f3e355
--- /dev/null
+++ b/src/test/dos/golden/guswave/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 200,
+ "level": 0.05794,
+ "loudest": 2000,
+ "spectrum": [
+ 0.07972,
+ 0.00043,
+ 0.00119,
+ 0.00188
+ ]
+ },
+ {
+ "startMs": 520,
+ "lengthMs": 360,
+ "level": 0.0108,
+ "loudest": 440,
+ "spectrum": [
+ 0.00019,
+ 0.01346,
+ 0.00093,
+ 0.00221
+ ]
+ },
+ {
+ "startMs": 1200,
+ "lengthMs": 380,
+ "level": 0.01091,
+ "loudest": 659,
+ "spectrum": [
+ 0.00563,
+ 0.00086,
+ 0.01302,
+ 0.00085
+ ]
+ },
+ {
+ "startMs": 1880,
+ "lengthMs": 400,
+ "level": 0.01081,
+ "loudest": 880,
+ "spectrum": [
+ 0.00017,
+ 0.00026,
+ 0.00084,
+ 0.01272
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/guswave/dosbox/audio.json b/src/test/dos/golden/guswave/dosbox/audio.json
new file mode 100644
index 00000000..ddfe5ec1
--- /dev/null
+++ b/src/test/dos/golden/guswave/dosbox/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.09523,
+ "loudest": 2000,
+ "spectrum": [
+ 0.12723,
+ 0.0021,
+ 0.00144,
+ 0.00268
+ ]
+ },
+ {
+ "startMs": 560,
+ "lengthMs": 440,
+ "level": 0.03381,
+ "loudest": 440,
+ "spectrum": [
+ 0.00055,
+ 0.04161,
+ 0.00278,
+ 0.00668
+ ]
+ },
+ {
+ "startMs": 1280,
+ "lengthMs": 460,
+ "level": 0.03435,
+ "loudest": 659,
+ "spectrum": [
+ 0.01706,
+ 0.00293,
+ 0.04009,
+ 0.00281
+ ]
+ },
+ {
+ "startMs": 2020,
+ "lengthMs": 440,
+ "level": 0.03557,
+ "loudest": 880,
+ "spectrum": [
+ 0.00074,
+ 0.00088,
+ 0.00293,
+ 0.04236
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/guswt/86box/audio.json b/src/test/dos/golden/guswt/86box/audio.json
new file mode 100644
index 00000000..8acd3842
--- /dev/null
+++ b/src/test/dos/golden/guswt/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 200,
+ "level": 0.02435,
+ "loudest": 2000,
+ "spectrum": [
+ 0.03432,
+ 0.00025,
+ 0.00026,
+ 0.00049
+ ]
+ },
+ {
+ "startMs": 600,
+ "lengthMs": 500,
+ "level": 0.00985,
+ "loudest": 440,
+ "spectrum": [
+ 0.00016,
+ 0.01206,
+ 0.00082,
+ 0.00196
+ ]
+ },
+ {
+ "startMs": 1360,
+ "lengthMs": 520,
+ "level": 0.01014,
+ "loudest": 659,
+ "spectrum": [
+ 0.00534,
+ 0.00081,
+ 0.01207,
+ 0.00078
+ ]
+ },
+ {
+ "startMs": 2200,
+ "lengthMs": 500,
+ "level": 0.0103,
+ "loudest": 880,
+ "spectrum": [
+ 0.00016,
+ 0.00026,
+ 0.00082,
+ 0.01185
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/guswt/dosbox/audio.json b/src/test/dos/golden/guswt/dosbox/audio.json
new file mode 100644
index 00000000..838ee741
--- /dev/null
+++ b/src/test/dos/golden/guswt/dosbox/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.04937,
+ "loudest": 2000,
+ "spectrum": [
+ 0.06782,
+ 0.00057,
+ 0.0006,
+ 0.00107
+ ]
+ },
+ {
+ "startMs": 560,
+ "lengthMs": 460,
+ "level": 0.03255,
+ "loudest": 440,
+ "spectrum": [
+ 0.0005,
+ 0.0406,
+ 0.00278,
+ 0.00659
+ ]
+ },
+ {
+ "startMs": 1260,
+ "lengthMs": 480,
+ "level": 0.03316,
+ "loudest": 659,
+ "spectrum": [
+ 0.01635,
+ 0.00298,
+ 0.0391,
+ 0.00271
+ ]
+ },
+ {
+ "startMs": 1980,
+ "lengthMs": 500,
+ "level": 0.03228,
+ "loudest": 880,
+ "spectrum": [
+ 0.00065,
+ 0.00102,
+ 0.00249,
+ 0.03755
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/lptcovox/86box/audio.json b/src/test/dos/golden/lptcovox/86box/audio.json
new file mode 100644
index 00000000..3df75496
--- /dev/null
+++ b/src/test/dos/golden/lptcovox/86box/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.07822,
+ "loudest": 2000,
+ "spectrum": [
+ 0.09952,
+ 0.00397,
+ 0.00591,
+ 0.00379,
+ 0.00412,
+ 0.00351
+ ]
+ },
+ {
+ "startMs": 540,
+ "lengthMs": 420,
+ "level": 0.09116,
+ "loudest": 440,
+ "spectrum": [
+ 0.00179,
+ 0.11732,
+ 0.00353,
+ 0.00808,
+ 0.01183,
+ 0.03664
+ ]
+ },
+ {
+ "startMs": 1200,
+ "lengthMs": 420,
+ "level": 0.05295,
+ "loudest": 880,
+ "spectrum": [
+ 0.0011,
+ 0.00201,
+ 0.07267,
+ 0.00525,
+ 0.0008,
+ 0.00146
+ ]
+ },
+ {
+ "startMs": 1960,
+ "lengthMs": 420,
+ "level": 0.08074,
+ "loudest": 660,
+ "spectrum": [
+ 0.02067,
+ 0.00775,
+ 0.00776,
+ 0.10648,
+ 0.00522,
+ 0.00233
+ ]
+ },
+ {
+ "startMs": 2640,
+ "lengthMs": 420,
+ "level": 0.08509,
+ "loudest": 330,
+ "spectrum": [
+ 0.00192,
+ 0.01027,
+ 0.00364,
+ 0.00492,
+ 0.1091,
+ 0.00237
+ ]
+ },
+ {
+ "startMs": 3300,
+ "lengthMs": 420,
+ "level": 0.07481,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00262,
+ 0.00269,
+ 0.00343,
+ 0.00341,
+ 0.00247,
+ 0.09852
+ ]
+ },
+ {
+ "startMs": 4000,
+ "lengthMs": 320,
+ "level": 0.08399,
+ "loudest": 440,
+ "spectrum": [
+ 0.00815,
+ 0.02331,
+ 0.01623,
+ 0.01948,
+ 0.01586,
+ 0.01076
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/lptcovox/dosbox/audio.json b/src/test/dos/golden/lptcovox/dosbox/audio.json
new file mode 100644
index 00000000..c59db7e7
--- /dev/null
+++ b/src/test/dos/golden/lptcovox/dosbox/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 240,
+ "level": 0.13205,
+ "loudest": 2000,
+ "spectrum": [
+ 0.17046,
+ 0.00225,
+ 0.00369,
+ 0.00321,
+ 0.00239,
+ 0.00432
+ ]
+ },
+ {
+ "startMs": 520,
+ "lengthMs": 440,
+ "level": 0.17894,
+ "loudest": 440,
+ "spectrum": [
+ 0.00385,
+ 0.22412,
+ 0.00655,
+ 0.0161,
+ 0.02431,
+ 0.07227
+ ]
+ },
+ {
+ "startMs": 1260,
+ "lengthMs": 440,
+ "level": 0.10489,
+ "loudest": 880,
+ "spectrum": [
+ 0.00217,
+ 0.0035,
+ 0.1394,
+ 0.00969,
+ 0.00185,
+ 0.00313
+ ]
+ },
+ {
+ "startMs": 2000,
+ "lengthMs": 440,
+ "level": 0.15971,
+ "loudest": 660,
+ "spectrum": [
+ 0.04278,
+ 0.01532,
+ 0.01477,
+ 0.20645,
+ 0.01042,
+ 0.00389
+ ]
+ },
+ {
+ "startMs": 2740,
+ "lengthMs": 420,
+ "level": 0.17254,
+ "loudest": 330,
+ "spectrum": [
+ 0.00306,
+ 0.01997,
+ 0.00716,
+ 0.00999,
+ 0.22042,
+ 0.00445
+ ]
+ },
+ {
+ "startMs": 3460,
+ "lengthMs": 440,
+ "level": 0.14902,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00482,
+ 0.00477,
+ 0.00573,
+ 0.00534,
+ 0.00353,
+ 0.19638
+ ]
+ },
+ {
+ "startMs": 4200,
+ "lengthMs": 340,
+ "level": 0.16269,
+ "loudest": 440,
+ "spectrum": [
+ 0.01644,
+ 0.04366,
+ 0.03618,
+ 0.04198,
+ 0.0339,
+ 0.02029
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/lptdss/86box/audio.json b/src/test/dos/golden/lptdss/86box/audio.json
new file mode 100644
index 00000000..8cac1a0f
--- /dev/null
+++ b/src/test/dos/golden/lptdss/86box/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.06781,
+ "loudest": 2000,
+ "spectrum": [
+ 0.08542,
+ 0.00115,
+ 0.00489,
+ 0.00152,
+ 0.00176,
+ 0.00207
+ ]
+ },
+ {
+ "startMs": 500,
+ "lengthMs": 420,
+ "level": 0.09022,
+ "loudest": 440,
+ "spectrum": [
+ 0.00195,
+ 0.11705,
+ 0.00328,
+ 0.00808,
+ 0.01144,
+ 0.03509
+ ]
+ },
+ {
+ "startMs": 1200,
+ "lengthMs": 420,
+ "level": 0.05141,
+ "loudest": 880,
+ "spectrum": [
+ 0.00108,
+ 0.00182,
+ 0.07173,
+ 0.00498,
+ 0.00076,
+ 0.00171
+ ]
+ },
+ {
+ "startMs": 1940,
+ "lengthMs": 420,
+ "level": 0.07963,
+ "loudest": 660,
+ "spectrum": [
+ 0.01899,
+ 0.00751,
+ 0.00761,
+ 0.10595,
+ 0.00486,
+ 0.0017
+ ]
+ },
+ {
+ "startMs": 2640,
+ "lengthMs": 420,
+ "level": 0.08437,
+ "loudest": 330,
+ "spectrum": [
+ 0.00115,
+ 0.00947,
+ 0.00426,
+ 0.00485,
+ 0.10911,
+ 0.00218
+ ]
+ },
+ {
+ "startMs": 3300,
+ "lengthMs": 420,
+ "level": 0.07142,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00244,
+ 0.00353,
+ 0.00275,
+ 0.0025,
+ 0.00421,
+ 0.09561
+ ]
+ },
+ {
+ "startMs": 4000,
+ "lengthMs": 320,
+ "level": 0.08258,
+ "loudest": 440,
+ "spectrum": [
+ 0.00746,
+ 0.02262,
+ 0.0178,
+ 0.01756,
+ 0.01638,
+ 0.01038
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/lptdss/dosbox/audio.json b/src/test/dos/golden/lptdss/dosbox/audio.json
new file mode 100644
index 00000000..bce4c2bd
--- /dev/null
+++ b/src/test/dos/golden/lptdss/dosbox/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.14797,
+ "loudest": 2000,
+ "spectrum": [
+ 0.18154,
+ 0.00334,
+ 0.01315,
+ 0.00397,
+ 0.00417,
+ 0.00517
+ ]
+ },
+ {
+ "startMs": 520,
+ "lengthMs": 440,
+ "level": 0.17947,
+ "loudest": 440,
+ "spectrum": [
+ 0.00339,
+ 0.22489,
+ 0.00653,
+ 0.01702,
+ 0.02384,
+ 0.06852
+ ]
+ },
+ {
+ "startMs": 1260,
+ "lengthMs": 440,
+ "level": 0.10196,
+ "loudest": 880,
+ "spectrum": [
+ 0.00259,
+ 0.00329,
+ 0.13812,
+ 0.00937,
+ 0.00175,
+ 0.00326
+ ]
+ },
+ {
+ "startMs": 2000,
+ "lengthMs": 440,
+ "level": 0.15761,
+ "loudest": 660,
+ "spectrum": [
+ 0.03724,
+ 0.01663,
+ 0.01558,
+ 0.20348,
+ 0.01177,
+ 0.00415
+ ]
+ },
+ {
+ "startMs": 2720,
+ "lengthMs": 440,
+ "level": 0.16603,
+ "loudest": 330,
+ "spectrum": [
+ 0.00462,
+ 0.02128,
+ 0.00818,
+ 0.01037,
+ 0.20401,
+ 0.00554
+ ]
+ },
+ {
+ "startMs": 3460,
+ "lengthMs": 440,
+ "level": 0.14546,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00464,
+ 0.00835,
+ 0.00638,
+ 0.00587,
+ 0.00739,
+ 0.18603
+ ]
+ },
+ {
+ "startMs": 4180,
+ "lengthMs": 360,
+ "level": 0.15196,
+ "loudest": 440,
+ "spectrum": [
+ 0.01409,
+ 0.03593,
+ 0.02529,
+ 0.0328,
+ 0.03127,
+ 0.02188
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/lptst86/86box/audio.json b/src/test/dos/golden/lptst86/86box/audio.json
new file mode 100644
index 00000000..f938cd30
--- /dev/null
+++ b/src/test/dos/golden/lptst86/86box/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.0642,
+ "loudest": 2000,
+ "spectrum": [
+ 0.08045,
+ 0.00548,
+ 0.00524,
+ 0.0034,
+ 0.00199,
+ 0.00216
+ ]
+ },
+ {
+ "startMs": 480,
+ "lengthMs": 420,
+ "level": 0.08822,
+ "loudest": 440,
+ "spectrum": [
+ 0.0017,
+ 0.11638,
+ 0.0031,
+ 0.00812,
+ 0.01057,
+ 0.034
+ ]
+ },
+ {
+ "startMs": 1180,
+ "lengthMs": 420,
+ "level": 0.05078,
+ "loudest": 880,
+ "spectrum": [
+ 0.00113,
+ 0.00173,
+ 0.07047,
+ 0.00498,
+ 0.00076,
+ 0.00153
+ ]
+ },
+ {
+ "startMs": 1960,
+ "lengthMs": 420,
+ "level": 0.07832,
+ "loudest": 660,
+ "spectrum": [
+ 0.01744,
+ 0.00745,
+ 0.00744,
+ 0.10467,
+ 0.00546,
+ 0.0025
+ ]
+ },
+ {
+ "startMs": 2620,
+ "lengthMs": 420,
+ "level": 0.08358,
+ "loudest": 330,
+ "spectrum": [
+ 0.00152,
+ 0.01036,
+ 0.00342,
+ 0.00534,
+ 0.10867,
+ 0.0021
+ ]
+ },
+ {
+ "startMs": 3300,
+ "lengthMs": 420,
+ "level": 0.06846,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00185,
+ 0.00216,
+ 0.00243,
+ 0.00332,
+ 0.00209,
+ 0.0917
+ ]
+ },
+ {
+ "startMs": 4000,
+ "lengthMs": 320,
+ "level": 0.07942,
+ "loudest": 330,
+ "spectrum": [
+ 0.00566,
+ 0.02065,
+ 0.01286,
+ 0.02026,
+ 0.02075,
+ 0.00963
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/lptston/dosbox/audio.json b/src/test/dos/golden/lptston/dosbox/audio.json
new file mode 100644
index 00000000..18fdcd9f
--- /dev/null
+++ b/src/test/dos/golden/lptston/dosbox/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 240,
+ "level": 0.14199,
+ "loudest": 2000,
+ "spectrum": [
+ 0.17474,
+ 0.00357,
+ 0.00375,
+ 0.00403,
+ 0.00309,
+ 0.00552
+ ]
+ },
+ {
+ "startMs": 540,
+ "lengthMs": 440,
+ "level": 0.17877,
+ "loudest": 440,
+ "spectrum": [
+ 0.00436,
+ 0.22634,
+ 0.0063,
+ 0.01589,
+ 0.02178,
+ 0.07162
+ ]
+ },
+ {
+ "startMs": 1280,
+ "lengthMs": 440,
+ "level": 0.10461,
+ "loudest": 880,
+ "spectrum": [
+ 0.00185,
+ 0.00419,
+ 0.14149,
+ 0.01009,
+ 0.0019,
+ 0.0036
+ ]
+ },
+ {
+ "startMs": 2000,
+ "lengthMs": 440,
+ "level": 0.1603,
+ "loudest": 660,
+ "spectrum": [
+ 0.04442,
+ 0.01447,
+ 0.01448,
+ 0.20645,
+ 0.01027,
+ 0.0048
+ ]
+ },
+ {
+ "startMs": 2740,
+ "lengthMs": 460,
+ "level": 0.15941,
+ "loudest": 330,
+ "spectrum": [
+ 0.00328,
+ 0.01831,
+ 0.00687,
+ 0.01037,
+ 0.20004,
+ 0.00477
+ ]
+ },
+ {
+ "startMs": 3480,
+ "lengthMs": 440,
+ "level": 0.14975,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00475,
+ 0.00443,
+ 0.00539,
+ 0.00512,
+ 0.00377,
+ 0.19665
+ ]
+ },
+ {
+ "startMs": 4220,
+ "lengthMs": 340,
+ "level": 0.16183,
+ "loudest": 440,
+ "spectrum": [
+ 0.01679,
+ 0.04285,
+ 0.03553,
+ 0.03457,
+ 0.04073,
+ 0.02275
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/musmod/86box/audio.json b/src/test/dos/golden/musmod/86box/audio.json
index 9e4f9cd9..2b240e14 100644
--- a/src/test/dos/golden/musmod/86box/audio.json
+++ b/src/test/dos/golden/musmod/86box/audio.json
@@ -10,37 +10,49 @@
{
"startMs": 0,
"lengthMs": 220,
- "level": 0.02665,
+ "level": 0.02577,
"loudest": 2000,
"spectrum": [
- 0.03692,
+ 0.03484,
0.00042,
- 0.00043,
+ 0.00039,
0.00078
]
},
{
- "startMs": 540,
- "lengthMs": 380,
- "level": 0.04395,
+ "startMs": 560,
+ "lengthMs": 360,
+ "level": 0.04537,
"loudest": 440,
"spectrum": [
- 0.0006,
- 0.0599,
- 0.0045,
+ 0.00064,
+ 0.06311,
+ 0.00446,
0.001
]
},
+ {
+ "startMs": 1280,
+ "lengthMs": 360,
+ "level": 0.04506,
+ "loudest": 659,
+ "spectrum": [
+ 0.00012,
+ 0.00332,
+ 0.06183,
+ 0.00439
+ ]
+ },
{
"startMs": 1980,
"lengthMs": 380,
- "level": 0.04317,
+ "level": 0.04251,
"loudest": 880,
"spectrum": [
- 0.00089,
- 0.00209,
- 0.00344,
- 0.05669
+ 0.00087,
+ 0.00204,
+ 0.00306,
+ 0.05662
]
}
]
diff --git a/src/test/dos/golden/player/86box/audio.json b/src/test/dos/golden/player/86box/audio.json
new file mode 100644
index 00000000..316266a8
--- /dev/null
+++ b/src/test/dos/golden/player/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 320,
+ "level": 0.0728,
+ "loudest": 2000,
+ "spectrum": [
+ 0.10019,
+ 0.00102,
+ 0.00168,
+ 0.00215
+ ]
+ },
+ {
+ "startMs": 660,
+ "lengthMs": 380,
+ "level": 0.08192,
+ "loudest": 440,
+ "spectrum": [
+ 0.00097,
+ 0.11334,
+ 0.00796,
+ 0.00282
+ ]
+ },
+ {
+ "startMs": 1380,
+ "lengthMs": 380,
+ "level": 0.08138,
+ "loudest": 659,
+ "spectrum": [
+ 0.00088,
+ 0.0078,
+ 0.11258,
+ 0.00812
+ ]
+ },
+ {
+ "startMs": 2100,
+ "lengthMs": 380,
+ "level": 0.08062,
+ "loudest": 880,
+ "spectrum": [
+ 0.00157,
+ 0.00295,
+ 0.0081,
+ 0.11148
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/player/dosbox/audio.json b/src/test/dos/golden/player/dosbox/audio.json
new file mode 100644
index 00000000..02fcf2da
--- /dev/null
+++ b/src/test/dos/golden/player/dosbox/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 240,
+ "level": 0.11082,
+ "loudest": 2000,
+ "spectrum": [
+ 0.15102,
+ 0.00113,
+ 0.00109,
+ 0.00208
+ ]
+ },
+ {
+ "startMs": 600,
+ "lengthMs": 360,
+ "level": 0.12726,
+ "loudest": 440,
+ "spectrum": [
+ 0.00132,
+ 0.178,
+ 0.01205,
+ 0.00418
+ ]
+ },
+ {
+ "startMs": 1300,
+ "lengthMs": 380,
+ "level": 0.12126,
+ "loudest": 659,
+ "spectrum": [
+ 0.00135,
+ 0.01154,
+ 0.16756,
+ 0.0121
+ ]
+ },
+ {
+ "startMs": 2020,
+ "lengthMs": 380,
+ "level": 0.11975,
+ "loudest": 880,
+ "spectrum": [
+ 0.00243,
+ 0.00434,
+ 0.01305,
+ 0.16058
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/prdjcol/prdjcol.prn b/src/test/dos/golden/prdjcol/prdjcol.prn
new file mode 100644
index 00000000..ec4bf0cf
Binary files /dev/null and b/src/test/dos/golden/prdjcol/prdjcol.prn differ
diff --git a/src/test/dos/golden/prlj300/prlj300.prn b/src/test/dos/golden/prlj300/prlj300.prn
new file mode 100644
index 00000000..9cac0acd
Binary files /dev/null and b/src/test/dos/golden/prlj300/prlj300.prn differ
diff --git a/src/test/dos/golden/prps/prps.prn b/src/test/dos/golden/prps/prps.prn
new file mode 100644
index 00000000..0d33f281
--- /dev/null
+++ b/src/test/dos/golden/prps/prps.prn
@@ -0,0 +1,104 @@
+%!PS
+%%Creator: DVX
+%%LanguageLevel: 2
+%%EndComments
+/dvxPage { currentpagedevice /PageSize get aload pop 36 sub exch 576 sub 2 div exch } bind def
+/dvxBegin { /dvxDpi exch def gsave dvxPage translate 72 dvxDpi div dup neg scale } bind def
+/dvxData { /dvxA currentfile /ASCII85Decode filter def /dvxR dvxA /RunLengthDecode filter def } bind def
+/dvxDone { dvxR flushfile dvxA flushfile } bind def
+/dvxMask { /dvxH exch def /dvxW exch def gsave 0 exch translate dvxW dvxH scale dvxData dvxW dvxH true [dvxW 0 0 dvxH 0 0] dvxR imagemask dvxDone grestore } bind def
+/dvxColor { /dvxH exch def /dvxW exch def gsave 0 exch translate dvxW dvxH scale dvxData dvxW dvxH 1 [dvxW 0 0 dvxH 0 0] dvxR false 4 colorimage dvxDone grestore } bind def
+/dvxRe { findfont dup length dict begin { 1 index /FID ne { def } { pop pop } ifelse } forall /Encoding ISOLatin1Encoding def currentdict end definefont pop } bind def
+/DvxCourier /Courier dvxRe /dvxF /DvxCourier findfont def
+/dvxTop { dvxPage /dvxY exch def /dvxX exch def dvxF dvxSize scalefont setfont } bind def
+/L { dvxX dvxY dvxLead sub dup /dvxY exch def moveto show } bind def
+/dvxM matrix def
+/dvxL { 4 2 roll moveto lineto stroke } bind def
+/dvxEA { dvxM currentmatrix pop 6 4 roll translate 4 2 roll scale 0 0 1 5 -2 roll neg exch neg exch arcn dvxM setmatrix } bind def
+/dvxE { 0 360 dvxEA closepath } bind def
+/dvxBox { 4 2 roll moveto 1 index 0 rlineto 0 exch rlineto neg 0 rlineto closepath } bind def
+/dvxD { 0 360 arc fill } bind def
+/dvxHz { /dvxQ exch def /dvxP exch def { 0.5 add dvxP exch moveto dvxQ dvxP sub 0 rlineto } for } bind def
+/dvxVt { /dvxQ exch def /dvxP exch def { 0.5 add dvxP moveto 0 dvxQ dvxP sub rlineto } for } bind def
+/dvxUp { /dvxQ exch def /dvxP exch def { 1 add dup dvxP sub dvxP moveto dvxQ sub dvxQ lineto } for } bind def
+/dvxDn { /dvxQ exch def /dvxP exch def { dup dvxP add dvxP moveto dvxQ add dvxQ lineto } for } bind def
+/dvxSF { /dvxSy exch def /dvxSx exch def findfont [dvxSx 0 0 dvxSy neg 0 0] makefont setfont } bind def
+/dvxT { moveto show } bind def
+/dvxImg { /dvxC exch def /dvxH exch def /dvxW exch def gsave 4 2 roll translate scale dvxData dvxW dvxH 8 [dvxW 0 0 dvxH 0 0] dvxR dvxC 1 eq { image } { false 3 colorimage } ifelse dvxDone grestore } bind def
+%%EndProlog
+300 dvxBegin
+0 0 2400 3000 rectclip 1 setlinecap 1 setlinejoin
+/DvxF5 /Times-Bold dvxRe
+/DvxF5 75 75 dvxSF
+(Printer Object) 300 362.475 dvxT
+/DvxF0 /Helvetica dvxRe
+/DvxF0 45.8333 45.8333 dvxSF
+(Shapes, text, and a picture on ) 300 429.21 dvxT
+(PostScript \(color\)) 922 429.21 dvxT
+0 0 0.502 setrgbcolor
+3 setlinewidth
+300.5 600.5 2100.5 600.5 dvxL
+1 0.784 0 setrgbcolor
+300 750 601 451 dvxBox fill
+0 0 0 setrgbcolor
+1 setlinewidth
+300.5 750.5 900.5 750.5 dvxL
+900.5 751.5 900.5 1200.5 dvxL
+899.5 1200.5 300.5 1200.5 dvxL
+300.5 1199.5 300.5 751.5 dvxL
+0 0.502 0 setrgbcolor
+gsave 1350.5 975.5 225.5 225.5 dvxE clip newpath
+752 8 1200 1125 1576 dvxHz
+1128 8 1575 750 1201 dvxVt
+1 setlinewidth 0 setlinecap stroke grestore
+1350.5 975.5 225 225 dvxE stroke
+0.502 0 0.502 setrgbcolor
+0 setlinecap
+[12 4 ] 0 setdash
+300 1350.5 2101 1350.5 dvxL
+0.784 0 0 setrgbcolor
+[12 4 ] 0 setdash
+1875.5 975.5 180 180 0 171.887 dvxEA stroke
+300 1500 600 600 32 32 3 dvxImg
+/cPeNrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*&u!&"9O!<3$!rr<'!
+!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!%n3N!<3$!rr<'!!!*$!!<3$!rr<'!
+!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!rW"SN!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!
+rr<'!!!*$!!<3$!rr=YN!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!
+s8E!Nrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$Nrr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<<#u0)knOrr<'!!!*$!!<3$!rr<'!
+!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!/cPeNrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!
+!!*$!!<3$!rr<'!!!*&u!&"9O!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!
+rr<'!!%n3N!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!rW"SN!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr=YN!!*$!!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!s8E!Nrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!
+!!*$!!<3$!rr<'!!!*$!!<3$Nrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!
+!!*$!!<<#u0)knOrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!/cPeN
+rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*&u!&"9O!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!%n3N!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!rW"SN!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!
+!!*$!!<3$!rr=YN!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!s8E!N
+rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$Nrr<'!!!*$!!<3$!
+rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<<#u0)knOrr<'!!!*$!!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!/cPeNrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*&u!&"9O!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!
+!%n3N!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!rW"SN!!*$!!<3$!
+rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr=YN!!*$!!<3$!rr<'!!!*$!!<3$!
+rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!s8E!Nrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$Nrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!
+!<<#u0)knOrr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!0)u"P!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!s3Ug"!!*$!!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!!<3$!rr<'!d/Yg"!<3$!rr<'!!!*$!!<3$!rr<'!!!*$!
+!<3$!rr<'!!!*$!!<3$!rr<'!!!*&H!&"
+grestore showpage
+%%EOF
diff --git a/src/test/dos/golden/softwave/86box/audio.json b/src/test/dos/golden/softwave/86box/audio.json
new file mode 100644
index 00000000..4c3cd570
--- /dev/null
+++ b/src/test/dos/golden/softwave/86box/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 200,
+ "level": 0.02681,
+ "loudest": 2000,
+ "spectrum": [
+ 0.03688,
+ 0.00015,
+ 0.00019,
+ 0.00033
+ ]
+ },
+ {
+ "startMs": 720,
+ "lengthMs": 480,
+ "level": 0.02958,
+ "loudest": 440,
+ "spectrum": [
+ 0.00046,
+ 0.03678,
+ 0.00246,
+ 0.00588
+ ]
+ },
+ {
+ "startMs": 1420,
+ "lengthMs": 480,
+ "level": 0.03084,
+ "loudest": 659,
+ "spectrum": [
+ 0.0165,
+ 0.00247,
+ 0.03693,
+ 0.00248
+ ]
+ },
+ {
+ "startMs": 2120,
+ "lengthMs": 480,
+ "level": 0.03111,
+ "loudest": 880,
+ "spectrum": [
+ 0.00055,
+ 0.00084,
+ 0.00251,
+ 0.03648
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/softwave/dosbox/audio.json b/src/test/dos/golden/softwave/dosbox/audio.json
new file mode 100644
index 00000000..901781ca
--- /dev/null
+++ b/src/test/dos/golden/softwave/dosbox/audio.json
@@ -0,0 +1,59 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 659,
+ 880
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 200,
+ "level": 0.05695,
+ "loudest": 2000,
+ "spectrum": [
+ 0.07839,
+ 0.0002,
+ 0.00029,
+ 0.00058
+ ]
+ },
+ {
+ "startMs": 760,
+ "lengthMs": 480,
+ "level": 0.04438,
+ "loudest": 440,
+ "spectrum": [
+ 0.00072,
+ 0.05534,
+ 0.00376,
+ 0.00899
+ ]
+ },
+ {
+ "startMs": 1460,
+ "lengthMs": 480,
+ "level": 0.04618,
+ "loudest": 659,
+ "spectrum": [
+ 0.02449,
+ 0.00372,
+ 0.0551,
+ 0.0037
+ ]
+ },
+ {
+ "startMs": 2160,
+ "lengthMs": 480,
+ "level": 0.04611,
+ "loudest": 880,
+ "spectrum": [
+ 0.0008,
+ 0.00119,
+ 0.00364,
+ 0.05041
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/golden/wss/86box/audio.json b/src/test/dos/golden/wss/86box/audio.json
new file mode 100644
index 00000000..8d337dc2
--- /dev/null
+++ b/src/test/dos/golden/wss/86box/audio.json
@@ -0,0 +1,111 @@
+{
+ "windowMs": 20,
+ "freqs": [
+ 2000,
+ 440,
+ 880,
+ 660,
+ 330,
+ 1320
+ ],
+ "sounds": [
+ {
+ "startMs": 0,
+ "lengthMs": 220,
+ "level": 0.13786,
+ "loudest": 2000,
+ "spectrum": [
+ 0.18281,
+ 0.00445,
+ 0.00266,
+ 0.00127,
+ 0.00268,
+ 0.00321
+ ]
+ },
+ {
+ "startMs": 480,
+ "lengthMs": 420,
+ "level": 0.1818,
+ "loudest": 440,
+ "spectrum": [
+ 0.00255,
+ 0.23549,
+ 0.00632,
+ 0.01685,
+ 0.02205,
+ 0.0752
+ ]
+ },
+ {
+ "startMs": 1180,
+ "lengthMs": 420,
+ "level": 0.10675,
+ "loudest": 880,
+ "spectrum": [
+ 0.00226,
+ 0.00363,
+ 0.14743,
+ 0.01016,
+ 0.00144,
+ 0.00364
+ ]
+ },
+ {
+ "startMs": 1920,
+ "lengthMs": 420,
+ "level": 0.16404,
+ "loudest": 660,
+ "spectrum": [
+ 0.04626,
+ 0.01499,
+ 0.01568,
+ 0.21563,
+ 0.01059,
+ 0.00371
+ ]
+ },
+ {
+ "startMs": 2620,
+ "lengthMs": 420,
+ "level": 0.17278,
+ "loudest": 330,
+ "spectrum": [
+ 0.00289,
+ 0.02086,
+ 0.00683,
+ 0.01049,
+ 0.22091,
+ 0.00475
+ ]
+ },
+ {
+ "startMs": 3300,
+ "lengthMs": 420,
+ "level": 0.15361,
+ "loudest": 1320,
+ "spectrum": [
+ 0.00518,
+ 0.00441,
+ 0.00452,
+ 0.00568,
+ 0.00348,
+ 0.20646
+ ]
+ },
+ {
+ "startMs": 4000,
+ "lengthMs": 320,
+ "level": 0.16767,
+ "loudest": 660,
+ "spectrum": [
+ 0.01558,
+ 0.04174,
+ 0.02895,
+ 0.04341,
+ 0.04062,
+ 0.02266
+ ]
+ }
+ ]
+}
diff --git a/src/test/dos/mkMedia.py b/src/test/dos/mkMedia.py
new file mode 100755
index 00000000..9d83bd32
--- /dev/null
+++ b/src/test/dos/mkMedia.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python3
+
+# 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.
+
+# mkMedia.py -- Media Player's test files for runDos.sh (player.scr),
+# written into the staged TEST folder as each script is staged:
+#
+# PLAYER.WAV 22,050 Hz 16-bit mono: a 2000 Hz sync tone, then A4, E5,
+# and A5 (SYNC_MS, then NOTE_MS each, GAP_MS apart), the
+# sounds player.scr's # AUDIO line lists
+# PLAYER.MOD a ProTracker module titled MOD_TITLE: a looping sine
+# sample playing C-3, E-3, and G-3 eight rows apart, then the
+# song ends
+# PLAYER.MID a type 0 MIDI file whose track is named MIDI_TITLE: C4, E4,
+# and G4 on the piano, a beat each at 120 beats a minute
+# PLAYER.VOC a Creative Voice File: a second of a 660 Hz sine, 8-bit at
+# VOC_RATE
+#
+# mkMedia.py FOLDER
+
+import math
+import struct
+import sys
+
+WAV_RATE = 22050
+WAV_AMP = 12000
+SYNC_HZ = 2000
+SYNC_MS = 300
+NOTE_MS = 360
+GAP_MS = 360
+NOTES_HZ = (440, 659, 880)
+FADE_MS = 5 # each sound fades in and out (no clicks)
+
+MOD_TITLE = "Player Test Module"
+MOD_SAMPLE = 32 # bytes of the looping sine
+MOD_AMP = 100
+MOD_ROWS = 64
+MOD_CHANNELS = 4
+MOD_NOTE_GAP = 8 # rows between the notes
+MOD_PERIODS = (428, 339, 285) # C-3, E-3, G-3
+MOD_VOLUME = 64
+
+MIDI_TITLE = "Player Test MIDI"
+MIDI_DIVISION = 96
+MIDI_NOTES = (60, 64, 67)
+MIDI_VELOCITY = 100
+MIDI_PIANO = 0
+
+VOC_RATE = 10000 # a time constant of exactly 156 (256 - 1,000,000 / rate)
+VOC_HZ = 660
+VOC_AMP = 60
+VOC_VERSION = 0x010A
+VOC_CHECK = 0x1234
+VOC_HEADER = 26
+
+
+def tone(rate, hz, ms, amp):
+ count = rate * ms // 1000
+ fade = rate * FADE_MS // 1000
+ out = []
+
+ for i in range(count):
+ level = min(1.0, i / fade, (count - 1 - i) / fade)
+ out.append(int(round(amp * level * math.sin(2.0 * math.pi * hz * i / rate))))
+
+ return out
+
+
+def silence(rate, ms):
+ return [0] * (rate * ms // 1000)
+
+
+def writeWav(path):
+ frames = tone(WAV_RATE, SYNC_HZ, SYNC_MS, WAV_AMP) + silence(WAV_RATE, GAP_MS)
+
+ for hz in NOTES_HZ:
+ frames += tone(WAV_RATE, hz, NOTE_MS, WAV_AMP) + silence(WAV_RATE, GAP_MS)
+
+ data = struct.pack("<%dh" % len(frames), *frames)
+ head = b"RIFF" + struct.pack("HBBHH", words, 0, MOD_VOLUME, 0, words)
+ header += (b"\0" * 22 + struct.pack(">HBBHH", 0, 0, 0, 0, 1)) * 30
+ header += bytes([1, 127]) + bytes([0] * 128) + b"M.K."
+
+ pattern = bytearray(MOD_ROWS * MOD_CHANNELS * 4)
+
+ for n, period in enumerate(MOD_PERIODS):
+ at = n * MOD_NOTE_GAP * MOD_CHANNELS * 4
+ pattern[at:at + 4] = bytes([period >> 8, period & 0xFF, 0x10, 0])
+
+ # The row after the last note's eight: a pattern break ends the song.
+ at = len(MOD_PERIODS) * MOD_NOTE_GAP * MOD_CHANNELS * 4
+ pattern[at + 2:at + 4] = bytes([0x0D, 0])
+
+ sample = bytes((int(round(MOD_AMP * math.sin(2.0 * math.pi * i / MOD_SAMPLE))) & 0xFF) for i in range(MOD_SAMPLE))
+
+ with open(path, "wb") as f:
+ f.write(header + bytes(pattern) + sample)
+
+
+# A MIDI variable-length number: seven bits a byte, the high bit set on
+# every byte but the last.
+def vlq(n):
+ out = bytes([n & 0x7F])
+
+ while n > 0x7F:
+ n >>= 7
+ out = bytes([0x80 | (n & 0x7F)]) + out
+
+ return out
+
+
+def writeMid(path):
+ title = MIDI_TITLE.encode("ascii")
+ track = b"\x00\xFF\x03" + bytes([len(title)]) + title + bytes([0, 0xC0, MIDI_PIANO])
+
+ for note in MIDI_NOTES:
+ track += bytes([0, 0x90, note, MIDI_VELOCITY]) + vlq(MIDI_DIVISION) + bytes([0x80, note, 0])
+
+ track += b"\x00\xFF\x2F\x00"
+
+ with open(path, "wb") as f:
+ f.write(b"MThd" + struct.pack(">IHHH", 6, 0, 1, MIDI_DIVISION) + b"MTrk" + struct.pack(">I", len(track)) + track)
+
+
+def writeVoc(path):
+ samples = bytes(128 + v for v in tone(VOC_RATE, VOC_HZ, 1000, VOC_AMP))
+ body = bytes([256 - 1000000 // VOC_RATE, 0]) + samples
+ head = b"Creative Voice File\x1A" + struct.pack(" ended at + ms (due +) ok|FAIL
// MUSTEST: PASS | FAIL
// MUSTEST: done
-// Arguments: "mod" (the module; plays on the sound stream), "fm" (the
-// MIDI file; must play on the FM chip) and "mpu" (the MIDI file; must
-// play on the MPU-401 port - the script sets DVX.INI [audio] midi =
-// mpu401).
+// Arguments: "mod" (the module; plays on the sound stream), "modwave" (the
+// module; must play on the wavetable synthesizer's voices), "fm" (the
+// MIDI file; must play on the FM chip), "mpu" (the MIDI file; must play
+// on the MPU-401 port - the script sets DVX.INI [audio] midi = mpu401),
+// "wave" (the MIDI file; must play on the wavetable synthesizer, with
+// the General MIDI SoundFont add-on staged; its samples loaded before the
+// timing starts), "softwave" (as wave, on the SoundFont software
+// synthesizer: the script sets DVX.INI [audio] midi = softwave), "rom"
+// (as wave, on a SoundFont written here whose flute is the AWE's General
+// MIDI ROM's own: it plays with nothing loaded, on an AWE only), "pat"
+// (as wave, on a Gravis patch set written here: its flute a sine wave)
+// and "load" (no music test: how much of the CPU the sound takes, a busy
+// loop's work in two seconds with nothing playing (after a run that only
+// warms up), then with eight
+// looping sample voices (at the output's rate, then at half of it), then
+// with a four-channel module (where it plays said), logged as
+// MUSTEST: load: units, % of the CPU).
#include "dvxApp.h"
#include "audio/audio.h"
@@ -52,6 +65,13 @@
#define MUS_TITLE "Music Test"
#define MUS_MOD_FILE "TEST\\MUSTEST.MOD"
#define MUS_MID_FILE "TEST\\MUSTEST.MID"
+#define MUS_ROM_FILE "TEST\\ROMTEST.SF2"
+#define MUS_LOAD_MODE "load"
+#define MUS_PAT_DIR "TEST\\MUSPAT"
+#define MUS_PAT_MIDI_DIR MUS_PAT_DIR "\\MIDI"
+#define MUS_PAT_INI MUS_PAT_DIR "\\" MUSIC_PATCH_SET_INI
+#define MUS_PAT_FILE MUS_PAT_MIDI_DIR "\\FLUTE.PAT"
+#define MUS_PAT_MAP "[Melodic Bank 0]\r\n73=FLUTE\r\n"
#define MUS_LEAD_MS 1500 // the epoch is this far after the start
#define MUS_SYNC_HZ 2000
#define MUS_SYNC_MS 200
@@ -97,6 +117,7 @@
#define MOD_ROW_END 15 // A5 silenced and a pattern break: the song ends after this row
#define MOD_AMPLITUDE 100
#define MOD_TWO_PI 6.283185307
+#define MOD_CHANNELS 4
// MIDI: one track, 500 ticks a beat at 120 beats a minute (1 tick = 1 ms)
#define MID_DIVISION 500
@@ -107,17 +128,122 @@
#define MID_E5 76
#define MID_A5 81
+// The load test (see the top)
+#define LOAD_MS 2000
+#define LOAD_TURNS 2000 // a unit of work: this many turns of a loop
+#define LOAD_VOICES 8
+#define LOAD_FRAMES 22050 // their sample: a second of a sine, mono 16-bit
+#define LOAD_RATE 22050
+#define LOAD_BITS 16
+#define LOAD_CHANNELS 1
+#define LOAD_PERIOD 50
+#define LOAD_AMPLITUDE 12000
+#define LOAD_VOLUME 100
+#define LOAD_PERCENT 100
+
+// The ROM test's SoundFont 2: a preset (the flute, 0:73) over an
+// instrument whose one zone loops the AWE ROM's flute (its addresses in
+// the ROM, in frames; it sounds at key 89 and 28 cents)
+#define ROM_FLUTE_START 0x2018D
+#define ROM_FLUTE_END 0x20724
+#define ROM_FLUTE_LOOP 0x206E2
+#define ROM_FLUTE_LOOP_END 0x20720
+#define ROM_RATE 44100
+#define ROM_ROOT 89
+#define ROM_CORRECTION (-28)
+#define ROM_SAMPLE_TYPE 0x8001 // a ROM sample, mono
+#define SF_VERSION_MAJOR 2
+#define SF_VERSION_MINOR 1
+#define SF_GEN_INSTRUMENT 41
+#define SF_GEN_SAMPLE_ID 53
+#define SF_GEN_SAMPLE_MODES 54
+#define SF_LOOP_ON 1
+#define SF_NAME_BYTES 20
+#define SF_PHDR_BYTES 38
+#define SF_BAG_BYTES 4
+#define SF_MOD_BYTES 10
+#define SF_GEN_BYTES 4
+#define SF_INST_BYTES 22
+#define SF_SHDR_BYTES 46
+#define SF_PHDR_PRESET 20
+#define SF_PHDR_BAG 24
+#define SF_INST_BAG 20
+#define SF_SHDR_START 20
+#define SF_SHDR_END 24
+#define SF_SHDR_LOOP 28
+#define SF_SHDR_LOOP_END 32
+#define SF_SHDR_RATE 36
+#define SF_SHDR_KEY 40
+#define SF_SHDR_CORRECTION 41
+#define SF_SHDR_TYPE 44
+#define RIFF_ID_BYTES 4
+#define RIFF_HEAD_BYTES 8 // an id and a length
+#define RIFF_FORM_BYTES 12 // "RIFF", its length, "sfbk"
+
+// The patch set's flute: one 16-bit wave, ten cycles of a sine of 50
+// frames at 22,050 Hz (441 Hz), looping from the third
+#define PAT_HEAD_BYTES 129
+#define PAT_SKIP_BYTES (63 + 47) // the instrument and layer headers
+#define PAT_WAVE_BYTES 96
+#define PAT_WAVES_AT 85
+#define PAT_W_SIZE 8
+#define PAT_W_LOOP_START 12
+#define PAT_W_LOOP_END 16
+#define PAT_W_RATE 20
+#define PAT_W_LOW 22
+#define PAT_W_HIGH 26
+#define PAT_W_ROOT 30
+#define PAT_W_BALANCE 36
+#define PAT_W_MODES 55
+#define PAT_W_SCALE_KEY 56
+#define PAT_W_SCALE_FACTOR 58
+#define PAT_MODES 0x05 // 16-bit, looping
+#define PAT_RATE 22050
+#define PAT_PERIOD 50
+#define PAT_FRAMES 500
+#define PAT_LOOP 100
+#define PAT_ROOT_MILLIHZ 441000
+#define PAT_LOW_MILLIHZ 8176 // every key
+#define PAT_HIGH_MILLIHZ 12543853
+#define PAT_PIVOT 60
+#define PAT_SEMITONE 1024 // scale factor: a semitone a key
+#define PAT_BALANCE 7
+#define PAT_AMPLITUDE 16000
+#define PAT_FRAME_BYTES 2
+
+// What a mode plays and where it must play; a bank it writes first and
+// the name musicSetBank takes it by (NULL: DVX.INI's)
+typedef struct {
+ const char *name;
+ int32_t where;
+ bool module;
+ bool (*writeBank)(void);
+ const char *bank;
+} ModeT;
+
// ============================================================
// Prototypes
// ============================================================
+static void loadSay(const char *what, uint32_t units, uint32_t idle);
+static void loadTest(AudioCtxT *audio);
+static uint32_t loadUnits(void);
+static uint32_t makeLoadMod(uint8_t *buf);
static uint32_t makeMid(uint8_t *buf);
static uint32_t makeMod(uint8_t *buf);
+static uint32_t makeRomFont(uint8_t *buf);
static uint32_t midEvent(uint8_t *buf, uint32_t at, uint32_t delta, uint8_t status, uint8_t data1, uint8_t data2, bool twoData);
static int64_t nowMs(void);
static void onEnd(MusicT *music, void *userData);
+static void put16(uint8_t *p, uint32_t v);
+static void put32(uint8_t *p, uint32_t v);
static void putCell(uint8_t *pattern, int32_t row, int32_t channel, uint32_t period, uint8_t effect, uint8_t param);
+static uint32_t riffChunk(uint8_t *buf, uint32_t at, const char *id, const uint8_t *body, uint32_t len);
+static uint32_t riffListClose(uint8_t *buf, uint32_t at, uint32_t from);
+static uint32_t riffListOpen(uint8_t *buf, uint32_t at, const char *type);
static bool writeFile(const char *path, const uint8_t *data, uint32_t size);
+static bool writePatchSet(void);
+static bool writeRomFont(void);
int32_t appMain(DxeAppContextT *ctx);
SHELL_APP_DESCRIPTOR(
@@ -128,10 +254,115 @@ SHELL_APP_DESCRIPTOR(
.priority = TS_PRIORITY_NORMAL
);
+static const ModeT sModes[] = {
+ { "mod", MUSIC_OUT_STREAM, true, NULL, NULL },
+ { "modwave", MUSIC_OUT_WAVE, true, NULL, NULL },
+ { "fm", MUSIC_OUT_FM, false, NULL, NULL },
+ { "mpu", MUSIC_OUT_MPU401, false, NULL, NULL },
+ { "wave", MUSIC_OUT_WAVE, false, NULL, NULL },
+ { "softwave", MUSIC_OUT_SOFTWAVE, false, NULL, NULL },
+ { "rom", MUSIC_OUT_WAVE, false, writeRomFont, MUS_ROM_FILE },
+ { "pat", MUSIC_OUT_WAVE, false, writePatchSet, MUS_PAT_INI }
+};
+
static uint64_t sEpochUs = 0;
static int64_t sEndedMs = -1;
+static void loadSay(const char *what, uint32_t units, uint32_t idle) {
+ dvxLog("MUSTEST: load: %s %lu units, %lu%% of the CPU", what, (unsigned long)units, (unsigned long)(idle > units ? (idle - units) * LOAD_PERCENT / idle : 0));
+}
+
+
+// Nothing playing (after a first run that only warms up), the voices,
+// the module (see the top).
+static void loadTest(AudioCtxT *audio) {
+ static int16_t pcm[LOAD_FRAMES];
+ static uint8_t mod[MUS_BUF_MAX];
+ AudioFormatT fmt;
+ AudioSampleT *sample = NULL;
+ MusicT *music = NULL;
+ uint32_t idle;
+
+ loadUnits();
+ idle = loadUnits();
+
+ loadSay("nothing playing", idle, idle);
+
+ for (int32_t i = 0; i < LOAD_FRAMES; i++) {
+ pcm[i] = (int16_t)(LOAD_AMPLITUDE * sin(MOD_TWO_PI * i / LOAD_PERIOD));
+ }
+
+ fmt.bits = LOAD_BITS;
+ fmt.channels = LOAD_CHANNELS;
+
+ // At the output's rate, then at half of it (each output frame between
+ // two of the sample's)
+ for (int32_t half = 0; half < 2; half++) {
+ fmt.rate = half ? LOAD_RATE / 2 : LOAD_RATE;
+
+ if (audioSampleCreate(audio, pcm, LOAD_FRAMES, &fmt, &sample) == AUDIO_OK) {
+ for (int32_t v = 0; v < LOAD_VOICES; v++) {
+ audioPlay(sample, LOAD_VOLUME, 0, true);
+ }
+
+ loadSay(half ? "8 sample voices at half the rate" : "8 sample voices", loadUnits(), idle);
+ audioStopAll(audio);
+ audioSampleFree(sample);
+ }
+ }
+
+ if (writeFile(MUS_MOD_FILE, mod, makeLoadMod(mod)) && musicLoad(MUS_MOD_FILE, &music) == MUSIC_OK && musicPlay(music, true) == MUSIC_OK) {
+ MusicInfoT info;
+
+ musicGetInfo(music, &info);
+ loadSay(info.where == MUSIC_OUT_WAVE ? "a 4-channel module on the wavetable synthesizer" : "a 4-channel module mixed in software", loadUnits(), idle);
+ }
+
+ musicFree(music);
+ remove(MUS_MOD_FILE);
+}
+
+
+// Units of work done in LOAD_MS, the sound and music libraries pumped
+// after each (as the shell's idle time would).
+static uint32_t loadUnits(void) {
+ volatile uint32_t sum = 0;
+ uint64_t until = platformMicros() + (uint64_t)LOAD_MS * MUS_US_PER_MS;
+ uint32_t units = 0;
+
+ while (platformMicros() < until) {
+ for (uint32_t i = 0; i < LOAD_TURNS; i++) {
+ sum += i;
+ }
+
+ audioPump();
+ musicPump();
+ units++;
+ }
+
+ return units;
+}
+
+
+// The load test's module: makeMod's sample on all four channels from
+// row 0 through the pattern, the song starting over.
+static uint32_t makeLoadMod(uint8_t *buf) {
+ static const uint32_t periods[MOD_CHANNELS] = { MOD_PERIOD_A4, MOD_PERIOD_E5, MOD_PERIOD_A5, MOD_PERIOD_E5 };
+ uint32_t len = makeMod(buf);
+ uint8_t *pattern = buf + MOD_PATTERN_AT;
+
+ memset(pattern, 0, MOD_PATTERN_BYTES);
+
+ for (int32_t c = 0; c < MOD_CHANNELS; c++) {
+ putCell(pattern, 0, c, periods[c], 0, 0);
+ }
+
+ buf[MOD_RESTART] = 0;
+ return len;
+}
+
+
// Flute: A4, E5, A5, 400 ms each, 300 ms apart; the track ends at 2000 ms.
static uint32_t makeMid(uint8_t *buf) {
const uint8_t header[] = { 'M', 'T', 'h', 'd', 0, 0, 0, 6, 0, 0, 0, 1, MID_DIVISION >> 8, MID_DIVISION & 0xFF, 'M', 'T', 'r', 'k', 0, 0, 0, 0 };
@@ -199,6 +430,77 @@ static uint32_t makeMod(uint8_t *buf) {
// One event: variable-length delta (under 2^14), status, one or two data
// bytes (a meta event: type, then length 0).
+// The ROM test's SoundFont (see ROM_FLUTE_*); no sample data of its own.
+static uint32_t makeRomFont(uint8_t *buf) {
+ uint8_t rec[SF_SHDR_BYTES * 2];
+ uint32_t at;
+ uint32_t list;
+
+ memcpy(buf, "RIFF", RIFF_ID_BYTES);
+ memcpy(buf + RIFF_HEAD_BYTES, "sfbk", RIFF_ID_BYTES);
+ at = RIFF_FORM_BYTES;
+
+
+ // INFO: the version; sdta: no samples
+ put16(rec, SF_VERSION_MAJOR);
+ put16(rec + 2, SF_VERSION_MINOR);
+ list = at;
+ at = riffListOpen(buf, at, "INFO");
+ at = riffChunk(buf, at, "ifil", rec, SF_GEN_BYTES);
+ at = riffListClose(buf, at, list);
+ list = at;
+ at = riffListOpen(buf, at, "sdta");
+ at = riffChunk(buf, at, "smpl", rec, 0);
+ at = riffListClose(buf, at, list);
+
+ // pdta: the preset and its terminal, the instrument, the sample
+ list = at;
+ at = riffListOpen(buf, at, "pdta");
+ memset(rec, 0, sizeof(rec));
+ memcpy(rec, "Flute", 5);
+ put16(rec + SF_PHDR_PRESET, MID_FLUTE);
+ memcpy(rec + SF_PHDR_BYTES, "EOP", 3);
+ put16(rec + SF_PHDR_BYTES + SF_PHDR_BAG, 1);
+ at = riffChunk(buf, at, "phdr", rec, SF_PHDR_BYTES * 2);
+ memset(rec, 0, sizeof(rec));
+ put16(rec + SF_BAG_BYTES, 1);
+ at = riffChunk(buf, at, "pbag", rec, SF_BAG_BYTES * 2);
+ memset(rec, 0, sizeof(rec));
+ at = riffChunk(buf, at, "pmod", rec, SF_MOD_BYTES);
+ put16(rec, SF_GEN_INSTRUMENT);
+ at = riffChunk(buf, at, "pgen", rec, SF_GEN_BYTES * 2);
+ memset(rec, 0, sizeof(rec));
+ memcpy(rec, "Flute", 5);
+ memcpy(rec + SF_INST_BYTES, "EOI", 3);
+ put16(rec + SF_INST_BYTES + SF_INST_BAG, 1);
+ at = riffChunk(buf, at, "inst", rec, SF_INST_BYTES * 2);
+ memset(rec, 0, sizeof(rec));
+ put16(rec + SF_BAG_BYTES, 2);
+ at = riffChunk(buf, at, "ibag", rec, SF_BAG_BYTES * 2);
+ memset(rec, 0, sizeof(rec));
+ at = riffChunk(buf, at, "imod", rec, SF_MOD_BYTES);
+ put16(rec, SF_GEN_SAMPLE_MODES);
+ put16(rec + 2, SF_LOOP_ON);
+ put16(rec + SF_GEN_BYTES, SF_GEN_SAMPLE_ID);
+ at = riffChunk(buf, at, "igen", rec, SF_GEN_BYTES * 3);
+ memset(rec, 0, sizeof(rec));
+ memcpy(rec, "flutec4", 7);
+ put32(rec + SF_SHDR_START, ROM_FLUTE_START);
+ put32(rec + SF_SHDR_END, ROM_FLUTE_END);
+ put32(rec + SF_SHDR_LOOP, ROM_FLUTE_LOOP);
+ put32(rec + SF_SHDR_LOOP_END, ROM_FLUTE_LOOP_END);
+ put32(rec + SF_SHDR_RATE, ROM_RATE);
+ rec[SF_SHDR_KEY] = ROM_ROOT;
+ rec[SF_SHDR_CORRECTION] = (uint8_t)(int8_t)ROM_CORRECTION;
+ put16(rec + SF_SHDR_TYPE, ROM_SAMPLE_TYPE);
+ memcpy(rec + SF_SHDR_BYTES, "EOS", 3);
+ at = riffChunk(buf, at, "shdr", rec, SF_SHDR_BYTES * 2);
+ at = riffListClose(buf, at, list);
+ put32(buf + RIFF_ID_BYTES, at - RIFF_HEAD_BYTES);
+ return at;
+}
+
+
static uint32_t midEvent(uint8_t *buf, uint32_t at, uint32_t delta, uint8_t status, uint8_t data1, uint8_t data2, bool twoData) {
if (delta >= 0x80) {
buf[at++] = (uint8_t)(0x80 | (delta >> 7));
@@ -228,6 +530,18 @@ static void onEnd(MusicT *music, void *userData) {
}
+static void put16(uint8_t *p, uint32_t v) {
+ p[0] = (uint8_t)v;
+ p[1] = (uint8_t)(v >> 8);
+}
+
+
+static void put32(uint8_t *p, uint32_t v) {
+ put16(p, v);
+ put16(p + 2, v >> 16);
+}
+
+
static void putCell(uint8_t *pattern, int32_t row, int32_t channel, uint32_t period, uint8_t effect, uint8_t param) {
uint8_t *cell = pattern + row * MOD_ROW_BYTES + channel * MOD_CELL_BYTES;
@@ -238,6 +552,32 @@ static void putCell(uint8_t *pattern, int32_t row, int32_t channel, uint32_t per
}
+// A chunk (id, length, body) at at; where the next one goes (no SoundFont
+// chunk here has an odd length).
+static uint32_t riffChunk(uint8_t *buf, uint32_t at, const char *id, const uint8_t *body, uint32_t len) {
+ memcpy(buf + at, id, RIFF_ID_BYTES);
+ put32(buf + at + RIFF_ID_BYTES, len);
+ memcpy(buf + at + RIFF_HEAD_BYTES, body, len);
+ return at + RIFF_HEAD_BYTES + len;
+}
+
+
+// The LIST opened at from ends at at: its id and length.
+static uint32_t riffListClose(uint8_t *buf, uint32_t at, uint32_t from) {
+ memcpy(buf + from, "LIST", RIFF_ID_BYTES);
+ put32(buf + from + RIFF_ID_BYTES, at - from - RIFF_HEAD_BYTES);
+ return at;
+}
+
+
+// A LIST of type at at (its id and length written as it closes); where
+// its first chunk goes.
+static uint32_t riffListOpen(uint8_t *buf, uint32_t at, const char *type) {
+ memcpy(buf + at + RIFF_HEAD_BYTES, type, RIFF_ID_BYTES);
+ return at + RIFF_FORM_BYTES;
+}
+
+
static bool writeFile(const char *path, const uint8_t *data, uint32_t size) {
FILE *f = fopen(path, "wb");
@@ -252,17 +592,70 @@ static bool writeFile(const char *path, const uint8_t *data, uint32_t size) {
}
+// The patch set: its INI (program 73, the flute, is FLUTE) and the flute
+// (see PAT_*).
+static bool writePatchSet(void) {
+ static uint8_t pat[PAT_HEAD_BYTES + PAT_SKIP_BYTES + PAT_WAVE_BYTES + PAT_FRAMES * PAT_FRAME_BYTES];
+ uint8_t *w = pat + PAT_HEAD_BYTES + PAT_SKIP_BYTES;
+ uint8_t *data = w + PAT_WAVE_BYTES;
+
+ memset(pat, 0, sizeof(pat));
+ memcpy(pat, "GF1PATCH110", 11);
+ put16(pat + PAT_WAVES_AT, 1);
+ put32(w + PAT_W_SIZE, PAT_FRAMES * PAT_FRAME_BYTES);
+ put32(w + PAT_W_LOOP_START, PAT_LOOP * PAT_FRAME_BYTES);
+ put32(w + PAT_W_LOOP_END, PAT_FRAMES * PAT_FRAME_BYTES);
+ put16(w + PAT_W_RATE, PAT_RATE);
+ put32(w + PAT_W_LOW, PAT_LOW_MILLIHZ);
+ put32(w + PAT_W_HIGH, PAT_HIGH_MILLIHZ);
+ put32(w + PAT_W_ROOT, PAT_ROOT_MILLIHZ);
+ w[PAT_W_BALANCE] = PAT_BALANCE;
+ w[PAT_W_MODES] = PAT_MODES;
+ put16(w + PAT_W_SCALE_KEY, PAT_PIVOT);
+ put16(w + PAT_W_SCALE_FACTOR, PAT_SEMITONE);
+
+ for (int32_t i = 0; i < PAT_FRAMES; i++) {
+ put16(data + i * PAT_FRAME_BYTES, (uint32_t)(uint16_t)(int16_t)(PAT_AMPLITUDE * sin(MOD_TWO_PI * i / PAT_PERIOD)));
+ }
+
+ return platformMkdirRecursive(MUS_PAT_MIDI_DIR) == 0 && writeFile(MUS_PAT_INI, (const uint8_t *)MUS_PAT_MAP, (uint32_t)strlen(MUS_PAT_MAP)) && writeFile(MUS_PAT_FILE, pat, sizeof(pat));
+}
+
+
+static bool writeRomFont(void) {
+ static uint8_t font[MUS_BUF_MAX];
+
+ return writeFile(MUS_ROM_FILE, font, makeRomFont(font));
+}
+
+
int32_t appMain(DxeAppContextT *ctx) {
static uint8_t buf[MUS_BUF_MAX];
- const char *mode = ctx->args[0] ? ctx->args : "mod";
- bool isMod = strcmp(mode, "mod") == 0;
- int32_t wantOut = isMod ? MUSIC_OUT_STREAM : (strcmp(mode, "mpu") == 0 ? MUSIC_OUT_MPU401 : MUSIC_OUT_FM);
- const char *path = isMod ? MUS_MOD_FILE : MUS_MID_FILE;
- uint32_t size = isMod ? makeMod(buf) : makeMid(buf);
+ const char *mode = ctx->args[0] ? ctx->args : sModes[0].name;
+ const ModeT *m = &sModes[0];
int32_t problems = 0;
MusicT *music = NULL;
MusicInfoT info;
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sModes); i++) {
+ m = strcmp(mode, sModes[i].name) == 0 ? &sModes[i] : m;
+ }
+
+ if (strcmp(mode, MUS_LOAD_MODE) == 0) {
+ AudioCtxT *audio = audioOpen();
+
+ if (audio) {
+ loadTest(audio);
+ audioClose(audio);
+ }
+
+ dvxLog("MUSTEST: %s done", mode);
+ return 0;
+ }
+
+ const char *path = m->module ? MUS_MOD_FILE : MUS_MID_FILE;
+ uint32_t size = m->module ? makeMod(buf) : makeMid(buf);
+
AudioCtxT *audio = audioOpen();
if (!audio || !writeFile(path, buf, size) || musicLoad(path, &music) != MUSIC_OK) {
@@ -272,6 +665,18 @@ int32_t appMain(DxeAppContextT *ctx) {
}
musicSetEndFn(music, onEnd, NULL);
+
+ if (m->writeBank && (!m->writeBank() || musicSetBank(music, m->bank) != MUSIC_OK)) {
+ dvxLog("MUSTEST: %s: the bank was refused", mode);
+ problems++;
+ }
+
+ // A wavetable music's samples go into the synthesizer as it is set up
+ // (setting its position does that), before the timing starts
+ if (m->where == MUSIC_OUT_WAVE || m->where == MUSIC_OUT_SOFTWAVE) {
+ musicSetPosition(music, 0);
+ }
+
sEpochUs = platformMicros() + (uint64_t)MUS_LEAD_MS * MUS_US_PER_MS;
while (nowMs() < 0) {
@@ -287,10 +692,10 @@ int32_t appMain(DxeAppContextT *ctx) {
musicPlay(music, false);
musicGetInfo(music, &info);
- dvxLog("MUSTEST: %s: %s on %d: %s", mode, isMod ? "module" : "MIDI", (int)info.where, info.output);
+ dvxLog("MUSTEST: %s: %s on %d: %s", mode, m->module ? "module" : "MIDI", (int)info.where, info.output);
- if (info.where != wantOut) {
- dvxLog("MUSTEST: %s plays in the wrong place (%d, not %d)", mode, (int)info.where, (int)wantOut);
+ if (info.where != m->where) {
+ dvxLog("MUSTEST: %s plays in the wrong place (%d, not %d)", mode, (int)info.where, (int)m->where);
problems++;
}
@@ -299,7 +704,7 @@ int32_t appMain(DxeAppContextT *ctx) {
tsYield();
}
- int64_t due = MUS_START_MS + (isMod ? MUS_MOD_MS : MUS_MIDI_MS);
+ int64_t due = MUS_START_MS + (m->module ? MUS_MOD_MS : MUS_MIDI_MS);
bool ok = sEndedMs >= due - MUS_EARLY_MS && sEndedMs <= due + MUS_LATE_MS;
dvxLog("MUSTEST: %s ended at +%ld ms (due +%ld) %s", mode, (long)sEndedMs, (long)due, ok ? "ok" : "FAIL");
diff --git a/src/test/dos/runDos.sh b/src/test/dos/runDos.sh
index 80b22784..1db2c41a 100755
--- a/src/test/dos/runDos.sh
+++ b/src/test/dos/runDos.sh
@@ -83,6 +83,7 @@ GOLDEN_DIR="${HERE}/golden"
SHOTS_DIR="${DVX_ROOT}/screenshots"
MASKS_DIR="${SHOTS_DIR}/masks"
FIXTURES_DIR="${HERE}/fixtures"
+MKMEDIA_PY="${HERE}/mkMedia.py" # Media Player's test files (stageTree)
NVR_DIR="${HERE}/nvr"
IMGCMP="${DVX_ROOT}/obj/test/dos/imgCmp"
BIN_DIR="${DVX_ROOT}/bin"
@@ -239,13 +240,29 @@ NEED_JOYDRIVE=0
# (--update-goldens writes it unless the recording still matches it);
# the first frequency is the sync tone.
# Without the line neither emulator has a sound card (DOSBox's card would
-# stall with its sound output off) and both are silent. "# INI: section key value"
+# stall with its sound output off) and both are silent; a script with a
+# "# AUDIO-CARD:" line (below) and no "# AUDIO:" one has that card and is
+# recorded, but the recording is not compared (a measurement). "# INI: section key value"
# sets a key in the staged DVX.INI (e.g. "# INI: audio driver speaker").
# "# SDK-GAMES" also stages the SDK's game templates (sdkGame, sdkBGame).
# "# AUDIO-CARD: sb1|sb2|sbpro2" plays through an older Sound Blaster
# instead (86Box sb / sb2.0 / sbprov2 at their defaults A220 I7 D1, the
# sb2.0 on IRQ 5, BLASTER set in AUTOEXEC; DOSBox sb1 / sb2 / sbpro2,
-# A220 I7 D1).
+# A220 I7 D1); "wss" through 86Box's Windows Sound System (an AD1848 at
+# 534h behind the configuration register at 530h, OPL3 at 388h; DOSBox
+# has none: its scripts are # ONLY: 86box); "awe32" through 86Box's Sound
+# Blaster AWE32 (its EMU8000 at 620h, 512 KB, the ROM from the pinned ROM
+# set; BLASTER with E620; DOSBox has none); "gus" through a Gravis
+# UltraSound alone (86Box gus at 220h, ULTRASND naming DMA 1 and IRQ 7 for
+# its latches; DOSBox's at 240h, which sets ULTRASND itself); "gusmax"
+# through 86Box's UltraSound MAX (the same, with its CS4231 codec at 32Ch
+# once its control register turns it on; DOSBox has none: # ONLY: 86box);
+# "sb16gus"
+# through the Sound Blaster 16 with a GUS beside it for music (86Box's
+# second card, moved to 240h, ULTRASND naming DMA 3 and IRQ 7; DOSBox's
+# both at their defaults); "covox|disney|ston1" through
+# a DAC on LPT1 (378h) and no sound card (86Box lpt_dac / dss /
+# lpt_dac_stereo, DOSBox lpt_dac covox / disney / ston1, its filter off).
# "# AUDIO-MIDI: fluidsynth" (86Box) connects the Sound Blaster 16's
# MPU-401 port (330h) to 86Box's FluidSynth with the pinned General MIDI
# sound font (toolchains/soundfont, src/test/music/installMusicRefs.sh),
@@ -258,8 +275,14 @@ NEED_SOUND=0
AUDIO_FREQS=""
SOUND_TEXT=""
BOX86_SNDCARD="none"
+BOX86_SNDCARD2="none"
+BOX86_GUS_BASE=""
+BOX86_LPT1=0
+BOX86_LPT1_DEVICE="none"
DOSBOX_NOSOUND="true"
DOSBOX_SBTYPE="none"
+DOSBOX_LPT_DAC="none"
+DOSBOX_GUS="false"
AUDIO_ENV=()
INI_SETS=()
DOSBOX_MAPPER_LINE=""
@@ -319,7 +342,9 @@ SETUPLOG_EXPECTS=()
# text file from fixtures/ into the staged DVX tree (DOS line ends).
# "# PACKAGE: fixture path" builds an add-on package (.DPK) there with the
# package tool from fixtures/: its PACKAGE.INI and the files
-# beside it.
+# beside it. "# ADDON: name" puts a DVX add-on's files, as the build made
+# them in obj/addons/, into the staged tree (names in lower case,
+# as bin/ has them), as if it were installed.
# "# WIDGET: fixture" installs a control project fixture (a control file
# written in BASIC, built like every fixture) as WIDGETS\\
# .WGT, so DVX loads it at boot.
@@ -339,6 +364,7 @@ SECLINK_PIDS=()
PROXY_EXPECTS=()
STAGES=()
PACKAGES=()
+ADDONS=()
WIDGET_FIXTURES=()
BROKEN_FIXTURES=()
BOX86_SERIAL2=0
@@ -634,6 +660,10 @@ stageTree() {
cp -r "${FIXTURES_DIR}/files/." "${dir}/DVX/test/"
fi
+ # Media Player's music and sound files (player.scr), made here rather
+ # than kept as binaries
+ python3 "${MKMEDIA_PY}" "${dir}/DVX/test"
+
local project
local fixture
@@ -723,6 +753,25 @@ PY
fi
done
+ # "# ADDON: name": a built add-on's files, as if installed
+ local addon
+ local file
+
+ for addon in "${ADDONS[@]}"; do
+ if [ ! -d "${DVX_ROOT}/obj/addons/${addon}" ]; then
+ echo "runDos: add-on ${addon} is not built (obj/addons/${addon})"
+ continue
+ fi
+
+ while IFS= read -r file; do
+ local lower
+
+ lower="$(printf '%s' "${file}" | tr '[:upper:]' '[:lower:]')"
+ mkdir -p "$(dirname "${dir}/DVX/${lower}")"
+ cp "${DVX_ROOT}/obj/addons/${addon}/${file}" "${dir}/DVX/${lower}"
+ done < <(cd "${DVX_ROOT}/obj/addons/${addon}" && find . -type f | sed 's|^\./||')
+ done
+
# "# WIDGET: fixture": the fixture's control file installed in WIDGETS
local widget
@@ -890,6 +939,7 @@ joystick_type = ${JOY_BOX86}
[Sound]
sndcard = ${BOX86_SNDCARD}
+sndcard2 = ${BOX86_SNDCARD2}
midi_device = ${BOX86_MIDI}
[Storage controllers]
@@ -904,7 +954,8 @@ serial1_enabled = 1
serial1_device = pipe
serial2_enabled = ${BOX86_SERIAL2}
serial2_device = pipe
-lpt1_enabled = 0
+lpt1_enabled = ${BOX86_LPT1}
+lpt1_device = ${BOX86_LPT1_DEVICE}
[Named Pipe (COM) #1]
path = $3
@@ -930,6 +981,14 @@ reverb = 0
EOF
fi
+ if [ -n "${BOX86_GUS_BASE}" ]; then
+ cat >> "${vm}/86box.cfg" <> "${vm}/86box.cfg" </dev/null || true
- find "${RUN}/stage" -maxdepth 2 -ipath "*/${SETUP_SHOTS_DIR}/*.bmp" -exec cp {} "${RUN}/out/" \; 2>/dev/null || true
+ find "${RUN}/stage" -maxdepth 2 -ipath "${RUN}/stage/${SETUP_SHOTS_DIR}/*.bmp" -exec cp {} "${RUN}/out/" \; 2>/dev/null || true
fi
local expect
@@ -1372,7 +1438,10 @@ runDosbox() {
for expect in "${EXPECTS[@]}"; do
find "${RUN}/stage/DVX" -ipath "${RUN}/stage/DVX/${expect%% *}" -exec cp {} "${RUN}/out/" \; 2>/dev/null || true
done
- find "${RUN}/stage/DVX" -maxdepth 2 -ipath '*/test/*.bmp' -exec cp {} "${RUN}/out/" \; 2>/dev/null || true
+ # -ipath matches the whole path, which has a test folder of its own
+ # (obj/test/dos): the pattern starts at the staged DVX folder, so only
+ # DVX\TEST's pictures are shots (not SYSTEM\DOSPROG.BMP).
+ find "${RUN}/stage/DVX" -maxdepth 2 -ipath "${RUN}/stage/DVX/test/*.bmp" -exec cp {} "${RUN}/out/" \; 2>/dev/null || true
}
@@ -1546,7 +1615,7 @@ checkAudio() {
local rec="${RUN}/audio.wav"
local rc
- if [ "${NEED_SOUND}" -eq 0 ]; then
+ if [ -z "${AUDIO_FREQS}" ]; then
return 0
fi
@@ -1827,6 +1896,12 @@ for script in "${SCRIPTS[@]}"; do
PACKAGES+=("${expect}")
done < <(sed -n 's/^#[[:space:]]*PACKAGE:[[:space:]]*\([^[:space:]]*\)[[:space:]]\{1,\}\([^[:space:]]*\).*$/\1 \2/p' "${script}")
+ ADDONS=()
+
+ while IFS= read -r expect; do
+ ADDONS+=("${expect}")
+ done < <(sed -n 's/^#[[:space:]]*ADDON:[[:space:]]*\([^[:space:]]*\).*$/\1/p' "${script}")
+
WIDGET_FIXTURES=()
while IFS= read -r expect; do
@@ -1865,16 +1940,26 @@ for script in "${SCRIPTS[@]}"; do
NEED_SOUND=0
SOUND_TEXT=""
BOX86_SNDCARD="none"
+ BOX86_SNDCARD2="none"
+ BOX86_GUS_BASE=""
+ BOX86_LPT1=0
+ BOX86_LPT1_DEVICE="none"
BOX86_MIDI="none"
DOSBOX_NOSOUND="true"
DOSBOX_SBTYPE="none"
+ DOSBOX_LPT_DAC="none"
+ DOSBOX_GUS="false"
- if [ -n "${AUDIO_FREQS}" ]; then
+ # A card without "# AUDIO:" is recorded but not compared
+ AUDIO_CARD="$(sed -n 's/^#[[:space:]]*AUDIO-CARD:[[:space:]]*\([a-z0-9]*\).*$/\1/p' "${script}" | head -n 1)"
+
+ if [ -n "${AUDIO_FREQS}" ] || [ -n "${AUDIO_CARD}" ]; then
NEED_SOUND=1
DOSBOX_NOSOUND="false"
# 86Box card, the BLASTER value that matches it, DOSBox sbtype
- case "$(sed -n 's/^#[[:space:]]*AUDIO-CARD:[[:space:]]*\([a-z0-9]*\).*$/\1/p' "${script}" | head -n 1)" in
+
+ case "${AUDIO_CARD}" in
sb1)
BOX86_SNDCARD="sb"
SOUND_TEXT="SET BLASTER=A220 I7 D1 T1"
@@ -1890,6 +1975,40 @@ for script in "${SCRIPTS[@]}"; do
SOUND_TEXT="SET BLASTER=A220 I7 D1 T4"
DOSBOX_SBTYPE="sbpro2"
;;
+ wss)
+ BOX86_SNDCARD="wss"
+ ;;
+ awe32)
+ BOX86_SNDCARD="sbawe32"
+ SOUND_TEXT="SET BLASTER=A220 I5 D1 H5 P330 E620 T6"
+ ;;
+ gus)
+ BOX86_SNDCARD="gus"
+ SOUND_TEXT="SET ULTRASND=220,1,1,7,7"
+ DOSBOX_GUS="true"
+ ;;
+ gusmax)
+ BOX86_SNDCARD="gusmax"
+ SOUND_TEXT="SET ULTRASND=220,1,1,7,7"
+ ;;
+ sb16gus)
+ BOX86_SNDCARD="sb16"
+ BOX86_SNDCARD2="gus"
+ BOX86_GUS_BASE="0240"
+ SOUND_TEXT="SET BLASTER=A220 I5 D1 H5 T6"$'\n'"SET ULTRASND=240,3,3,7,7"
+ DOSBOX_SBTYPE="sb16"
+ DOSBOX_GUS="true"
+ ;;
+ covox|disney|ston1)
+ BOX86_LPT1=1
+ DOSBOX_LPT_DAC="${AUDIO_CARD}"
+
+ case "${AUDIO_CARD}" in
+ covox) BOX86_LPT1_DEVICE="lpt_dac" ;;
+ disney) BOX86_LPT1_DEVICE="dss" ;;
+ *) BOX86_LPT1_DEVICE="lpt_dac_stereo" ;;
+ esac
+ ;;
*)
BOX86_SNDCARD="sb16"
SOUND_TEXT="SET BLASTER=A220 I5 D1 H5 T6"
diff --git a/src/test/dos/scripts/apps.scr b/src/test/dos/scripts/apps.scr
index 5c411f9d..e8807d95 100644
--- a/src/test/dos/scripts/apps.scr
+++ b/src/test/dos/scripts/apps.scr
@@ -97,6 +97,13 @@ SHOT welcome
KEY Alt+F4
WAITNOWIN "Welcome to DVX"
+LAUNCH apps/kpunch/player/player.app
+WAITWIN "Media Player"
+WAIT 10
+SHOT player
+KEY Alt+F4
+WAITNOWIN "Media Player"
+
LAUNCH apps/kpunch/dvxbasic/dvxbasic.app
WAITWIN "DVX BASIC*"
WAIT 20
diff --git a/src/test/dos/scripts/awemod.scr b/src/test/dos/scripts/awemod.scr
new file mode 100644
index 00000000..372642ca
--- /dev/null
+++ b/src/test/dos/scripts/awemod.scr
@@ -0,0 +1,20 @@
+# awemod.scr -- a tracker module on the Sound Blaster AWE32's EMU8000,
+# recorded.
+#
+# 86Box's Sound Blaster AWE32 (# AUDIO-CARD). TEST\MUSTEST.APP writes a
+# MOD (a sine sample: A4, E5, A5, 360 ms each, 360 ms apart) and plays it
+# after a 2000 Hz sync tone; it must play on the EMU8000's voices (its
+# sample in the EMU8000's memory, libxmp's frames played by the music
+# timer) and end on time (# LOG). The recording must match the golden
+# (# AUDIO). DOSBox has no AWE32.
+# ONLY: 86box
+# AUDIO-CARD: awe32
+# AUDIO: 2000 440 659 880
+# LOG: MUSTEST: modwave: module on 5: Sound Blaster AWE EMU8000 at 620h
+# LOG: MUSTEST: modwave PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP modwave
+WAITLOG "MUSTEST: modwave done" 90
+QUIT
diff --git a/src/test/dos/scripts/awerom.scr b/src/test/dos/scripts/awerom.scr
new file mode 100644
index 00000000..decc42d1
--- /dev/null
+++ b/src/test/dos/scripts/awerom.scr
@@ -0,0 +1,21 @@
+# awerom.scr -- a MIDI file on the Sound Blaster AWE32's sample ROM,
+# recorded.
+#
+# 86Box's Sound Blaster AWE32 (# AUDIO-CARD) and no SoundFont add-on.
+# TEST\MUSTEST.APP writes a MIDI file (a flute: A4, E5, A5, 400 ms each,
+# 300 ms apart) and a SoundFont whose flute is the AWE's General MIDI
+# ROM's own (ROM samples, no sample data), and plays it after a 2000 Hz
+# sync tone: it must play on the EMU8000 from its ROM, with nothing
+# loaded into its memory, and end on time (# LOG). The recording must
+# match the golden (# AUDIO). DOSBox has no AWE32.
+# ONLY: 86box
+# AUDIO-CARD: awe32
+# AUDIO: 2000 440 659 880
+# LOG: MUSTEST: rom: MIDI on 5: Sound Blaster AWE EMU8000 at 620h
+# LOG: MUSTEST: rom PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP rom
+WAITLOG "MUSTEST: rom done" 90
+QUIT
diff --git a/src/test/dos/scripts/awewave.scr b/src/test/dos/scripts/awewave.scr
new file mode 100644
index 00000000..c10766e9
--- /dev/null
+++ b/src/test/dos/scripts/awewave.scr
@@ -0,0 +1,23 @@
+# awewave.scr -- a MIDI file on the Sound Blaster AWE32's EMU8000,
+# recorded.
+#
+# 86Box's Sound Blaster AWE32 (# AUDIO-CARD) with the DVX General MIDI
+# SoundFont add-on installed (# ADDON). awe.drv finds the EMU8000
+# through BLASTER's E, sets it up and sizes its memory; TEST\MUSTEST.APP
+# writes a MIDI file (a flute: A4, E5, A5, 400 ms each, 300 ms apart) and
+# plays it after a 2000 Hz sync tone; it must play on the EMU8000 (the
+# flute's samples loaded into its memory, its own envelopes, filter and
+# effects) and end on time (# LOG). The recording must match the golden
+# (# AUDIO). DOSBox has no AWE32.
+# ONLY: 86box
+# AUDIO-CARD: awe32
+# ADDON: dvxgm
+# AUDIO: 2000 440 659 880
+# LOG: MUSTEST: wave: MIDI on 5: Sound Blaster AWE EMU8000 at 620h
+# LOG: MUSTEST: wave PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP wave
+WAITLOG "MUSTEST: wave done" 90
+QUIT
diff --git a/src/test/dos/scripts/dosprog.scr b/src/test/dos/scripts/dosprog.scr
new file mode 100644
index 00000000..98e175aa
--- /dev/null
+++ b/src/test/dos/scripts/dosprog.scr
@@ -0,0 +1,34 @@
+# dosprog.scr -- a DOS program run from DVX: DVX steps aside and comes back.
+#
+# TEST\DOSRUN.APP (fixtures/dosrun) asks for TEST\DOSPROG.BAT with
+# ShellRunDos. DVX names it among the programs that close first ("Run
+# DOS Program"; Enter answers Yes), lets the screen and its hardware go,
+# and runs the batch file through the command interpreter in its own
+# folder: it writes TEST\DOSRAN.TXT (# EXPECT). DVX comes back (the log
+# says so), sets its mode again and repaints (VIDEOCHECK: the screen is
+# what DVX drew; dp-back), and the keyboard,
+# the mouse, and the timer work again (F1 opens the help, its window is
+# closed, a second screenshot).
+# STAGE: dosrun/dosprog.bat test/DOSPROG.BAT
+# EXPECT: TEST/DOSRAN.TXT dosran.txt
+TIMEOUT 60
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/DOSRUN.APP
+WAITWIN "Run DOS Program"
+WAIT 10
+SHOT dp-ask
+KEY Enter
+WAITLOG "DOS: back from"
+WAITNOWIN "DOS Run"
+WAIT 20
+VIDEOCHECK
+SHOT dp-back
+KEY F1
+WAITWIN "* - DVX Help"
+WAITMS 500
+KEY Alt+F4
+WAITNOWIN "* - DVX Help"
+WAIT 10
+SHOT dp-after
+QUIT
diff --git a/src/test/dos/scripts/endbas.scr b/src/test/dos/scripts/endbas.scr
new file mode 100644
index 00000000..36589166
--- /dev/null
+++ b/src/test/dos/scripts/endbas.scr
@@ -0,0 +1,22 @@
+# endbas.scr -- End Task on a BASIC program: the BASIC runtime's kill
+# hook ends the program's VM, forms, and module while its window is
+# still there (the log says so), then the shell unloads it and DVX goes
+# on. The Task Manager lists Program Manager and Clock; Down selects
+# Clock, Alt+E ends it.
+# LOG: BASIC: 'Clock' ended by force; its runtime freed
+TIMEOUT 90
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH apps/kpunch/clock/clock.app
+WAITWIN "Clock"
+WAIT 10
+KEY Ctrl+Esc
+WAITWIN "Task Manager"
+KEY Down
+WAIT 10
+KEY Alt+E
+WAITNOWIN "Clock"
+WAIT 10
+ASSERTWIN "Program Manager"
+ASSERTWIN "Task Manager"
+QUIT
diff --git a/src/test/dos/scripts/f1.scr b/src/test/dos/scripts/f1.scr
index 56f7c71f..fcb7adc1 100644
--- a/src/test/dos/scripts/f1.scr
+++ b/src/test/dos/scripts/f1.scr
@@ -169,5 +169,18 @@ WAIT 10
KEY Alt+F4
WAITNOWIN "Welcome to DVX"
+LAUNCH apps/kpunch/player/player.app
+WAITWIN "Media Player"
+WAIT 10
+KEY F1
+WAITWIN "* - DVX Help"
+WAIT 20
+SHOT h-player
+KEY Alt+F4
+WAITNOWIN "* - DVX Help"
+WAIT 10
+KEY Alt+F4
+WAITNOWIN "Media Player"
+
ASSERTWIN "Program Manager"
QUIT
diff --git a/src/test/dos/scripts/gus.scr b/src/test/dos/scripts/gus.scr
new file mode 100644
index 00000000..0ceb231e
--- /dev/null
+++ b/src/test/dos/scripts/gus.scr
@@ -0,0 +1,17 @@
+# gus.scr -- sound through a Gravis UltraSound, recorded.
+#
+# A Gravis UltraSound alone (# AUDIO-CARD: no Sound Blaster), found by
+# gus.drv through ULTRASND: digital sound on two of its voices from a
+# ring in its memory, refilled by DMA from its interrupts.
+# TEST\SNDTEST.APP plays its "full" timeline; every sound must end on
+# time and the recording must match the golden.
+# AUDIO-CARD: gus
+# AUDIO: 2000 440 880 660 330 1320
+# LOG: SNDTEST: full: device gus: Gravis UltraSound at
+# LOG: SNDTEST: full PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/SNDTEST.APP full
+WAITLOG "SNDTEST: full done" 90
+QUIT
diff --git a/src/test/dos/scripts/gusmod.scr b/src/test/dos/scripts/gusmod.scr
new file mode 100644
index 00000000..14e56c1c
--- /dev/null
+++ b/src/test/dos/scripts/gusmod.scr
@@ -0,0 +1,19 @@
+# gusmod.scr -- a tracker module on the Gravis UltraSound's own voices,
+# recorded.
+#
+# A Gravis UltraSound alone (# AUDIO-CARD). TEST\MUSTEST.APP writes a
+# MOD (a sine sample: A4, E5, A5, 360 ms each, 360 ms apart) and plays it
+# after a 2000 Hz sync tone; it must play on the GUS's voices (its sample
+# in the GUS's memory, libxmp's frames played by the music timer, nothing
+# mixed in software) and end on time (# LOG). The recording must match
+# the golden (# AUDIO).
+# AUDIO-CARD: gus
+# AUDIO: 2000 440 659 880
+# LOG: MUSTEST: modwave: module on 5: Gravis UltraSound wavetable at
+# LOG: MUSTEST: modwave PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP modwave
+WAITLOG "MUSTEST: modwave done" 90
+QUIT
diff --git a/src/test/dos/scripts/guspat.scr b/src/test/dos/scripts/guspat.scr
new file mode 100644
index 00000000..18422d98
--- /dev/null
+++ b/src/test/dos/scripts/guspat.scr
@@ -0,0 +1,19 @@
+# guspat.scr -- a MIDI file on a Gravis UltraSound patch set, recorded.
+#
+# A Gravis UltraSound alone (# AUDIO-CARD) and no SoundFont add-on.
+# TEST\MUSTEST.APP writes a MIDI file (a flute: A4, E5, A5, 400 ms each,
+# 300 ms apart) and a Gravis patch set (ULTRASND.INI naming FLUTE.PAT for
+# program 73, a looping 441 Hz sine wave) and plays the file on it after
+# a 2000 Hz sync tone: it must play on the GUS's own voices (the patch's
+# wave loaded into its memory, tuned to A4) and end on time (# LOG). The
+# recording must match the golden (# AUDIO).
+# AUDIO-CARD: gus
+# AUDIO: 2000 440 659 880
+# LOG: MUSTEST: pat: MIDI on 5: Gravis UltraSound wavetable at
+# LOG: MUSTEST: pat PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP pat
+WAITLOG "MUSTEST: pat done" 90
+QUIT
diff --git a/src/test/dos/scripts/guswave.scr b/src/test/dos/scripts/guswave.scr
new file mode 100644
index 00000000..d4057576
--- /dev/null
+++ b/src/test/dos/scripts/guswave.scr
@@ -0,0 +1,20 @@
+# guswave.scr -- a MIDI file on the Gravis UltraSound's wavetable
+# synthesizer, recorded.
+#
+# A Gravis UltraSound alone (# AUDIO-CARD) with the DVX General MIDI
+# SoundFont add-on installed (# ADDON). TEST\MUSTEST.APP writes a MIDI
+# file (a flute: A4, E5, A5, 400 ms each, 300 ms apart) and plays it after
+# a 2000 Hz sync tone; it must play on the GUS's own voices (the flute's
+# samples loaded into its memory) and end on time (# LOG). The recording
+# must match the golden (# AUDIO).
+# AUDIO-CARD: gus
+# ADDON: dvxgm
+# AUDIO: 2000 440 659 880
+# LOG: MUSTEST: wave: MIDI on 5: Gravis UltraSound wavetable at
+# LOG: MUSTEST: wave PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP wave
+WAITLOG "MUSTEST: wave done" 90
+QUIT
diff --git a/src/test/dos/scripts/guswt.scr b/src/test/dos/scripts/guswt.scr
new file mode 100644
index 00000000..9447f924
--- /dev/null
+++ b/src/test/dos/scripts/guswt.scr
@@ -0,0 +1,23 @@
+# guswt.scr -- a MIDI file on a Gravis UltraSound beside a Sound Blaster,
+# recorded.
+#
+# A Sound Blaster 16 and a Gravis UltraSound (# AUDIO-CARD: the GUS at
+# 240h) with the DVX General MIDI SoundFont add-on installed (# ADDON).
+# Digital sound plays on the Sound Blaster, so the GUS is only the
+# wavetable synthesizer (guswt.drv, all its voices for music).
+# TEST\MUSTEST.APP writes a MIDI file (a flute: A4, E5, A5, 400 ms each,
+# 300 ms apart) and plays it after a 2000 Hz sync tone; it must play on
+# the GUS's voices and end on time (# LOG). The recording must match the
+# golden (# AUDIO).
+# AUDIO-CARD: sb16gus
+# ADDON: dvxgm
+# AUDIO: 2000 440 659 880
+# LOG: Audio: using 'sb': Sound Blaster 16
+# LOG: MUSTEST: wave: MIDI on 5: Gravis UltraSound wavetable at 240h
+# LOG: MUSTEST: wave PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP wave
+WAITLOG "MUSTEST: wave done" 90
+QUIT
diff --git a/src/test/dos/scripts/lptcovox.scr b/src/test/dos/scripts/lptcovox.scr
new file mode 100644
index 00000000..5d8b3d60
--- /dev/null
+++ b/src/test/dos/scripts/lptcovox.scr
@@ -0,0 +1,18 @@
+# lptcovox.scr -- sound through a Covox Speech Thing, recorded.
+#
+# No sound card; a Covox on LPT1 (# AUDIO-CARD). A Covox cannot be
+# found, so DVX.INI names it (lptDac covox); lpt.drv writes a sample to
+# the port on every timer interrupt at the mixing rate. TEST\SNDTEST.APP
+# plays its "full" timeline; every sound must end on time and the
+# recording must match the golden.
+# AUDIO-CARD: covox
+# AUDIO: 2000 440 880 660 330 1320
+# LOG: SNDTEST: full: device lpt: Covox at 378h
+# LOG: SNDTEST: full PASS
+# INI: audio lptDac covox
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/SNDTEST.APP full
+WAITLOG "SNDTEST: full done" 90
+QUIT
diff --git a/src/test/dos/scripts/lptdss.scr b/src/test/dos/scripts/lptdss.scr
new file mode 100644
index 00000000..c5dd1523
--- /dev/null
+++ b/src/test/dos/scripts/lptdss.scr
@@ -0,0 +1,17 @@
+# lptdss.scr -- sound through a Disney Sound Source, recorded.
+#
+# No sound card; a Disney Sound Source on LPT1 (# AUDIO-CARD), which
+# lpt.drv finds by itself (lptDac auto: its FIFO fills and drains) at the
+# port the BIOS lists, and feeds from the timer at its own 7 kHz.
+# TEST\SNDTEST.APP plays its "full" timeline; every sound must end on
+# time and the recording must match the golden.
+# AUDIO-CARD: disney
+# AUDIO: 2000 440 880 660 330 1320
+# LOG: SNDTEST: full: device lpt: Disney Sound Source at 378h
+# LOG: SNDTEST: full PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/SNDTEST.APP full
+WAITLOG "SNDTEST: full done" 90
+QUIT
diff --git a/src/test/dos/scripts/lptst86.scr b/src/test/dos/scripts/lptst86.scr
new file mode 100644
index 00000000..b1e24b1a
--- /dev/null
+++ b/src/test/dos/scripts/lptst86.scr
@@ -0,0 +1,20 @@
+# lptst86.scr -- sound through 86Box's stereo LPT DAC, recorded.
+#
+# No sound card; 86Box's "Stereo LPT DAC" on LPT1 (# AUDIO-CARD ston1),
+# which takes a sample for the left channel with STROBE high and for the
+# right with it low: the Stereo-on-1's writes play on it, but it has no
+# D7 to BUSY wire to be found by, so DVX.INI names it (lptDac stereo).
+# TEST\SNDTEST.APP plays its "full" timeline; every sound must end on
+# time and the recording must match the golden.
+# ONLY: 86box
+# AUDIO-CARD: ston1
+# AUDIO: 2000 440 880 660 330 1320
+# LOG: SNDTEST: full: device lpt: Stereo-on-1 at 378h
+# LOG: SNDTEST: full PASS
+# INI: audio lptDac stereo
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/SNDTEST.APP full
+WAITLOG "SNDTEST: full done" 90
+QUIT
diff --git a/src/test/dos/scripts/lptston.scr b/src/test/dos/scripts/lptston.scr
new file mode 100644
index 00000000..3f02d6b6
--- /dev/null
+++ b/src/test/dos/scripts/lptston.scr
@@ -0,0 +1,19 @@
+# lptston.scr -- sound through a Stereo-on-1, recorded.
+#
+# No sound card; a Stereo-on-1 on LPT1 (# AUDIO-CARD), which lpt.drv
+# finds by itself (lptDac auto: D7 wired back to BUSY) and feeds a frame,
+# left then right, on every timer interrupt. DOSBox only: 86Box's stereo
+# LPT DAC is a different device (lptst86.scr). TEST\SNDTEST.APP plays
+# its "full" timeline; every sound must end on time and the recording
+# must match the golden.
+# ONLY: dosbox
+# AUDIO-CARD: ston1
+# AUDIO: 2000 440 880 660 330 1320
+# LOG: SNDTEST: full: device lpt: Stereo-on-1 at 378h
+# LOG: SNDTEST: full PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/SNDTEST.APP full
+WAITLOG "SNDTEST: full done" 90
+QUIT
diff --git a/src/test/dos/scripts/midiin.scr b/src/test/dos/scripts/midiin.scr
new file mode 100644
index 00000000..3835a72f
--- /dev/null
+++ b/src/test/dos/scripts/midiin.scr
@@ -0,0 +1,28 @@
+# midiin.scr -- a MIDI keyboard into the Sound Recorder: DVX.INI [audio]
+# midiIn = mpu401 opens the Sound Blaster 16's MPU-401 port for input.
+# MIDI Keyboard (Options, K) can be chosen; Record opens the port's
+# input (the log). Nothing comes in on an emulator, so Stop closes the
+# input with nothing lost and leaves no recording to save (rc-midi: the
+# window after Stop, the format MIDI).
+# AUDIO-CARD: sb16
+# INI: audio midiIn mpu401
+# LOG: Audio: MIDI input open
+# LOG: Audio: MIDI input closed (0 bytes lost)
+TIMEOUT 90
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH apps/kpunch/recorder/recorder.app
+WAITWIN "New recording - Sound Recorder"
+WAIT 10
+KEY Alt+O
+KEY K
+KEY Alt+R
+WAITLOG "Audio: MIDI input open"
+WAITMS 1000
+KEY Alt+T
+WAITLOG "Audio: MIDI input closed"
+WAIT 10
+SHOT rc-midi
+KEY Alt+F4
+WAITNOWIN "*Sound Recorder"
+QUIT
diff --git a/src/test/dos/scripts/player.scr b/src/test/dos/scripts/player.scr
new file mode 100644
index 00000000..69d0d418
--- /dev/null
+++ b/src/test/dos/scripts/player.scr
@@ -0,0 +1,46 @@
+# player.scr -- Media Player plays a WAV file, a module, a MIDI file, and
+# a VOC file.
+#
+# The files are written into TEST\ as the run is staged (mkMedia.py).
+# Started with PLAYER.WAV, the player streams it from the disk to its end:
+# a 2000 Hz sync tone, then A4, E5, and A5, recorded and compared with the
+# golden (# AUDIO). It stops there (the last track, Repeat Off) and is
+# captured. The volume slider (Alt+U) then goes to 0, so the recording
+# holds the WAV only, and each of the other files is handed to the running
+# player by another start (NewArgs): it is added to the list and plays,
+# Ctrl+S stops it, and the window is captured (its title: the module's
+# own, the MIDI file's track name, the VOC's file name; what it is and
+# where it plays).
+# AUDIO: 2000 440 659 880
+TIMEOUT 60
+WAITWIN "Program Manager"
+MOVE 639 479
+
+LAUNCH apps/kpunch/player/player.app TEST/PLAYER.WAV
+WAITWIN "PLAYER.WAV - Media Player"
+WAITMS 6000
+SHOT p-wav
+KEY Alt+U
+KEY Home
+
+LAUNCH apps/kpunch/player/player.app TEST/PLAYER.MOD
+WAITWIN "Player Test Module - Media Player"
+KEY Ctrl+S
+WAIT 10
+SHOT p-mod
+
+LAUNCH apps/kpunch/player/player.app TEST/PLAYER.MID
+WAITWIN "Player Test MIDI - Media Player"
+KEY Ctrl+S
+WAIT 10
+SHOT p-mid
+
+LAUNCH apps/kpunch/player/player.app TEST/PLAYER.VOC
+WAITWIN "PLAYER.VOC - Media Player"
+KEY Ctrl+S
+WAIT 10
+SHOT p-voc
+
+KEY Alt+F4
+WAITNOWIN "*Media Player"
+QUIT
diff --git a/src/test/dos/scripts/prdjcol.scr b/src/test/dos/scripts/prdjcol.scr
new file mode 100644
index 00000000..d5268a6d
--- /dev/null
+++ b/src/test/dos/scripts/prdjcol.scr
@@ -0,0 +1,18 @@
+# prdjcol.scr -- the Printer object on the DeskJet 550C: the page in
+# four PCL planes (black, cyan, magenta, yellow).
+#
+# TEST\PRDRAW.APP (fixtures/prdraw) draws a page with the Printer object
+# and ends the document; DVX.INI sends it to the file TEST\PRDJCOL.PRN,
+# which is compared with golden/prdjcol/prdjcol.prn.
+#
+# INI: printer model deskjetcolor
+# INI: printer port FILE
+# INI: printer file TEST\PRDJCOL.PRN
+# EXPECT: TEST\PRDJCOL.PRN prdjcol.prn
+TIMEOUT 90
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/PRDRAW.APP
+WAITLOG "Print: finished,"
+WAITWIN "Printer done" 60
+QUIT
diff --git a/src/test/dos/scripts/print.scr b/src/test/dos/scripts/print.scr
index 5b8b44f1..19b63b2d 100644
--- a/src/test/dos/scripts/print.scr
+++ b/src/test/dos/scripts/print.scr
@@ -12,8 +12,9 @@ LAUNCH apps/kpunch/notepad/notepad.app TEST\PRTEST.TXT
WAITWIN "*PRTEST.TXT - Notepad"
KEY Alt+F P
WAITWIN "Print"
-# Print to: the drop-down has the focus; File is the fourth entry
-KEY Down Down Down
+# Print to: the drop-down has the focus; F picks File (after the LPT and
+# COM ports)
+TYPE "F"
KEY Tab
KEY Ctrl+A
TYPE "TEST\PRINT.PRN"
diff --git a/src/test/dos/scripts/prlj300.scr b/src/test/dos/scripts/prlj300.scr
new file mode 100644
index 00000000..8f5b757a
--- /dev/null
+++ b/src/test/dos/scripts/prlj300.scr
@@ -0,0 +1,18 @@
+# prlj300.scr -- the Printer object on the 300 dpi LaserJet: the page as
+# packed PCL rows, black only, its text in DVX's own fonts.
+#
+# TEST\PRDRAW.APP (fixtures/prdraw) draws a page with the Printer object
+# and ends the document; DVX.INI sends it to the file TEST\PRLJ300.PRN,
+# which is compared with golden/prlj300/prlj300.prn.
+#
+# INI: printer model laserjet300
+# INI: printer port FILE
+# INI: printer file TEST\PRLJ300.PRN
+# EXPECT: TEST\PRLJ300.PRN prlj300.prn
+TIMEOUT 90
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/PRDRAW.APP
+WAITLOG "Print: finished,"
+WAITWIN "Printer done" 60
+QUIT
diff --git a/src/test/dos/scripts/prps.scr b/src/test/dos/scripts/prps.scr
new file mode 100644
index 00000000..ef70965f
--- /dev/null
+++ b/src/test/dos/scripts/prps.scr
@@ -0,0 +1,19 @@
+# prps.scr -- the Printer object on a color PostScript printer: the
+# page drawn by the printer itself (vector PostScript), its text in the
+# printer's Times and Helvetica.
+#
+# TEST\PRDRAW.APP (fixtures/prdraw) draws a page with the Printer object
+# and ends the document; DVX.INI sends it to the file TEST\PRPS.PRN,
+# which is compared with golden/prps/prps.prn.
+#
+# INI: printer model postscriptcolor
+# INI: printer port FILE
+# INI: printer file TEST\PRPS.PRN
+# EXPECT: TEST\PRPS.PRN prps.prn
+TIMEOUT 90
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/PRDRAW.APP
+WAITLOG "Print: finished,"
+WAITWIN "Printer done" 60
+QUIT
diff --git a/src/test/dos/scripts/record.scr b/src/test/dos/scripts/record.scr
new file mode 100644
index 00000000..6d481f22
--- /dev/null
+++ b/src/test/dos/scripts/record.scr
@@ -0,0 +1,42 @@
+# record.scr -- the Sound Recorder records from the sound card, plays
+# the recording, and saves it.
+#
+# The window as it opens (rc-open). Record (Alt+R) takes the card for
+# recording (the log names the format); two seconds later Stop (Alt+T)
+# gives it back (the log counts the frames, which depend on the emulated
+# card, so only the lines are checked: # LOG). Play (Alt+P), stopped;
+# File > Save As writes REC.WAV (in the folder the dialog opens in) and
+# the title follows. An emulated card records silence, so the sound
+# itself is not compared (# AUDIO-CARD: the Sound Blaster 16, recorded
+# but not compared).
+# AUDIO-CARD: sb16
+# LOG: Audio: recording at
+# LOG: Audio: recording ended
+# NOLOG: did not start recording
+# NOLOG: no interrupt from the card
+TIMEOUT 90
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH apps/kpunch/recorder/recorder.app
+WAITWIN "New recording - Sound Recorder"
+WAIT 10
+SHOT rc-open
+KEY Alt+R
+WAITLOG "Audio: recording at"
+WAITMS 2000
+KEY Alt+T
+WAITLOG "Audio: recording ended"
+WAITWIN "New recording (not saved) - Sound Recorder"
+KEY Alt+P
+WAITMS 500
+KEY Alt+T
+KEY Alt+F
+KEY A
+WAITWIN "Save As"
+KEY Alt+N
+TYPE "REC.WAV"
+KEY Enter
+WAITWIN "REC.WAV - Sound Recorder"
+KEY Alt+F4
+WAITNOWIN "*Sound Recorder"
+QUIT
diff --git a/src/test/dos/scripts/saver.scr b/src/test/dos/scripts/saver.scr
new file mode 100644
index 00000000..a5d843eb
--- /dev/null
+++ b/src/test/dos/scripts/saver.scr
@@ -0,0 +1,68 @@
+# saver.scr -- the screen saver: after the wait with no input it takes
+# the whole screen, and the next input gives the screen back.
+#
+# DVX.INI chooses Blank Screen with a one-minute wait (# INI). With no
+# input the shell starts it (the log says so) and it shows a black
+# screen in full-screen mode (sv-blank); a key ends it (the log), and the
+# desktop is back as it was, the key not passed to Program Manager
+# (VIDEOCHECK, sv-back). Then Control Panel's Desktop page: its saver
+# row (sv-cpd), and Test with each of the other savers (Marquee,
+# Mystify, and Starfield, picked with Down and Enter in the list): each
+# starts, draws for a while, and a key ends it. Closing Control Panel
+# cancels.
+# INI: desktop saver blank.app
+# INI: desktop saverWait 1
+TIMEOUT 240
+WAITWIN "Program Manager"
+MOVE 639 479
+WAITLOG "Shell: screen saver blank.app started"
+WAITMS 2000
+SHOT sv-blank
+KEY Space
+WAITLOG "Shell: screen saver blank.app ended"
+WAIT 20
+VIDEOCHECK
+SHOT sv-back
+
+LAUNCH apps/kpunch/cpanel/cpanel.app
+WAITWIN "Control Panel"
+WAIT 10
+CLICK "Control Panel" 38 59
+MOVE 639 479
+KEY Right
+KEY Right
+WAIT 10
+KEY Alt+V
+KEY Down
+KEY Enter
+WAIT 10
+SHOT sv-cpd
+KEY Alt+T
+WAITLOG "Shell: screen saver marquee.app started"
+WAITMS 3000
+KEY Space
+WAITLOG "Shell: screen saver marquee.app ended"
+WAITWIN "Control Panel"
+KEY Alt+V
+KEY Down
+KEY Enter
+KEY Alt+T
+WAITLOG "Shell: screen saver mystify.app started"
+WAITMS 3000
+KEY Space
+WAITLOG "Shell: screen saver mystify.app ended"
+WAITWIN "Control Panel"
+KEY Alt+V
+KEY Down
+KEY Enter
+KEY Alt+T
+WAITLOG "Shell: screen saver starfld.app started"
+WAITMS 3000
+KEY Space
+WAITLOG "Shell: screen saver starfld.app ended"
+WAITWIN "Control Panel"
+WAIT 20
+VIDEOCHECK
+KEY Alt+F4
+WAITNOWIN "Control Panel"
+QUIT
diff --git a/src/test/dos/scripts/sndload.scr b/src/test/dos/scripts/sndload.scr
new file mode 100644
index 00000000..3a035952
--- /dev/null
+++ b/src/test/dos/scripts/sndload.scr
@@ -0,0 +1,18 @@
+# sndload.scr -- how much of a 486 the sound takes (a measurement).
+#
+# A Gravis UltraSound alone (# AUDIO-CARD, recorded but not compared).
+# TEST\MUSTEST.APP load runs a busy loop for two seconds with nothing
+# playing, then with eight looping sample voices (mixed in software into
+# the GUS's digital sound), then with a four-channel module (on the GUS's
+# own voices); DVX.LOG gets each one's work and its share of the CPU
+# ("MUSTEST: load: ..."). Nothing here fails on a slow run; the numbers
+# are recorded in claudeDocs/soundCardsPlan.md (S7).
+# ONLY: 86box
+# AUDIO-CARD: gus
+# LOG: MUSTEST: load: a 4-channel module on the wavetable synthesizer
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP load
+WAITLOG "MUSTEST: load done" 90
+QUIT
diff --git a/src/test/dos/scripts/softwave.scr b/src/test/dos/scripts/softwave.scr
new file mode 100644
index 00000000..4cdf515b
--- /dev/null
+++ b/src/test/dos/scripts/softwave.scr
@@ -0,0 +1,21 @@
+# softwave.scr -- a MIDI file on the SoundFont software synthesizer,
+# recorded.
+#
+# A Sound Blaster 16 (# AUDIO-CARD default) with the DVX General MIDI
+# SoundFont add-on installed (# ADDON) and DVX.INI [audio] midi =
+# softwave (# INI). TEST\MUSTEST.APP writes a MIDI file (a flute: A4,
+# E5, A5, 400 ms each, 300 ms apart) and plays it after a 2000 Hz sync
+# tone: it must play on the SoundFont software synthesizer (the flute's
+# samples in memory, mixed into the sound stream) and end on time
+# (# LOG). The recording must match the golden (# AUDIO).
+# ADDON: dvxgm
+# INI: audio midi softwave
+# AUDIO: 2000 440 659 880
+# LOG: MUSTEST: softwave: MIDI on 6: SoundFont software synthesizer
+# LOG: MUSTEST: softwave PASS
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/MUSTEST.APP softwave
+WAITLOG "MUSTEST: softwave done" 90
+QUIT
diff --git a/src/test/dos/scripts/svdbg.scr b/src/test/dos/scripts/svdbg.scr
new file mode 100644
index 00000000..f29a6c8b
--- /dev/null
+++ b/src/test/dos/scripts/svdbg.scr
@@ -0,0 +1,20 @@
+# svdbg.scr -- TEMPORARY: how long a full-screen saver takes to close.
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH shared/savers/blank.app
+WAITLOG "basrt: program screen full screen on"
+WAITMS 2000
+LOGTIME blank-close
+KEY Alt+F4
+WAITNOWIN "Blank Screen"
+LOGTIME blank-gone
+WAITMS 1000
+LAUNCH shared/savers/starfld.app
+WAITWIN "Starfield"
+WAITMS 2000
+LOGTIME star-close
+KEY Alt+F4
+WAITNOWIN "Starfield"
+LOGTIME star-gone
+QUIT
diff --git a/src/test/dos/scripts/wss.scr b/src/test/dos/scripts/wss.scr
new file mode 100644
index 00000000..2c80a230
--- /dev/null
+++ b/src/test/dos/scripts/wss.scr
@@ -0,0 +1,20 @@
+# wss.scr -- sound through a Windows Sound System board, recorded.
+#
+# 86Box's Windows Sound System (# AUDIO-CARD): an AD1848 codec behind
+# Microsoft's configuration register at 530h, no Sound Blaster. wss.drv
+# finds the codec, routes it to IRQ 7 and DMA 1, and plays 16-bit stereo
+# at 22,050 Hz. TEST\SNDTEST.APP plays its "full" timeline; every sound
+# must end on time and the recording must match the golden. DOSBox has
+# no Windows Sound System.
+# ONLY: 86box
+# AUDIO-CARD: wss
+# AUDIO: 2000 440 880 660 330 1320
+# LOG: SNDTEST: full: device wss: Windows Sound System (AD1848 codec) at 530h
+# LOG: SNDTEST: full PASS
+# INI: audio fmTones false
+TIMEOUT 120
+WAITWIN "Program Manager"
+MOVE 639 479
+LAUNCH TEST/SNDTEST.APP full
+WAITLOG "SNDTEST: full done" 90
+QUIT
diff --git a/src/test/formrt/testFormRt.c b/src/test/formrt/testFormRt.c
index 61feebe4..065744f4 100644
--- a/src/test/formrt/testFormRt.c
+++ b/src/test/formrt/testFormRt.c
@@ -341,6 +341,7 @@ static void caseDragDrop(void);
static void caseDrawMethods(void);
static void caseDrawPaint(void);
static void caseDrawScale(void);
+static void caseDrawStyle(void);
static void caseDrawThemed(void);
static void caseDynamicControl(void);
static void caseEditorMembers(void);
@@ -2965,6 +2966,71 @@ static void caseDrawScale(void) {
}
+// DrawStyle on a PictureBox: dashes and dots along lines and circles
+// (solid with a wider pen), no outlines when invisible (fills still
+// drawn), and the error for a bad value.
+static void caseDrawStyle(void) {
+ tCase("DrawStyle");
+ tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
+
+ const char *frm =
+ "Begin Form Form1\n"
+ " Caption = \"Style\"\n"
+ " Begin PictureBox Pic\n"
+ " End\n"
+ "End\n"
+ "Sub Form1_T ()\n"
+ " Pic.Resize 60, 40\n"
+ " Pic.BackColor = RGB(255, 255, 255)\n"
+ " Pic.Cls\n"
+ " Print Pic.DrawStyle\n"
+ " Pic.DrawStyle = vbDash\n"
+ " Pic.Line (0, 5)-(59, 5), RGB(0, 0, 0)\n"
+ " Print Pic.Point(2, 5); Pic.Point(13, 5); Pic.Point(17, 5)\n"
+ " Pic.DrawStyle = vbDot\n"
+ " Pic.Line (0, 8)-(59, 8)\n"
+ " Print Pic.Point(0, 8); Pic.Point(2, 8)\n"
+ " Pic.DrawWidth = 3\n"
+ " Pic.Line (0, 12)-(59, 12)\n"
+ " Print Pic.Point(13, 12)\n"
+ " Pic.DrawWidth = 1\n"
+ " Pic.DrawStyle = vbInvisible\n"
+ " Pic.Line (0, 16)-(59, 16)\n"
+ " Print Pic.Point(5, 16)\n"
+ " Pic.FillStyle = vbFSSolid\n"
+ " Pic.FillColor = RGB(255, 0, 0)\n"
+ " Pic.Line (10, 20)-(20, 30), , B\n"
+ " Print Pic.Point(15, 25); Pic.Point(10, 25)\n"
+ " Pic.FillStyle = vbFSTransparent\n"
+ " Pic.DrawStyle = vbDash\n"
+ " Pic.Circle (45, 25), 10\n"
+ " Print Pic.Point(55, 25); Pic.Point(45, 15); Pic.DrawStyle\n"
+ " Pic.DrawStyle = 7\n"
+ "End Sub\n";
+ FrtAppT app;
+
+ if (!expectBuild(&app, "", &frm, 1)) {
+ tShutdown();
+ return;
+ }
+
+ frtStart(&app, NULL);
+ frtFire(&app, "Form1", "T");
+ TEST_STR_EQ(frtOut(&app),
+ "0 \n"
+ "0 16777215 0 \n"
+ "0 16777215 \n"
+ "0 \n"
+ "16777215 \n"
+ "255 255 \n"
+ "0 16777215 1 \n");
+ TEST_TRUE(app.rt->terminated);
+ frtClearError(&app);
+ frtDestroy(&app);
+ tShutdown();
+}
+
+
// The drawing members that draw DVX's own look through libdvx on a
// PictureBox (DrawBevel, DrawButtonFace, DrawArrow, DrawScrollBar upright
// and horizontal, DrawScrollArrow, PrintAccel), the measuring ones
@@ -10709,6 +10775,7 @@ int main(void) {
caseScrollPaneChange();
caseDrawPaint();
caseDrawScale();
+ caseDrawStyle();
caseEventLoopExits();
caseExternCalls();
caseExternMarshal();
diff --git a/src/test/ide/Makefile b/src/test/ide/Makefile
index 9b00aeec..33876db1 100644
--- a/src/test/ide/Makefile
+++ b/src/test/ide/Makefile
@@ -61,7 +61,7 @@ STUBTARGET := $(BINDIR)/testStub
IDE_SRCS := ideDesigner.c ideProperties.c ideProject.c ideMenuEditor.c ideToolbox.c idePackage.c
COMP_SRCS := lexer.c ast.c scope.c parse.c parseExpr.c parseStmt.c parseDecl.c resolve.c resolveExpr.c typecheck.c fold.c gen.c genExpr.c genStmt.c compiler.c codegen.c strip.c obfuscate.c compact.c
RT_SRCS := vm.c values.c dates.c serialize.c gfx.c screen.c
-FRM_SRCS := formrt.c formGfx.c formDesign.c formCtl.c frmParser.c frmModel.c frmModelBas.c basNativeHost.c basRunHost.c
+FRM_SRCS := formrt.c formGfx.c formPrinter.c formDesign.c formCtl.c frmParser.c frmModel.c frmModelBas.c basNativeHost.c basRunHost.c
DEV_SRCS := basValidate.c basDebug.c basProcs.c basSource.c basHelpMap.c basProject.c
OBJS := $(BINDIR)/testIde.o $(BINDIR)/ideStubs.o
diff --git a/src/test/lint/analyzerBaseline.txt b/src/test/lint/analyzerBaseline.txt
index 8e1b91cd..678e2e9a 100644
--- a/src/test/lint/analyzerBaseline.txt
+++ b/src/test/lint/analyzerBaseline.txt
@@ -6,8 +6,10 @@
# staticGate.sh --update keeps these comments with their keys, drops
# keys whose count is now 0, and marks new or grown keys UNTRIAGED.
-# FP: value is stored via arrput in sFileValues (freed on page close); stb_ds OOM hook never returns, so arrput cannot fail and drop it.
-1 src/apps/kpunch/cpanel/cpSounds.c|addFile|leak of [CWE-401] [-Wanalyzer-malloc-leak]
+# FP: the strdup'd value is stored via arrput in c->values (freed with the choices); the stb_ds OOM hook never returns, so arrput cannot fail and drop it.
+1 src/apps/kpunch/cpanel/cpSounds.c|choiceAdd|leak of [CWE-401] [-Wanalyzer-malloc-leak]
+# FP: the strdup'd label is stored via arrput in c->owned (freed with the choices), or freed at once; the stb_ds OOM hook never returns, so arrput cannot drop it.
+1 src/apps/kpunch/cpanel/cpSounds.c|choiceSetLabel|leak of [CWE-401] [-Wanalyzer-malloc-leak]
# FP: the malloc is stored in the module being built (mod->classes / mod->withEvents entries or an interface map), which basModuleFree releases on success and on every failure path; the analyzer loses the store into the caller-owned module.
3 src/apps/kpunch/dvxbasic/compiler/compiler.c|buildWithEvents|leak of [CWE-401] [-Wanalyzer-malloc-leak]
# FP: the malloc is stored in sw->full (sw is rt->screenWin, heap), freed by screenModeChanged and basFormRtScreenClose; analyzer loses the store into the caller's heap struct.
@@ -28,7 +30,7 @@
4 src/apps/kpunch/dvxbasic/runtime/vm.c|execStringOp|use of uninitialized value [CWE-457] [-Wanalyzer-use-of-uninitialized-value]
# FP: copy is stored via arrins at m->history[0] (trimmed by the pop loop, freed with the model); stb_ds grow aborts rather than failing.
1 src/libs/kpunch/dvxshell/pmModel.c|pmHistoryAdd|leak of [CWE-401] [-Wanalyzer-malloc-leak]
-# FP: allocSlot appends the calloc'd slot to sApps with arrput; the stb_ds OOM hook never returns, so the slot is never dropped (freed with the slot index).
+# FP: allocSlot (inlined) stores the calloc'd ShellAppT via arrput in sApps (slots live as long as the shell and are reused); the stb_ds OOM hook never returns, so arrput cannot drop it.
1 src/libs/kpunch/dvxshell/shellApp.c|shellLoadAppInternal|leak of [CWE-401] [-Wanalyzer-malloc-leak]
# FP: dialogCreate stores &state->base (the first member) as the window's userData; dvxDialogRun frees it, and every caller (fnShowLicence included) runs the dialog it creates.
1 src/libs/kpunch/libdvx/dvxDialog.c|dvxDialogCreate|leak of [CWE-401] [-Wanalyzer-malloc-leak]
@@ -42,16 +44,18 @@
1 src/libs/kpunch/libdvx/dvxText.c|idleTrim|dereference of NULL [CWE-476] [-Wanalyzer-null-dereference]
# FP: scanFonts stores sourceMake's allocation in face->src, an element of the global sFamilies stb_ds array (faceFree releases it); the analyzer loses the pointer in the array. Unchanged loop; the path set moved when the font list came from platformKindReadDir (never NULL).
1 src/libs/kpunch/libdvx/dvxText.c|scanFonts.part.0|leak of [CWE-401] [-Wanalyzer-malloc-leak]
-# FP: workArea gets width * height of a glyph whose width and height (uint8_t) are both positive on the paths that use the area, so the stb_ds array is allocated; the path assumes the area <= 0 and then enters the row loop.
-1 src/libs/kpunch/libdvx/dvxText.c|workArea|use of NULL where non-null expected [CWE-476] [-Wanalyzer-null-argument]
# FP: name is stored via arrput into the returned array (freed by dvxReadDirFree); was the stb_ds.h|dvxReadDir key.
3 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|dvxReadDir|leak of [CWE-401] [-Wanalyzer-malloc-leak]
+# FP: each name (dvxReadDir's, or a strdup'd VENDOR\NAME) moves into the returned list by arrput, which platformKindReadDir hands on or frees; the stb_ds OOM hook never returns.
+3 src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c|kindList|leak of [CWE-401] [-Wanalyzer-malloc-leak]
# FP: same arrput store as ideProperties.c|addPropRow, reported at the stb_ds macro expansion.
1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|addPropRow|leak of [CWE-401] [-Wanalyzer-malloc-leak]
# FP: li->count > idx guarantees widgetAdoptOwnedStrings copies count > 0 entries; the path needs its loop to run 0 times. Reached directly and via listPopupRemoveItem; the analyzer reports the path once or twice from run to run.
2 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|listItemsRemove|dereference of NULL [CWE-476] [-Wanalyzer-null-dereference]
# FP: strdup("") is stored via arrput in remapped, which becomes lv->ownedCells (freed with the widget).
1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|listViewRemapOwnedCells|leak of [CWE-401] [-Wanalyzer-malloc-leak]
+# FP: scanDir grows the caller's module list with arrput (inside stb_ds's arrgrow); the list is the loader's for the whole run, and the analyzer loses the store into *mods.
+1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|loadAllModules|leak of [CWE-401] [-Wanalyzer-malloc-leak]
# FP: row stored into d->rows via arrput; freeCache releases it (the real partial-load leak was fixed: freeCache walks arrlen).
1 src/libs/kpunch/libdvx/thirdparty/stb_ds.h|loadCache|leak of [CWE-401] [-Wanalyzer-malloc-leak]
# FP: copy/m.path are stored in sMemFiles via arrput and held on purpose until DVX exits (read-only media keeps settings in memory); stb_ds OOM hook never returns.
diff --git a/src/test/lint/clangTidyBaseline.txt b/src/test/lint/clangTidyBaseline.txt
index 8ff32475..c39cfa25 100644
--- a/src/test/lint/clangTidyBaseline.txt
+++ b/src/test/lint/clangTidyBaseline.txt
@@ -22,6 +22,8 @@
1 src/apps/kpunch/dvxbasic/formrt/frmModel.c|clang-analyzer-core.NullDereference|Access to field 'level' results in a dereference of a null pointer (loaded from variable 'mi')
# FP: frmModelReparent refuses a new parent inside ctrl: frmModelIsDescendant(m, parent, ctrl) asks whether parent descends from ancestor ctrl, the intended order
1 src/apps/kpunch/dvxbasic/formrt/frmModel.c|readability-suspicious-call-argument|2nd argument 'parent' (passed to 'ctrl') looks like it might be swapped with the 3rd, 'ctrl' (passed to 'ancestor')
+# FP: pkgDpkFolderFiles(folder, prefix, out) reads the staged files from scratch and names them under the package's install folder (the prefix), its parameter order; only the names differ
+1 src/apps/kpunch/dvxbasic/ide/idePackage.c|readability-suspicious-call-argument|1st argument 'scratch' (passed to 'folder') looks like it might be swapped with the 2nd, 'folder' (passed to 'prefix')
# FP: the project tree keeps a file index (or -1 for group rows) in the widget's void *userData, the only per-item slot the tree widget offers; it is cast back, never dereferenced
3 src/apps/kpunch/dvxbasic/ide/ideProject.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities
# FP: validateNewName(newValue, max, oldName): the new name is the one validated and oldName the one excepted from the in-use check; the order is right
@@ -76,6 +78,8 @@
1 src/libs/kpunch/dvxshell/shellApp.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities
# FP: the BASIC hotkey id is passed as dvxRegisterHotkey's user pointer on purpose
1 src/libs/kpunch/dvxshell/shellBas.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities
+# FP: bodyWrite gets at most the bytes the socket read into the 4096-byte buffer (a body takes part of them), never more
+1 src/libs/kpunch/http/http.c|clang-analyzer-unix.StdCLibraryFunctions|The 1st argument to 'fwrite' is a buffer with size 4096 but should be a buffer with size equal to or greater than the value of the 2nd argument (which is 1) times the 3rd argument
# FP: platformAltScanToChar returns 0 or an ASCII letter or digit from its table, never a negative char
1 src/libs/kpunch/libdvx/dvxApp.c|bugprone-signed-char-misuse,cert-str34-c|'signed char' to 'int32_t' (aka 'int') conversion; consider casting to 'unsigned char' first.
# FP: executeSysMenuCmd switches on the SysMenu*E ids openSysMenu put in the menu; handleMouseButton leaves the other HIT_* parts (none, border) without action on purpose
@@ -94,6 +98,8 @@
1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp|declaration uses identifier '__wrap_abort', which is a reserved identifier
# FP: GNU ld --wrap=exit (HOST_APP_LDFLAGS) requires exactly the name __wrap_exit
1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp|declaration uses identifier '__wrap_exit', which is a reserved identifier
+# FP: platformDosRun runs a DOS command line by definition (the host stands in for DOS with system(), as the DOS build does)
+1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|cert-env33-c|calling 'system' uses a command processor
# FP: platformVideoExclusiveCapture's signature is the platform interface; the DOS layer writes out, the host stub has nothing to write
1 src/libs/kpunch/libdvx/platform/dvxPlatformHost.c|readability-non-const-parameter|pointer parameter 'out' can be pointer to const
# FP: platformMkdirRecursive returns -1 when strlen(path) >= sizeof(buf), the test right before the strcpy
@@ -104,6 +110,14 @@
1 src/libs/kpunch/libtasks/taskswitch.c|performance-no-int-to-ptr|integer to pointer cast pessimizes optimization opportunities
# FP: sVibrato is a signed int8_t table of pitch steps (-2..2); the sign is meant
1 src/libs/kpunch/music/musicSynth.c|bugprone-signed-char-misuse,cert-str34-c|'signed char' to 'int32_t' (aka 'int') conversion; consider casting to 'unsigned char' first.
+# FP: platformPathJoin(out, size, dir, name) builds the SYSTEM folder (sys) and then the database folder in it, written to the variable named dir; only the names differ
+1 src/libs/kpunch/pkg/pkgDb.c|readability-suspicious-call-argument|1st argument 'dir' (passed to 'out') looks like it might be swapped with the 3rd, 'sys' (passed to 'dir')
+# FP: joinName(out, size, dir, name) writes the package name of a file: the prefix, then its path inside the folder (sub), to the variable named name; only the names differ
+1 src/libs/kpunch/pkg/pkgDpk.c|readability-suspicious-call-argument|1st argument 'name' (passed to 'out') looks like it might be swapped with the 4th, 'sub' (passed to 'name')
+# FP: platformPathJoin(out, size, dir, name) builds the SYSTEM folder (sys) and then the keys folder in it, written to the variable named dir; only the names differ
+1 src/libs/kpunch/pkg/pkgTrust.c|readability-suspicious-call-argument|1st argument 'dir' (passed to 'out') looks like it might be swapped with the 3rd, 'sys' (passed to 'dir')
+# FP: FontGlyphT's left and top are signed int8_t pixel offsets (a bearing or a glyph's top can be negative); the sign is meant
+2 src/libs/kpunch/print/printFont.c|bugprone-signed-char-misuse,cert-str34-c|'signed char' to 'int32_t' (aka 'int') conversion; consider casting to 'unsigned char' first.
# FP: outLine builds a length-delimited line (margin, text, CR LF) that outBytes writes with its length; no NUL is wanted
1 src/libs/kpunch/print/printText.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated
# FP: the frame-type switch drops unknown types from the wire on purpose, and rxState only ever holds RX_STATE_HUNT, ACTIVE or ESCAPE, each with a case
@@ -118,6 +132,10 @@
1 src/tools/dvxhlpc.c|bugprone-not-null-terminated-result|the result from calling 'memcpy' is not null-terminated
# FP: _POSIX_C_SOURCE is the POSIX feature-test macro; the program must define it before any include
1 src/tools/dvxhlpc.c|bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp|declaration uses identifier '_POSIX_C_SOURCE', which is a reserved identifier
+# FP: chunkEnd: the path assumes arrlen(*out) found no array, but chunkBegin always put the chunk's 8 header bytes there first, so the array the length goes into exists
+1 src/tools/mksf.c|clang-analyzer-core.NullDereference|Array access results in a null pointer dereference
+# FP: pickSlots: the path takes max (a step's regions) as 0, but every step in sMelodicSteps and sDrumSteps keeps at least 1 and fitSlots only calls it with count > 0, so mids[0] is always written
+1 src/tools/mksf.c|clang-analyzer-core.UndefinedBinaryOperatorResult|The right operand of '<' is a garbage value
# FP: sTelState only holds the eight TS_* states and each has a case
1 src/tools/proxy/proxy.c|bugprone-switch-missing-default-case|switching on non-enum value without default case may not cover all cases
# FP: parseState only holds PARSE_NORMAL, PARSE_ESC or PARSE_CSI, each with a case
diff --git a/src/test/lint/codespellWords.txt b/src/test/lint/codespellWords.txt
index 4fec884d..4d955109 100644
--- a/src/test/lint/codespellWords.txt
+++ b/src/test/lint/codespellWords.txt
@@ -51,3 +51,11 @@ alph
pard
headerr
caf
+#
+# The singemaster project the package server is built on (server/,
+# SINGE_HOME), and "\tyes" in C strings (a tab, then yes).
+singe
+tyes
+# The AD1848 codec's Auto-Calibrate In-progress bit, as its data sheet
+# names it (wssDrv.c)
+aci
diff --git a/src/test/lint/cppcheckBaseline.txt b/src/test/lint/cppcheckBaseline.txt
index 5faefb4e..dd254b6c 100644
--- a/src/test/lint/cppcheckBaseline.txt
+++ b/src/test/lint/cppcheckBaseline.txt
@@ -9,6 +9,8 @@
# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/apps/kpunch/cpanel/cpSounds.c|constParameterCallback|Parameter 'w' can be declared as pointer to const. However it seems that 'onBankTest' is a callback function, if 'w' is declared with const you might also need to cast function pointer(s).
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
1 src/apps/kpunch/cpanel/cpTypes.c|constParameterCallback|Parameter 'dlg' can be declared as pointer to const. However it seems that 'typeDlgOk' is a callback function, if 'dlg' is declared with const you might also need to cast function pointer(s).
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/apps/kpunch/cpanel/cpTypes.c|unsignedLessThanZero|Checking if unsigned expression '(sCells)?((struct stbds_array_header*)(sCells)-1)->capacity:0' is less than zero.
@@ -21,8 +23,6 @@
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/apps/kpunch/dvxbasic/basdev/basDevBas.c|unsignedLessThanZero|Checking if unsigned expression '(sCompError)?((struct stbds_array_header*)(sCompError)-1)->capacity:0' is less than zero.
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
-1 src/apps/kpunch/dvxbasic/basdev/basDevBas.c|unsignedLessThanZero|Checking if unsigned expression '(sText)?((struct stbds_array_header*)(sText)-1)->capacity:0' is less than zero.
-# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/apps/kpunch/dvxbasic/basdev/basDevBas.c|unsignedLessThanZero|Checking if unsigned expression 'r->breakpoints?((struct stbds_array_header*)(r->breakpoints)-1)->capacity:0' is less than zero.
# FP: style suggestion only (variable scope), no defect.
1 src/apps/kpunch/dvxbasic/compiler/compact.c|variableScope|The scope of the variable 'lineCount' can be reduced.
@@ -56,6 +56,20 @@
1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|constParameterCallback|Parameter 'win' can be declared as pointer to const. However it seems that 'dsgnOnMouse' is a callback function, if 'win' is declared with const you might also need to cast function pointer(s).
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/apps/kpunch/dvxbasic/formrt/formDesign.c|unsignedLessThanZero|Checking if unsigned expression '(sText)?((struct stbds_array_header*)(sText)-1)->capacity:0' is less than zero.
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/apps/kpunch/dvxbasic/formrt/formPrinter.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'prBandSpan' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/apps/kpunch/dvxbasic/formrt/formPrinter.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'prVecDot' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/apps/kpunch/dvxbasic/formrt/formPrinter.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'prVecEllipse' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/apps/kpunch/dvxbasic/formrt/formPrinter.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'prVecEllipseFill' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/apps/kpunch/dvxbasic/formrt/formPrinter.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'prVecLine' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/apps/kpunch/dvxbasic/formrt/formPrinter.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'prVecRect' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
+# FP: prShapeDo draws on the measuring surface, whose span callback (prMeasureSpan) moves p->touchBottom; cppcheck does not follow the callback.
+1 src/apps/kpunch/dvxbasic/formrt/formPrinter.c|knownConditionTrueFalse|Condition 'p->touchBottom<0' is always true
# FP: upperCopy bounds the destination; src[i] is read inside the NUL-terminated source string, so the order is safe.
1 src/apps/kpunch/dvxbasic/formrt/formrt.c|arrayIndexThenCheck|Array index 'i' is used before limits check.
# FP: signature fixed by the BasDeviceCallbacksT frameBusy pointer type (void *ctx shared with callbacks that write through it).
@@ -88,8 +102,6 @@
1 src/apps/kpunch/dvxbasic/ide/ideMain.c|unsignedLessThanZero|Checking if unsigned expression 'rows->cells?((struct stbds_array_header*)(rows->cells)-1)->capacity:0' is less than zero.
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/apps/kpunch/dvxbasic/ide/ideMain.c|unsignedLessThanZero|Checking if unsigned expression 'rows->strs?((struct stbds_array_header*)(rows->strs)-1)->capacity:0' is less than zero.
-# FP: style suggestion only (variable scope), no defect.
-1 src/apps/kpunch/dvxbasic/ide/ideMain.c|variableScope|The scope of the variable 'len' can be reduced.
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c|unsignedLessThanZero|Checking if unsigned expression '(sLabelBufs)?((struct stbds_array_header*)(sLabelBufs)-1)->capacity:0' is less than zero.
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
@@ -176,19 +188,21 @@
1 src/libs/kpunch/dvxshell/shellAssoc.c|variableScope|The scope of the variable 'tokenLen' can be reduced.
# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
1 src/libs/kpunch/dvxshell/shellBas.c|constParameterCallback|Parameter 'userCtx' can be declared as pointer to const. However it seems that 'hotkeyPressed' is a callback function, if 'userCtx' is declared with const you might also need to cast function pointer(s).
+# FP: bootNotesShow returns when the notes are empty, so the loop has written msg[0 .. len - 1] before the snprintf at msg + len - 1.
+1 src/libs/kpunch/dvxshell/shellMain.c|uninitvar|Uninitialized variable: msg
# FP: style suggestion only (variable scope), no defect.
1 src/libs/kpunch/dvxshell/shellRun.c|variableScope|The scope of the variable 'resolved' can be reduced.
# FP: the strdup'd name moves into scan->entries by arrput; stb_ds OOM hook never returns, so it cannot be dropped.
1 src/libs/kpunch/fileops/fileOps.c|memleak|Memory leak: entry.name
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
-1 src/libs/kpunch/fileops/fileOpsBas.c|unsignedLessThanZero|Checking if unsigned expression '(sText)?((struct stbds_array_header*)(sText)-1)->capacity:0' is less than zero.
-# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/libs/kpunch/hlpfile/hlpFileBas.c|unsignedLessThanZero|Checking if unsigned expression 'hd->found?((struct stbds_array_header*)(hd->found)-1)->capacity:0' is less than zero.
# FP: style suggestion only (could be const), no defect.
1 src/libs/kpunch/libdvx/dvxApp.c|constVariable|Variable 'keyRec' can be declared as const array
# FP: style suggestion only (could be const), no defect.
1 src/libs/kpunch/libdvx/dvxApp.c|constVariable|Variable 'mouseRec' can be declared as const array
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
+1 src/libs/kpunch/libdvx/dvxBas.c|unsignedLessThanZero|Checking if unsigned expression 't->buf?((struct stbds_array_header*)(t->buf)-1)->capacity:0' is less than zero.
+# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/libs/kpunch/libdvx/dvxFlow.c|unsignedLessThanZero|Checking if unsigned expression 'flow->items?((struct stbds_array_header*)(flow->items)-1)->capacity:0' is less than zero.
# FP: inside the stb_ds arrsetcap/arrsetlen macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/libs/kpunch/libdvx/dvxFlow.c|unsignedLessThanZero|Checking if unsigned expression 'flow->lines?((struct stbds_array_header*)(flow->lines)-1)->capacity:0' is less than zero.
@@ -216,6 +230,22 @@
1 src/libs/kpunch/listhelp/listHelp.c|unsignedLessThanZero|Checking if unsigned expression '(owned)?((struct stbds_array_header*)(owned)-1)->capacity:0' is less than zero.
# FP: inside the stb_ds arrdel macro expansion (unsigned capacity compared by the library macro), not DVX logic.
1 src/libs/kpunch/music/musicMmlPlay.c|unsignedLessThanZero|Checking if unsigned expression 'mml->parts?((struct stbds_array_header*)(mml->parts)-1)->capacity:0' is less than zero.
+# FP: parseOpts gets main's argv (char *const there); C does not convert char ** to const char *const * without a cast.
+1 src/libs/kpunch/pkg/pkgCli.c|constParameter|Parameter 'argv' can be declared as const array
+# FP: strtol's long is 64 bits on 64-bit hosts (the host tools and tests), where n > INT32_MAX is a real check; cppcheck runs as unix32.
+1 src/libs/kpunch/pkg/pkgManifest.c|compareValueOutOfTypeRangeError|Comparing expression of type 'signed long' against value 2147483647. Condition is always false.
+# FP: strtoul's unsigned long is 64 bits on 64-bit hosts (the host tools and tests), where n > UINT32_MAX is a real check; cppcheck runs as unix32.
+1 src/libs/kpunch/pkg/pkgManifest.c|compareValueOutOfTypeRangeError|Comparing expression of type 'unsigned long' against value 4294967295. Condition is always false.
+# FP: the strdup'd name moves into m->files by arrput (freed by pkgManifestFree); the stb_ds OOM hook never returns, so it cannot be dropped.
+1 src/libs/kpunch/pkg/pkgManifest.c|memleak|Memory leak: f.name
+# FP: the strdup'd name moves into op->drop by arrput (freed with the operation); the stb_ds OOM hook never returns, so it cannot be dropped.
+1 src/libs/kpunch/pkg/pkgOp.c|memleak|Memory leak: copy.name
+# FP: &detached is passed only when pkgSigRead filled it (hasDetached); taking its address otherwise reads nothing.
+1 src/libs/kpunch/pkg/pkgPlan.c|uninitvar|Uninitialized variable: &detached
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/libs/kpunch/print/printFont.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'bandSpan' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
+# FP: the function is used as a callback whose signature is fixed by the callback type; the parameter cannot be const.
+1 src/libs/kpunch/print/printRaster.c|constParameterCallback|Parameter 'ctx' can be declared as pointer to const. However it seems that 'pictureBand' is a callback function, if 'ctx' is declared with const you might also need to cast function pointer(s).
# FP: style suggestion only (could be const), no defect.
1 src/libs/kpunch/print/printText.c|constVariable|Variable 'order' can be declared as const array
# FP: style suggestion only (could be const), no defect.
diff --git a/src/test/lint/staticGate.sh b/src/test/lint/staticGate.sh
index 26db6784..b1334f28 100755
--- a/src/test/lint/staticGate.sh
+++ b/src/test/lint/staticGate.sh
@@ -491,7 +491,7 @@ fi
# its jobserver, and the query then prints directory lines into the value.
REAL_CC="${DJGPP_CC:-$(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printCc: ; @echo $(CC)' printCc)}"
# The copied tree has no toolchains/, so its make gets the real tree's.
-read -ra TOOLCHAIN_ARGS < <(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printTc: ; @echo DJGPP_PREFIX=$(DJGPP_PREFIX) CWSDSTUB=$(CWSDSTUB) FREETYPE_DIR=$(FREETYPE_DIR) FONT_SOURCES=$(FONT_SOURCES)' printTc)
+read -ra TOOLCHAIN_ARGS < <(env -u MAKEFLAGS -u MFLAGS -u MAKELEVEL make -s --no-print-directory -f "$ROOT/src/common.mk" --eval 'printTc: ; @echo DJGPP_PREFIX=$(DJGPP_PREFIX) CWSDSTUB=$(CWSDSTUB) FREETYPE_DIR=$(FREETYPE_DIR) FONT_SOURCES=$(FONT_SOURCES) SOUNDFONT_SOURCES=$(SOUNDFONT_SOURCES)' printTc)
# ------------------------------------------------------------
# cppcheck
diff --git a/src/test/music/Makefile b/src/test/music/Makefile
index c17aa6bc..837a3e7c 100644
--- a/src/test/music/Makefile
+++ b/src/test/music/Makefile
@@ -30,6 +30,13 @@
# installMusicRefs.sh, compiled into this test only; lsan.supp covers its
# one known leak). Module renders are
# checked against golden/render.txt (UPDATE_GOLDENS=1 rewrites it).
+#
+# playerTest runs the Media Player program (compiled from its project) in
+# the shell fixture with the same libraries, the shell, and fileops
+# linked in, on the fake sound driver. recorderTest runs the Sound
+# Recorder program the same way; the fake driver records (the test hands
+# it the blocks). The tests write their music and sound files with
+# musicFiles.c.
include ../dvxtest/dvxtest.mk
@@ -42,25 +49,37 @@ XMP_DIR = $(MUSIC_DIR)/thirdparty/libxmp-lite
EMU_DIR = $(DVXTEST_ROOT)/toolchains/emu8950
BINDIR = $(DVXTEST_BINDIR)/music
MUSIC_TEST = $(BINDIR)/musicTest
+PLAYER_TEST = $(BINDIR)/playerTest
+RECORDER_TEST = $(BINDIR)/recorderTest
+FILES_OBJ = $(BINDIR)/musicFiles.o
+SHELLDIR = $(DVXTEST_ROOT)/src/libs/kpunch/dvxshell
+SHELLTEST = $(DVXTEST_ROOT)/src/test/shell
+FILEOPS = $(DVXTEST_ROOT)/src/libs/kpunch/fileops
+APPS_SO = $(DVXTEST_OBJDIR)/apps
+SHELL_OBJS = $(patsubst $(SHELLDIR)/%.c,$(BINDIR)/shell/%.o,$(wildcard $(SHELLDIR)/*.c)) $(BINDIR)/shell/shellFixture.o
+FOPS_OBJS = $(patsubst $(FILEOPS)/%.c,$(BINDIR)/fileops/%.o,$(wildcard $(FILEOPS)/*.c))
WORKDIR = $(BINDIR)/work
DRVDIR = $(WORKDIR)/libs/kpunch/audio/drivers
BANKFILE = $(WORKDIR)/libs/kpunch/music/genmidi.op2
GOLDEN = $(abspath golden/render.txt)
+DVXGM = $(DVXTEST_ROOT)/obj/addons/dvxgm/SHARED/BANKS/KPUNCH/DVXGM.SF2
LIB_SRCS = $(filter-out $(MUSIC_DIR)/musicShell.c,$(wildcard $(MUSIC_DIR)/*.c)) $(filter-out $(AUDIO_DIR)/audioShell.c,$(wildcard $(AUDIO_DIR)/*.c))
LIB_OBJS = $(patsubst $(DVXTEST_ROOT)/src/libs/kpunch/%.c,$(BINDIR)/lib/%.o,$(LIB_SRCS))
XMP_SRCS = $(wildcard $(XMP_DIR)/src/*.c) $(wildcard $(XMP_DIR)/src/loaders/*.c)
+XMP_HDRS = $(wildcard $(XMP_DIR)/include/*.h $(XMP_DIR)/src/*.h $(XMP_DIR)/src/loaders/*.h)
XMP_OBJS = $(patsubst $(XMP_DIR)/%.c,$(BINDIR)/xmp/%.o,$(XMP_SRCS))
XMP_OPTS = -DLIBXMP_CORE_PLAYER -DLIBXMP_NO_PROWIZARD -DLIBXMP_NO_DEPACKERS -DBUILDING_STATIC -I$(XMP_DIR)/include -I$(XMP_DIR)/src
EMU_OBJS = $(BINDIR)/emu8950.o $(BINDIR)/emuadpcm.o
-CFLAGS = $(DVXTEST_CFLAGS) -I$(MUSIC_DIR) -I$(AUDIO_DIR) -I$(XMP_DIR)/include -I$(EMU_DIR) -I$(DVXTEST_ROOT)/src/libs/kpunch -DMUSIC_GOLDEN='"$(GOLDEN)"' -DMUSIC_WORKDIR='"$(WORKDIR)"' -DXMP_TEST_DIR='"$(XMP_DIR)/test"'
+CFLAGS = $(DVXTEST_CFLAGS) -I$(MUSIC_DIR) -I$(AUDIO_DIR) -I$(XMP_DIR)/include -I$(EMU_DIR) -I$(DVXTEST_ROOT)/src/libs/kpunch -DMUSIC_GOLDEN='"$(GOLDEN)"' -DMUSIC_WORKDIR='"$(WORKDIR)"' -DXMP_TEST_DIR='"$(XMP_DIR)/test"' -DMUSIC_ADDON_BANKS='"$(DVXTEST_ROOT)/src/addons/banks"' -DMUSIC_DVXGM='"$(DVXGM)"'
+APP_CFLAGS = $(CFLAGS) -fno-omit-frame-pointer -I$(SHELLDIR) -I$(SHELLTEST) -I$(DVXTEST_BASICDIR) -DDVXTEST_APPS_DIR='"$(APPS_SO)"' -DDVXTEST_FIXTURES_DIR='"$(BINDIR)"' -DSHELLTEST_BASSTUB_SO='"$(DVXTEST_BASSTUB_SO)"' -DSHELLTEST_BASCOMP='"$(DVXTEST_BASCOMP)"' -DPLAYER_DRV_DIR='"$(DRVDIR)"' -DPLAYER_BANK='"$(BANKFILE)"'
XMP_CFLAGS = $(filter-out $(WARNFLAGS) $(DEPFLAGS),$(DVXTEST_CFLAGS)) -w $(XMP_OPTS)
-FAKE_DEFS = -DFAKE_NAME='"pcm"' -DFAKE_PRIORITY=30 -DFAKE_CLOCK=0 -DFAKE_TONE=0 -DFAKE_DAC=1 -DFAKE_RATE=22050 -DFAKE_BITS=16 -DFAKE_CHANNELS=2 -DFAKE_OPEN_OK=1 -DFAKE_PLAY_OK=1
-FAKE_DRVS = $(DRVDIR)/pcm.drv $(DRVDIR)/fmfake.drv $(DRVDIR)/midifake.drv
+FAKE_DEFS = -DFAKE_NAME='"pcm"' -DFAKE_PRIORITY=30 -DFAKE_REC_SOURCES=7 -DFAKE_CLOCK=0 -DFAKE_TONE=0 -DFAKE_DAC=1 -DFAKE_RATE=22050 -DFAKE_BITS=16 -DFAKE_CHANNELS=2 -DFAKE_OPEN_OK=1 -DFAKE_PLAY_OK=1
+FAKE_DRVS = $(DRVDIR)/pcm.drv $(DRVDIR)/fmfake.drv $(DRVDIR)/midifake.drv $(DRVDIR)/wtfake.drv
-.PHONY: all run clean
+.PHONY: all run clean dvxgm host-tools
all: run
@@ -72,7 +91,23 @@ $(BINDIR)/lib/%.o: $(DVXTEST_ROOT)/src/libs/kpunch/%.c $(DVX_MAKEFILES)
@mkdir -p $(dir $@)
$(HOSTCC) $(CFLAGS) -c -o $@ $<
-$(BINDIR)/xmp/%.o: $(XMP_DIR)/%.c $(DVX_MAKEFILES)
+$(FILES_OBJ): musicFiles.c musicFiles.h $(DVX_MAKEFILES)
+ @mkdir -p $(BINDIR)
+ $(HOSTCC) $(CFLAGS) -c -o $@ $<
+
+$(BINDIR)/shell/shellFixture.o: $(SHELLTEST)/shellFixture.c $(DVX_MAKEFILES)
+ @mkdir -p $(dir $@)
+ $(HOSTCC) $(APP_CFLAGS) -c -o $@ $<
+
+$(BINDIR)/shell/%.o: $(SHELLDIR)/%.c $(DVX_MAKEFILES)
+ @mkdir -p $(dir $@)
+ $(HOSTCC) $(APP_CFLAGS) -c -o $@ $<
+
+$(BINDIR)/fileops/%.o: $(FILEOPS)/%.c $(DVX_MAKEFILES)
+ @mkdir -p $(dir $@)
+ $(HOSTCC) $(APP_CFLAGS) -c -o $@ $<
+
+$(BINDIR)/xmp/%.o: $(XMP_DIR)/%.c $(XMP_HDRS) $(DVX_MAKEFILES)
@mkdir -p $(dir $@)
$(HOSTCC) $(XMP_CFLAGS) -c -o $@ $<
@@ -92,18 +127,38 @@ $(DRVDIR)/midifake.drv: $(FAKE_DIR)/fakeSynth.c $(AUDIO_DIR)/audioDrv.h $(DVX_MA
@mkdir -p $(DRVDIR)
$(HOSTCC) $(CFLAGS) -shared -o $@ $<
+$(DRVDIR)/wtfake.drv: $(FAKE_DIR)/fakeSynth.c $(AUDIO_DIR)/audioDrv.h $(DVX_MAKEFILES)
+ @mkdir -p $(DRVDIR)
+ $(HOSTCC) $(CFLAGS) -shared -DFAKE_SYN_WT -o $@ $<
+
$(BANKFILE): $(MUSIC_DIR)/thirdparty/genmidi/GENMIDI.OP2
@mkdir -p $(dir $@)
cp $< $@
-$(MUSIC_TEST): musicTest.c $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) | dvxtest-libs
+$(MUSIC_TEST): musicTest.c $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) | dvxtest-libs
@mkdir -p $(BINDIR)
- $(HOSTCC) $(CFLAGS) -o $@ $< $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVXTEST_LINK)
+ $(HOSTCC) $(CFLAGS) -o $@ $< $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(EMU_OBJS) $(DVXTEST_LINK)
--include $(LIB_OBJS:.o=.d) $(MUSIC_TEST).d $(FAKE_DRVS:.drv=.d)
+$(PLAYER_TEST) $(RECORDER_TEST): $(BINDIR)/%: %.c $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(SHELL_OBJS) $(FOPS_OBJS) $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) $(DVXTEST_TASKS_A) $(DVXTEST_BASRT_A) $(DVXTEST_BASSTUB_SO) | dvxtest-libs dvxtest-apps dvxtest-bascomp
+ @mkdir -p $(BINDIR)
+ $(HOSTCC) $(APP_CFLAGS) -o $@ $< $(FILES_OBJ) $(LIB_OBJS) $(XMP_OBJS) $(SHELL_OBJS) $(FOPS_OBJS) $(DVXTEST_LDFLAGS) -Wl,--whole-archive $(DVXTEST_BASRT_A) $(DVXTEST_LIBDVX_A) $(DVXTEST_TASKS_A) $(DVXTEST_LIB) -Wl,--no-whole-archive -ldl -lm
-run: $(MUSIC_TEST) $(FAKE_DRVS) $(BANKFILE)
+host-tools:
+ $(MAKE) -C $(DVXTEST_ROOT)/src/tools host COV=0
+
+dvxtest-apps: | host-tools
+
+-include $(LIB_OBJS:.o=.d) $(FILES_OBJ:.o=.d) $(SHELL_OBJS:.o=.d) $(FOPS_OBJS:.o=.d) $(MUSIC_TEST).d $(PLAYER_TEST).d $(RECORDER_TEST).d $(FAKE_DRVS:.drv=.d)
+
+# The General MIDI SoundFont add-on, as the build makes it (src/tools
+# makes it again when it is out of date)
+dvxgm:
+ $(MAKE) -C $(DVXTEST_ROOT)/src/tools ../../obj/addons/dvxgm.stamp
+
+run: $(MUSIC_TEST) $(PLAYER_TEST) $(RECORDER_TEST) $(FAKE_DRVS) $(BANKFILE) dvxgm
cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1 LSAN_OPTIONS=suppressions=$(abspath lsan.supp) ./musicTest
+ cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1:detect_odr_violation=0 LSAN_OPTIONS=suppressions=$(abspath lsan.supp) ./playerTest
+ cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1:detect_odr_violation=0 LSAN_OPTIONS=suppressions=$(abspath lsan.supp) ./recorderTest
clean:
rm -rf $(BINDIR)
diff --git a/src/test/music/musicFiles.c b/src/test/music/musicFiles.c
new file mode 100644
index 00000000..f15236ea
--- /dev/null
+++ b/src/test/music/musicFiles.c
@@ -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.
+
+// musicFiles.c -- music and sound files the music tests write (see
+// musicFiles.h)
+
+#include "musicFiles.h"
+
+#include
+#include
+#include
+
+
+// An SMF: header of the type and division, then the tracks as given.
+uint32_t buildSmf(uint8_t *buf, uint16_t type, uint16_t division, const uint8_t *const *tracks, const uint32_t *lens, int32_t count) {
+ memcpy(buf, "MThd", 4);
+ put32be(buf + 4, 6);
+ buf[8] = (uint8_t)(type >> 8);
+ buf[9] = (uint8_t)type;
+ buf[10] = (uint8_t)(count >> 8);
+ buf[11] = (uint8_t)count;
+ buf[12] = (uint8_t)(division >> 8);
+ buf[13] = (uint8_t)division;
+
+ uint32_t at = 14;
+
+ for (int32_t i = 0; i < count; i++) {
+ memcpy(buf + at, "MTrk", 4);
+ put32be(buf + at + 4, lens[i]);
+ memcpy(buf + at + 8, tracks[i], lens[i]);
+ at += 8 + lens[i];
+ }
+
+ return at;
+}
+
+
+// A flute (FLUTE_PROGRAM) playing A4 for half a second.
+uint32_t fluteSmf(uint8_t *smf) {
+ static const uint8_t track[] = { 0x00, 0xC0, FLUTE_PROGRAM, 0x00, 0x90, 69, 127, 0x60, 0x80, 69, 0, 0x00, 0xFF, 0x2F, 0x00 };
+ const uint8_t *const tracks[] = { track };
+ const uint32_t lens[] = { sizeof(track) };
+
+ return buildSmf(smf, 0, 96, tracks, lens, 1);
+}
+
+
+// A ProTracker MOD: one 32-byte square-wave sample (looping), one
+// pattern: C-3 (period 428) on channel 1, row 16 a note cut, E-3 (339)
+// on channel 2 at row 32, speed 6 (default tempo), then the song ends.
+uint32_t makeMod(uint8_t *buf) {
+ const uint32_t sampleWords = 16;
+ uint32_t at;
+
+ memset(buf, 0, 1084 + 1024 + sampleWords * 2);
+ memcpy(buf, "dvx test", 8);
+
+ // Sample 1: length (words), finetune, volume, loop start, loop length.
+ buf[20 + 22] = 0;
+ buf[20 + 23] = (uint8_t)sampleWords;
+ buf[20 + 25] = 64;
+ buf[20 + 29] = (uint8_t)sampleWords;
+ buf[950] = 1; // song length
+ buf[951] = 127;
+ buf[952] = 0; // order 0: pattern 0
+ memcpy(buf + 1080, "M.K.", 4);
+ at = 1084;
+
+ // Row 0 channel 0: sample 1, period 428
+ buf[at] = 0x01;
+ buf[at + 1] = 0xAC;
+ buf[at + 2] = 0x10;
+ // Row 16 channel 0: note cut (EC0)
+ buf[at + 16 * 16 + 2] = 0x0E;
+ buf[at + 16 * 16 + 3] = 0xC0;
+ // Row 32 channel 1: sample 1, period 339
+ buf[at + 32 * 16 + 4] = 0x01;
+ buf[at + 32 * 16 + 4 + 1] = 0x53;
+ buf[at + 32 * 16 + 4 + 2] = 0x10;
+ at += 1024;
+
+ for (uint32_t i = 0; i < sampleWords * 2; i++) {
+ buf[at + i] = (uint8_t)(i < sampleWords ? 100 : (uint8_t)-100);
+ }
+
+ return at + sampleWords * 2;
+}
+
+
+void put16le(uint8_t *p, uint32_t v) {
+ p[0] = (uint8_t)v;
+ p[1] = (uint8_t)(v >> 8);
+}
+
+
+void put32be(uint8_t *p, uint32_t v) {
+ p[0] = (uint8_t)(v >> 24);
+ p[1] = (uint8_t)(v >> 16);
+ p[2] = (uint8_t)(v >> 8);
+ p[3] = (uint8_t)v;
+}
+
+
+void put32le(uint8_t *p, uint32_t v) {
+ put16le(p, v);
+ put16le(p + 2, v >> 16);
+}
+
+
+// Frame i of the sound files' sine (frames long), at amp.
+int32_t soundSine(uint32_t i, uint32_t frames, int32_t amp) {
+ uint32_t period = i < frames / 2 ? SOUND_PERIOD : SOUND_PERIOD * 2;
+
+ return (int32_t)lround(amp * sin(2.0 * M_PI * (double)(i % period) / period));
+}
+
+
+// A VOC file of soundSine: one sound block, 8-bit mono at SOUND_VOC_RATE.
+uint32_t vocBuild(uint8_t *out, uint32_t frames) {
+ uint32_t body = VOC_SOUND_BYTES + frames;
+ uint8_t *p = out + VOC_HEADER_BYTES + VOC_BLOCK_BYTES + VOC_SOUND_BYTES;
+
+ memcpy(out, "Creative Voice File\x1A", 20);
+ put16le(out + 20, VOC_HEADER_BYTES);
+ put16le(out + 22, VOC_VERSION);
+ put16le(out + 24, (~VOC_VERSION + VOC_CHECK_MAGIC) & 0xFFFF);
+ out[VOC_HEADER_BYTES] = VOC_BLOCK_SOUND;
+ put16le(out + VOC_HEADER_BYTES + 1, body);
+ out[VOC_HEADER_BYTES + 3] = (uint8_t)(body >> 16);
+ out[VOC_HEADER_BYTES + 4] = (uint8_t)(VOC_TC_BASE - VOC_TC_CLOCK / SOUND_VOC_RATE);
+ out[VOC_HEADER_BYTES + 5] = VOC_CODEC_PCM8;
+
+ for (uint32_t i = 0; i < frames; i++) {
+ *p++ = (uint8_t)(SOUND_SILENCE_8 + soundSine(i, frames, SOUND_AMP_8));
+ }
+
+ *p++ = 0;
+ return (uint32_t)(p - out);
+}
+
+
+// A WAV file of soundSine at SOUND_RATE: bits 8 or 16, the sine on the
+// left (stereo: the right silent), format tag tag.
+uint32_t wavBuild(uint8_t *out, uint32_t bits, uint32_t channels, uint32_t frames, uint32_t tag) {
+ uint32_t frameBytes = channels * bits / 8;
+ uint32_t dataBytes = frames * frameBytes;
+ uint8_t *p = out + WAV_HEADER_BYTES;
+
+ memcpy(out, "RIFF", 4);
+ put32le(out + 4, WAV_HEADER_BYTES - 8 + dataBytes);
+ memcpy(out + 8, "WAVEfmt ", 8);
+ put32le(out + 16, 16);
+ put16le(out + 20, tag);
+ put16le(out + 22, channels);
+ put32le(out + 24, SOUND_RATE);
+ put32le(out + 28, SOUND_RATE * frameBytes);
+ put16le(out + 32, frameBytes);
+ put16le(out + 34, bits);
+ memcpy(out + 36, "data", 4);
+ put32le(out + 40, dataBytes);
+
+ for (uint32_t i = 0; i < frames; i++) {
+ for (uint32_t c = 0; c < channels; c++) {
+ if (bits == 8) {
+ *p++ = (uint8_t)(c == 0 ? SOUND_SILENCE_8 + soundSine(i, frames, SOUND_AMP_8) : SOUND_SILENCE_8);
+ } else {
+ put16le(p, (uint32_t)(c == 0 ? soundSine(i, frames, SOUND_AMP_16) : 0));
+ p += 2;
+ }
+ }
+ }
+
+ return WAV_HEADER_BYTES + dataBytes;
+}
+
+
+void writeFile(const char *path, const void *data, uint32_t size) {
+ FILE *f = fopen(path, "wb");
+
+ if (f) {
+ fwrite(data, 1, size, f);
+ fclose(f);
+ }
+}
diff --git a/src/test/music/musicFiles.h b/src/test/music/musicFiles.h
new file mode 100644
index 00000000..7160dc2c
--- /dev/null
+++ b/src/test/music/musicFiles.h
@@ -0,0 +1,76 @@
+// 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.
+
+// musicFiles.h -- music and sound files the music tests write
+// (musicTest.c, playerTest.c): Standard MIDI Files, a ProTracker module,
+// and WAV and VOC files of a test sine, built in memory.
+#ifndef MUSIC_FILES_H
+#define MUSIC_FILES_H
+
+#include
+
+// fluteSmf's instrument
+#define FLUTE_PROGRAM 73 // General MIDI's flute (0-based)
+
+// The sound files: a second of a sine, SOUND_PERIOD frames a turn for the
+// first half and twice that for the second
+#define SOUND_RATE 11025
+#define SOUND_FRAMES 11025 // a second
+#define SOUND_MS 1000
+#define SOUND_PERIOD 25
+#define SOUND_HZ_FIRST 441.0 // 11,025 / 25
+#define SOUND_HZ_SECOND 220.5
+#define SOUND_VOC_RATE 10000 // a VOC's rate is a time constant: 256 - 1,000,000 / rate
+#define SOUND_VOC_FRAMES 10000 // a second
+#define SOUND_VOC_HZ_FIRST 400.0
+#define SOUND_VOC_HZ_SECOND 200.0
+#define SOUND_AMP_16 12000
+#define SOUND_AMP_8 60
+#define SOUND_SILENCE_8 128
+#define SOUND_BYTES_MAX (SOUND_FRAMES * 4 + 64) // 16-bit stereo and a header
+
+// WAV and VOC layout
+#define WAV_HEADER_BYTES 44
+#define WAV_TAG_PCM 1
+#define WAV_TAG_ADPCM 2
+#define VOC_HEADER_BYTES 26
+#define VOC_VERSION 0x010A
+#define VOC_CHECK_MAGIC 0x1234
+#define VOC_BLOCK_SOUND 1
+#define VOC_BLOCK_BYTES 4 // its type and 24-bit size
+#define VOC_SOUND_BYTES 2 // the time constant and the codec
+#define VOC_TC_BASE 256
+#define VOC_TC_CLOCK 1000000
+#define VOC_CODEC_PCM8 0
+
+uint32_t buildSmf(uint8_t *buf, uint16_t type, uint16_t division, const uint8_t *const *tracks, const uint32_t *lens, int32_t count);
+uint32_t fluteSmf(uint8_t *smf);
+uint32_t makeMod(uint8_t *buf);
+void put16le(uint8_t *p, uint32_t v);
+void put32be(uint8_t *p, uint32_t v);
+void put32le(uint8_t *p, uint32_t v);
+int32_t soundSine(uint32_t i, uint32_t frames, int32_t amp);
+uint32_t vocBuild(uint8_t *out, uint32_t frames);
+uint32_t wavBuild(uint8_t *out, uint32_t bits, uint32_t channels, uint32_t frames, uint32_t tag);
+void writeFile(const char *path, const void *data, uint32_t size);
+
+#endif
diff --git a/src/test/music/musicTest.c b/src/test/music/musicTest.c
index c386c43e..900893c1 100644
--- a/src/test/music/musicTest.c
+++ b/src/test/music/musicTest.c
@@ -48,6 +48,29 @@
// software synthesizer), the MPU-401 port, live MIDI, pause,
// stop, position, volume, end calls as the owning program,
// two programs at once, clean-up when a program ends
+// wave General MIDI on wavetable voices (a recording backend):
+// the region a note plays, its pitch, loudness, pan and
+// loop, bend and its range, volume, pan, the sustain pedal,
+// drums and exclusive classes, taking a voice, notes the bank
+// cannot play passed to the overflow, envelopes run here,
+// GM System On
+// waveload SoundFont samples in the fake wavetable driver's memory:
+// what a song needs, full quality, 8-bit, half and a quarter
+// of the rate, samples left out, what stays for the next
+// song and what makes room
+// waveplay whole musics on the wavetable synthesizer: played by the
+// timer, samples kept for the next music a while, notes the
+// SoundFont lacks on the FM chip, one music at a time,
+// midi = fm, a missing SoundFont, musicSetBank, live MIDI
+// softwave the SoundFont software synthesizer: a live note at its
+// pitch (a loop's period), live MIDI loading a program's
+// samples as it is chosen, a note the bank lacks on the FM
+// software synthesizer, MusicInfo's "where"
+// dvxgm the General MIDI SoundFont add-on as mksf makes it: every
+// instrument and the standard kit, mono, one velocity layer,
+// a few key regions an instrument, loops with their guard
+// frames, and a song of ten instruments and drums fitting a
+// 512 KB card at full quality
// basic the music.bas functions
// bank the MIDI bank DVX.INI names (a vendor's, by name), else the
// bundled one (none named, missing, or damaged)
@@ -64,11 +87,13 @@
#include "dvxtest.h"
#include "dvxPlat.h"
#include "dvxPlatHost.h"
+#include "musicFiles.h"
#include "musicInt.h"
#include "audioInt.h"
#include "emu8950.h"
#include "stb_ds.h"
+#include
#include
#include
#include
@@ -78,10 +103,23 @@
#define BANK_ASIDE "libs/kpunch/music/genmidi.off" // the bundled bank, moved aside
#define BANK_ALT_DIR DVX_USER_DIR "/" DVX_BANKS_DIR "/ACME"
#define BANK_ALT_FILE BANK_ALT_DIR "/ALT.OP2"
-#define BANK_ALT_NOTE 77 // instrument 0's fixed note in ALT.OP2
+#define BANK_ALT_NAME "ACME\\ALT.OP2" // as DVX.INI and musicSetBank name it
+// ALT.OP2 is the standard bank with the flute an octave up, so the pitch
+// of a flute A4 says which bank played it: 440 Hz, or 880 Hz on ALT.OP2.
#define BANK_SIGNATURE 8 // "#OPL_II#", then the instruments
-#define BANK_FIXED_NOTE 3 // an instrument's fixed note byte
-#define BANK_FIXED_NOTE_AT (BANK_SIGNATURE + BANK_FIXED_NOTE)
+#define BANK_INSTR_BYTES 36
+#define BANK_VOICE_AT 4 // in an instrument: its two voices
+#define BANK_VOICE_BYTES 16
+#define BANK_NOTE_AT 14 // in a voice: the note offset (16 bits, little-endian)
+#define BANK_OCTAVE 12
+#define FLUTE_HZ 440.0 // the A4 it plays
+#define FLUTE3_DIVISION 500 // the three-note song: 1 tick a millisecond
+#define FLUTE3_APART_MS 700 // its notes start this far apart
+#define FLUTE3_SETTLE_MS 100 // looked at from this far into each
+#define FLUTE3_WINDOW (RATE_DEV / 5) // for 200 ms
+#define ADDON_BANK_SUBDIR "/SHARED/BANKS/KPUNCH" // where an add-on bank package's bank is
+#define ADDON_BANK_COPY DVX_USER_DIR "/" DVX_BANKS_DIR "/ADDON.OP2"
+#define FLUTE_MACRO "T120 I74 O3 A2" // the same note as a macro (I is 1-based; O3 holds A 440)
#define BUF_MAX 8192
#define REC_MAX 256
#define SYSEX_REC_MAX 64
@@ -95,6 +133,22 @@
#define LCG_ADD 12345U
#define APP_A 5
#define APP_B 6
+
+// MIDI recordings (midiRecordTests)
+#define MIDIREC_FILE "MIDIREC.MID"
+#define MIDIREC_NO_DIR "NODIR/MIDIREC.MID"
+#define MIDIREC_CHANNEL 2
+#define MIDIREC_FLUTE 73
+#define MIDIREC_NOTE 60
+#define MIDIREC_NOTE2 64
+#define MIDIREC_VELOCITY 100
+#define MIDIREC_FIRST_MS 250
+#define MIDIREC_EARLY_MS 200 // before the last: counts as MIDIREC_FIRST_MS
+#define MIDIREC_OFF_MS 1000
+#define MIDIREC_LAST_MS 20000
+#define MIDIREC_CLOCK 0xF8 // a real-time byte, not a channel message
+#define MIDIREC_BAS_SLOTS 8 // musicBas.c's BAS_RECORD_SLOTS
+#define US_PER_MS 1000
#define RATE_DEV 22050
#define FULL MUSIC_VOLUME_MAX
#define US_SECOND 1000000U
@@ -127,6 +181,135 @@
#define OPL2_CLOCK 3579545
#define EMU_TYPE_YM3812 2
+// The wavetable synthesizer
+#define WT_FAKE_MEMORY (1024 * 1024) // the fake driver's memory as the tests keep it
+#define WT_VOICES_MAX 32
+#define WT_RAMP_ADDR 1000 // where the renderer tests say the samples are
+#define WT_HIT_ADDR 5000
+#define WT_RAMP_CENTS (-1200) // Ramp: 22,050 Hz
+#define WT_HIT_CENTS (-2400) // Hit: 11,025 Hz
+#define WT_RAMP_FRAMES 108 // Ramp's 100 and 8 guard frames (Hit's)
+#define WT_HIT_FRAMES 100 // Hit's (nothing after it)
+#define WT_RAMP_BYTES (WT_RAMP_FRAMES * 2) // Ramp in memory, 16-bit
+#define WT_PIANO_PITCH (-3190) // Piano key 40: Ramp 20 keys under its root, fine tune 10
+#define WT_PIANO_ATTEN 618 // the region's 1440 x 0.4, CC7 100's 42
+#define WT_CC7_ATTEN 42 // 400 log10(127 / 100)
+#define WT_DRUM_PITCH (-4405) // Kit key 40: Hit 20 keys under 60, -5 cents
+#define WT_DRUM_ATTEN 84 // velocity 100's 42 and CC7's 42
+#define WT_BEND_UP 199 // +8191 at 2 semitones
+#define WT_FLAGS_HW (AUDIO_WT_ENVELOPES | AUDIO_WT_LFOS)
+#define GM_PROGRAMS 128
+#define GM_REGIONS_MAX 4 // mksf's most key regions an instrument
+#define GM_RATE_MAX 23000 // mksf's 22,050 Hz, moved up to keep a short loop whole
+#define GM_GUARD 8 // frames after a loop: its first ones again
+#define SF_TEST_NAME "TEST.SF2"
+#define SF_TEST_FILE DVX_USER_DIR "/" DVX_BANKS_DIR "/" SF_TEST_NAME
+#define SF_LIVE_NAME "LIVE.SF2" // Piano on Kit: Hit only
+#define SF_LIVE_FILE DVX_USER_DIR "/" DVX_BANKS_DIR "/" SF_LIVE_NAME
+#define SOFT_KEY 59 // Piano: Ramp's loop, 90 cents under its root
+#define SOFT_HZ 261.6 // 22,050 / 80 frames x 2^(-90 / 1200)
+
+// Modules on the wavetable synthesizer (modHwTests)
+#define MODHW_SAMPLE_BYTES 80 // makeMod's 32 frames and 8 guard frames, 16-bit
+#define MODHW_PITCH_428 (-2895) // period 428: 8,287 Hz (libxmp's PAL rate)
+#define MODHW_PITCH_339 (-2495) // period 339: its note, a major third up
+#define MODHW_PITCH_XM_C4 (-2879) // an Extended Module's C-4: 8,363 Hz
+#define MODHW_ATTEN_FULL 80 // volume 64: libxmp's level
+#define MODHW_ATTEN_HALF 60 // the music at half: 6 dB more
+#define MODHW_FRAME_MS 20 // a tick at 125 BPM
+#define MODHW_CUT_MS 2000 // after row 16's note cut (1,920 ms)
+#define MODHW_NOTE2_MS 3900 // after row 32's note (3,840 ms)
+#define MODHW_ORDER_MS 7680 // makeMod's pattern: 64 rows of 6 ticks
+#define MODHW_SEEK_MS 9000 // into the twice-played song's second pattern
+#define MODHW_SEEK_TOL_MS (MODHW_FRAME_MS * 2)
+#define MODHW_SMALL_MEMORY 256 // the module's sample or Ramp, not both
+#define XM_FRAMES 16 // makeXm's sample: frame i is i x 8, 8-bit
+#define XM_LOOP_START 4 // ... a ping-pong loop of 4 .. 11
+#define XM_LOOP_LEN 8
+#define XM_STORED 20 // through the loop's end, then its way back
+#define XM_FRAME_STEP 8
+#define XM_NOTE_C4 49
+#define XM_HEADER_SIZE 276
+#define XM_INSTRUMENT_SIZE 263
+#define XM_SAMPLE_HEADER 40
+#define XM_ROWS 64
+#define XM_PINGPONG 2
+
+// Gravis UltraSound patch sets the tests write (patWrite): the file's
+// layout (a 129-byte header with the wave count at 85, a 63-byte
+// instrument, a 47-byte layer, then each wave's 96-byte header and data)
+#define PAT_DIR "GUS" // in the work folder: ULTRASND.INI, MIDI, OTHER
+#define PAT_LEGACY_DIR "GUS2" // [Melodic Patches] / [Drum Patches]
+#define PAT_EMPTY_DIR "GUS3" // an INI naming no patch there is
+#define PAT_NONE_DIR "GUS4" // no INI
+#define PAT_HEAD_BYTES 129
+#define PAT_SKIP_BYTES (63 + 47)
+#define PAT_WAVE_HEAD 96
+#define PAT_WAVES_AT 85
+#define PAT_W_SIZE 8
+#define PAT_W_LOOP_START 12
+#define PAT_W_LOOP_END 16
+#define PAT_W_RATE 20
+#define PAT_W_LOW 22
+#define PAT_W_HIGH 26
+#define PAT_W_ROOT 30
+#define PAT_W_BALANCE 36
+#define PAT_W_MODES 55
+#define PAT_W_SCALE_KEY 56
+#define PAT_W_SCALE_FACTOR 58
+#define PAT_MODE_16BIT 0x01
+#define PAT_MODE_UNSIGNED 0x02
+#define PAT_MODE_LOOP 0x04
+#define PAT_RATE 22050
+#define PAT_LOW_ALL 8176 // milli-Hz: key 0 ...
+#define PAT_HIGH_ALL 12543853 // ... to key 127
+#define PAT_SINE_ROOT 441000 // a 50-frame period at 22,050 Hz
+#define PAT_SINE_PERIOD 50
+#define PAT_SINE_FRAMES 500
+#define PAT_SINE_LOOP 100
+#define PAT_SINE_CENTS (-904) // fine tune: key 60 less 441 Hz's 69.04
+#define PAT_SPLIT_HIGH 270000 // TWO's first wave: up to key 60
+#define PAT_PIVOT 60
+#define PAT_SEMI 1024 // scale factor: a semitone a key
+#define PAT_AMP_16 16000
+#define PAT_AMP_8 100
+#define PAT_SILENCE_8 128
+#define PAT_KICK_KEY 36
+#define PAT_KEY_TOP 127
+#define PAT_A4_KEY 69
+#define PAT_A4_HZ 440.0
+#define PAT_FILE_MAX 2048
+
+// The SoundFont 1 bank sf1Build writes: a ROM sample, then one of its own
+#define SF1_TEST_NAME "TEST.SBK"
+#define SF1_TEST_FILE DVX_USER_DIR "/" DVX_BANKS_DIR "/" SF1_TEST_NAME
+#define SF1_ROM_START 0x1EE
+#define SF1_ROM_END 0x400
+#define SF1_ROM_LOOP 0x300
+#define SF1_ROM_LOOP_END 0x3F0
+#define SF1_RAM_FRAMES 120
+#define SF1_RAM_END 100
+#define SF1_RAM_LOOP 10
+#define SF1_RAM_LOOP_END 90
+#define SF1_ROOT_PITCH 6950 // samplePitch: 69 and 50 cents
+#define SF1_ATTEN_64 59 // 0..127 level 64: 200 log10(127 / 64) cB
+#define SF1_SOUND_DIR "SB16" // in the work folder: SOUND's
+
+// soundTests: its steps of time, what is heard, and places in the sound
+// files (musicFiles.h)
+#define SOUND_STEP (RATE_DEV / 4) // pump(SOUND_STEP) lets SOUND_STEP_MS pass
+#define SOUND_STEP_MS 250
+#define SOUND_SHORT (RATE_DEV / 10)
+#define SOUND_SHORT_MS 100
+#define SOUND_HELD 4 // device frames: the mixer's current and next source frames
+#define SOUND_TONE_RATIO 4.0 // a tone heard stands this far over the other
+#define SOUND_POS_TOL_MS 10
+#define SOUND_SEEK_MS 800 // into the second half
+#define SOUND_LOOP_MS 900
+#define SOUND_CUT_MS 400 // a WAV whose file ends this far into its frames
+#define SOUND_CUT_BYTES 30 // a WAV that ends inside its fmt chunk
+#define MS_SECOND 1000
+
// A recording sink
typedef struct {
MidiSinkT sink;
@@ -147,6 +330,20 @@ typedef struct {
int32_t releases;
} RecBackT;
+// A recording wavetable backend: voices 1..limit, each as last started or
+// updated
+typedef struct {
+ AudioWtVoiceT voice[WT_VOICES_MAX + 1];
+ uint32_t what[WT_VOICES_MAX + 1];
+ bool used[WT_VOICES_MAX + 1];
+ bool active[WT_VOICES_MAX + 1];
+ bool released[WT_VOICES_MAX + 1];
+ int32_t limit;
+ int32_t last; // the voice started last
+ int32_t starts;
+ int32_t frees;
+} RecWaveT;
+
typedef struct {
char name[GOLDEN_NAME_MAX];
uint64_t hash;
@@ -163,16 +360,46 @@ static int32_t sGoldenCount = 0;
static bool sGoldenDirty = false;
static bool sUpdate = false;
static int32_t sEnds = 0;
+static uint8_t sWtMemory[WT_FAKE_MEMORY];
+static AudioWtVoiceT sWtVoice[WT_VOICES_MAX];
+static uint32_t sWtActive = 0; // the fake driver's voices sounding (bits)
+static uint32_t sWtReleased = 0;
+static uint32_t sWtLast = 0;
+static int32_t sWtStarts = 0;
+static int32_t sWtUploads = 0;
static int32_t sEndApp = -1;
static uint32_t sSeed = 1;
+// A wave patWrite writes: frames of a sine of period frames (16-bit
+// signed, or 8-bit unsigned), looping from loopStart (none: 0)
+typedef struct {
+ uint32_t frames;
+ uint32_t period;
+ uint32_t loopStart;
+ uint32_t low; // milli-Hz
+ uint32_t high;
+ uint32_t root;
+ uint16_t scaleFactor;
+ uint8_t modes;
+} PatWaveSpecT;
+
+// A SoundFont the tests build (sfBuild), and what a test may spoil in it
+typedef struct {
+ uint16_t version; // ifil major (2)
+ uint32_t hitEnd; // the second sample's end (200)
+ uint16_t instrument; // preset 0's instrument (0)
+} SfSpecT;
+
+#define SF_TEST_MAX 4096
+#define SF_TEST_FRAMES 200
+
// ============================================================
// Prototypes
// ============================================================
+static double bankPitch(MusicT *m);
static void bankTests(void);
static void basicTests(void);
-static uint32_t buildSmf(uint8_t *buf, uint16_t type, uint16_t division, const uint8_t *const *tracks, const uint32_t *lens, int32_t count);
static void checkGolden(const char *name, uint64_t hash);
static double cmpGoertzel(const float *x, int32_t n, double hz, double rate);
static double cmpPitch(const float *x, int32_t n, double rate);
@@ -180,26 +407,38 @@ static void cmpRender(const uint8_t *regs, const uint8_t *vals, int32_t coun
static void cmpTests(void);
void fakeDrvEvent(const char *driver, const char *event);
void fakeDrvTone(uint32_t hz);
+void fakeSynthEvent(const char *synth, const char *event);
+bool fakeSynthGet(uint8_t *byte);
bool fakeSynthPut(uint8_t byte);
void fakeSynthWrite(uint32_t reg, uint8_t value);
+bool fakeWtActive(uint32_t voice);
+void fakeWtRelease(uint32_t voice);
+void fakeWtStart(uint32_t voice, const AudioWtVoiceT *v);
+void fakeWtStop(uint32_t voice);
+void fakeWtUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what);
+bool fakeWtUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits);
static void fmTests(void);
+static void gmBankTests(void);
static uint64_t hash16(const int16_t *p, uint32_t count);
static int32_t keyedChannel(void);
static void loadGoldens(void);
-static uint32_t makeMod(uint8_t *buf);
static uint32_t makeS3m(uint8_t *buf);
+static uint32_t makeXm(uint8_t *buf);
+static void midiRecordTests(void);
static void mmlPlayTests(void);
static void mmlTests(void);
+static void modHwTests(void);
+static int16_t modHwWord(uint32_t addr, uint32_t frame);
static void mt32Tests(void);
static void onEnd(MusicT *music, void *userData);
static void op2Tests(void);
static int32_t outPeak(uint32_t frames);
+static int32_t outPeakFrom(uint32_t from, uint32_t frames);
static double outTone(uint32_t from, uint32_t n, double hz);
+static void patTests(void);
+static void patWrite(const char *path, const PatWaveSpecT *waves, int32_t count);
static void playTests(void);
static void pump(uint32_t frames);
-static void put16le(uint8_t *p, uint32_t v);
-static void put32be(uint8_t *p, uint32_t v);
-static void put32le(uint8_t *p, uint32_t v);
static int32_t recAlloc(void *owner);
static void recMessage(MidiSinkT *sink, uint8_t status, uint8_t data1, uint8_t data2);
static void recRelease(void *owner, int32_t voice);
@@ -211,57 +450,205 @@ static uint32_t rng(void);
static void saveGoldens(void);
static void seqTests(void);
static void setIni(const char *text);
+static uint32_t sf1Build(uint8_t *out);
+static void sf1Tests(void);
+static uint32_t sfBuild(const SfSpecT *spec, uint8_t *out);
+static void sfChunk(uint8_t *out, uint32_t *len, const char *id, const uint8_t *body, uint32_t bodyLen);
+static void sfTests(void);
static void smfTests(void);
-static void writeFile(const char *path, const void *data, uint32_t size);
+static void softwaveTests(void);
+static bool soundNear(uint32_t ms, uint32_t want);
+static void soundTests(void);
+static void waveLoadTests(void);
+static void wavePlayTests(void);
+static void waveTests(void);
+static bool wrecActive(void *owner, int32_t voice);
+static int32_t wrecAlloc(void *owner);
+static void wrecFree(void *owner, int32_t voice);
+static void wrecRelease(void *owner, int32_t voice);
+static void wrecStart(void *owner, int32_t voice, const AudioWtVoiceT *v);
+static void wrecUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what);
static void xmpTests(void);
-// The MIDI bank DVX.INI names, found by name in the banks folders (an
-// add-on vendor's folder too); the bundled one when it names none, or
-// one that is missing or damaged.
+// Play m (a flute A4) a moment, free it, and say which octave sounded:
+// FLUTE_HZ (the standard bank), twice it (ALT.OP2), or 0 for neither.
+static double bankPitch(MusicT *m) {
+ double low;
+ double high;
+
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ musicFree(m);
+ low = outTone(RATE_DEV / 20, RATE_DEV / 8, FLUTE_HZ);
+ high = outTone(RATE_DEV / 20, RATE_DEV / 8, FLUTE_HZ * 2);
+
+ if (low > 4.0 * outTone(RATE_DEV / 20, RATE_DEV / 8, FLUTE_HZ / 2) && low > high) {
+ return FLUTE_HZ;
+ }
+
+ return high > 4.0 * low ? FLUTE_HZ * 2 : 0;
+}
+
+
static void bankTests(void) {
- int32_t len = 0;
- char *data = platformReadFile(MUSIC_BANK_DEFAULT, &len);
- const Op2BankT *bank;
- int32_t err;
+ static uint8_t smf[BUF_MAX];
+ int32_t len = 0;
+ char *data = platformReadFile(MUSIC_BANK_DEFAULT, &len);
+ uint32_t size = fluteSmf(smf);
+ MusicT *m = NULL;
+ MusicT *m2 = NULL;
- tCase("bank: the one DVX.INI names, else the bundled one");
- TEST_TRUE(data != NULL && len > BANK_FIXED_NOTE_AT);
+ tCase("bank: DVX.INI's choice, read as each MIDI music starts");
+ TEST_TRUE(data != NULL && len >= BANK_SIGNATURE + (FLUTE_PROGRAM + 1) * BANK_INSTR_BYTES);
- if (!data || len <= BANK_FIXED_NOTE_AT) {
+ if (!data || len < BANK_SIGNATURE + (FLUTE_PROGRAM + 1) * BANK_INSTR_BYTES) {
free(data);
return;
}
+ for (int32_t v = 0; v < 2; v++) {
+ uint8_t *note = (uint8_t *)data + BANK_SIGNATURE + FLUTE_PROGRAM * BANK_INSTR_BYTES + BANK_VOICE_AT + v * BANK_VOICE_BYTES + BANK_NOTE_AT;
+ int32_t offset = (int16_t)(note[0] | (note[1] << 8)) + BANK_OCTAVE;
+
+ note[0] = (uint8_t)offset;
+ note[1] = (uint8_t)(offset >> 8);
+ }
+
TEST_EQ(platformMkdirRecursive(BANK_ALT_DIR), 0);
- data[BANK_FIXED_NOTE_AT] = BANK_ALT_NOTE;
writeFile(BANK_ALT_FILE, data, (uint32_t)len);
free(data);
- setIni("midiBank = ACME\\ALT.OP2\n");
- bank = musicBank(&err);
- TEST_EQ(err, MUSIC_OK);
- TEST_TRUE(bank && bank->instruments[0].fixedNote == BANK_ALT_NOTE);
- musicShutdown();
+ setIni("midi = software\nmidiBank = " BANK_ALT_NAME "\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_TRUE(bankPitch(m) == FLUTE_HZ * 2);
- setIni("midiBank = NOBANK.OP2\n");
- bank = musicBank(&err);
- TEST_EQ(err, MUSIC_OK);
- TEST_TRUE(bank && bank->instruments[0].fixedNote != BANK_ALT_NOTE);
- musicShutdown();
+ // Changed in DVX.INI: the next music has it, with no restart
+ setIni("midi = software\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_TRUE(bankPitch(m) == FLUTE_HZ);
+ tCase("bank: a music playing keeps its bank when the choice changes");
+ setIni("midi = software\nmidiBank = " BANK_ALT_NAME "\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, true), MUSIC_OK);
+ setIni("midi = software\n");
+ TEST_EQ(musicLoadMem(smf, size, &m2), MUSIC_OK);
+ TEST_EQ(musicPlay(m2, true), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ TEST_TRUE(outTone(RATE_DEV / 20, RATE_DEV / 8, FLUTE_HZ) > 0);
+ TEST_TRUE(outTone(RATE_DEV / 20, RATE_DEV / 8, FLUTE_HZ * 2) > 0);
+ musicFree(m2);
+ pump(RATE_DEV / 8);
+ musicFree(m);
+
+ tCase("bank: a missing or damaged choice is the standard one");
+ setIni("midi = software\nmidiBank = NOBANK.OP2\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_TRUE(bankPitch(m) == FLUTE_HZ);
+
+ tCase("bank: musicSetBank for one music, read at once");
+ setIni("midi = software\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, BANK_ALT_NAME), MUSIC_OK);
+ TEST_TRUE(bankPitch(m) == FLUTE_HZ * 2);
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, BANK_ALT_NAME), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, NULL), MUSIC_OK);
+ TEST_TRUE(bankPitch(m) == FLUTE_HZ);
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, "NOBANK.OP2"), MUSIC_ERR_BANK);
+ TEST_EQ(musicSetBank(m, ""), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, BANK_ALT_NAME), MUSIC_ERR_ARG);
+ musicFree(m);
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, BANK_ALT_NAME), MUSIC_ERR_ARG);
+ musicFree(m);
+ TEST_EQ(musicSetBank(NULL, ""), MUSIC_ERR_ARG);
+
+ tCase("bank: a damaged bank");
writeFile(BANK_ALT_FILE, "junk", 4);
- setIni("midiBank = ACME/ALT.OP2\n");
- bank = musicBank(&err);
- TEST_EQ(err, MUSIC_OK);
- TEST_TRUE(bank && bank->instruments[0].fixedNote != BANK_ALT_NOTE);
- musicShutdown();
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, BANK_ALT_NAME), MUSIC_ERR_BANK);
+ musicFree(m);
+ setIni("midi = software\nmidiBank = " BANK_ALT_NAME "\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_TRUE(bankPitch(m) == FLUTE_HZ);
+
+ tCase("bank: musicLoadMacro plays the macro on its instruments");
+ setIni("midi = software\n");
+ TEST_EQ(musicLoadMacro(FLUTE_MACRO, &m), MUSIC_OK);
+ TEST_TRUE(bankPitch(m) == FLUTE_HZ);
+ TEST_EQ(musicLoadMacro("T120 O3 A2", &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ TEST_TRUE(outTone(RATE_DEV / 20, RATE_DEV / 8, FLUTE_HZ) > 0);
+ musicFree(m);
+ TEST_EQ(musicLoadMacro("C D X", &m), MUSIC_ERR_MACRO);
+ TEST_TRUE(m == NULL);
+ TEST_EQ(musicLoadMacro(NULL, &m), MUSIC_ERR_ARG);
remove(BANK_ALT_FILE);
+
+ // Each of DVX's own add-on OP2 banks (src/addons/banks/NAME) is a
+ // bank the music plays a flute A on, in one octave or the other
+ tCase("bank: DVX's add-on banks");
+ DIR *dir = opendir(MUSIC_ADDON_BANKS);
+ int32_t addons = 0;
+
+ TEST_TRUE(dir != NULL);
+
+ for (struct dirent *e = dir ? readdir(dir) : NULL; e; e = readdir(dir)) {
+ char folder[DVX_MAX_PATH];
+ DIR *sub;
+
+ if (e->d_name[0] == '.' || e->d_type != DT_DIR) {
+ continue;
+ }
+
+ snprintf(folder, sizeof(folder), "%s/%s" ADDON_BANK_SUBDIR, MUSIC_ADDON_BANKS, e->d_name);
+ sub = opendir(folder);
+ TEST_TRUE(sub != NULL);
+
+ for (struct dirent *f = sub ? readdir(sub) : NULL; f; f = readdir(sub)) {
+ char path[DVX_MAX_PATH * 2];
+ char *bytes;
+ int32_t bankSize = 0;
+ double hz;
+
+ if (!dvxHasExt(f->d_name, ".OP2")) {
+ continue;
+ }
+
+ snprintf(path, sizeof(path), "%s/%s", folder, f->d_name);
+ bytes = platformReadFile(path, &bankSize);
+ TEST_TRUE(bytes != NULL);
+ writeFile(ADDON_BANK_COPY, bytes, (uint32_t)bankSize);
+ free(bytes);
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, "ADDON.OP2"), MUSIC_OK);
+ hz = bankPitch(m);
+ TEST_TRUE(hz == FLUTE_HZ || hz == FLUTE_HZ * 2);
+ addons++;
+ }
+
+ if (sub) {
+ closedir(sub);
+ }
+ }
+
+ if (dir) {
+ closedir(dir);
+ }
+
+ TEST_TRUE(addons > 0);
+ remove(ADDON_BANK_COPY);
}
static void basicTests(void) {
static uint8_t smf[BUF_MAX];
+ static uint8_t wav[SOUND_BYTES_MAX];
const uint8_t track[] = { 0x00, 0xC0, 73, 0x00, 0x90, 69, 100, 0x81, 0x40, 0x80, 69, 0, 0x00, 0xFF, 0x2F, 0x00 };
const uint8_t *tracks[] = { track };
uint32_t lens[] = { sizeof(track) };
@@ -306,6 +693,19 @@ static void basicTests(void) {
TEST_EQ(MusicPlaying(h), 0);
TEST_EQ(MusicPosition(h), 0);
+ tCase("basic: a sound file; one that cannot be played");
+ writeFile("BASTEST.WAV", wav, wavBuild(wav, 16, 1, SOUND_FRAMES, WAV_TAG_PCM));
+
+ int32_t snd = MusicLoad("BASTEST.WAV");
+
+ TEST_TRUE(snd > 0);
+ TEST_STR_EQ(MusicInfo(snd, "kind"), "sound");
+ TEST_EQ(MusicLength(snd), SOUND_MS);
+ MusicFree(snd);
+ writeFile("BASTEST.WAV", wav, wavBuild(wav, 16, 1, SOUND_FRAMES, WAV_TAG_ADPCM));
+ TEST_EQ(MusicLoad("BASTEST.WAV"), 0);
+ TEST_STR_EQ(MusicError(), audioErrorText(AUDIO_ERR_UNSUPPORTED));
+
tCase("basic: live MIDI with channels 1..16");
int32_t live = MusicMidiOpen();
@@ -324,6 +724,23 @@ static void basicTests(void) {
MusicNoteOn(live, 0, 60, 100);
TEST_STR_EQ(MusicError(), "Invalid argument");
+ tCase("basic: MusicLoadMacro, MusicSetBank");
+
+ int32_t tune = MusicLoadMacro(FLUTE_MACRO);
+
+ TEST_TRUE(tune > 0);
+ TEST_STR_EQ(MusicInfo(tune, "kind"), "midi");
+ MusicSetBank(tune, "NOBANK.OP2");
+ TEST_STR_EQ(MusicError(), "The instrument bank for MIDI music is missing or damaged");
+ MusicSetBank(tune, "");
+ TEST_STR_EQ(MusicError(), "No error");
+ MusicPlay(tune, 0);
+ MusicSetBank(tune, "");
+ TEST_STR_EQ(MusicError(), "Invalid argument");
+ MusicFree(tune);
+ TEST_EQ(MusicLoadMacro("C D X"), 0);
+ TEST_STR_EQ(MusicError(), "The music string has a mistake in it");
+
tCase("basic: numbers belong to their program; errors");
sAppId = APP_B;
MusicStop(h);
@@ -349,30 +766,6 @@ static void basicTests(void) {
}
-// An SMF: header of the type and division, then the tracks as given.
-static uint32_t buildSmf(uint8_t *buf, uint16_t type, uint16_t division, const uint8_t *const *tracks, const uint32_t *lens, int32_t count) {
- memcpy(buf, "MThd", 4);
- put32be(buf + 4, 6);
- buf[8] = (uint8_t)(type >> 8);
- buf[9] = (uint8_t)type;
- buf[10] = (uint8_t)(count >> 8);
- buf[11] = (uint8_t)count;
- buf[12] = (uint8_t)(division >> 8);
- buf[13] = (uint8_t)division;
-
- uint32_t at = 14;
-
- for (int32_t i = 0; i < count; i++) {
- memcpy(buf + at, "MTrk", 4);
- put32be(buf + at + 4, lens[i]);
- memcpy(buf + at + 8, tracks[i], lens[i]);
- at += 8 + lens[i];
- }
-
- return at;
-}
-
-
static void checkGolden(const char *name, uint64_t hash) {
for (int32_t i = 0; i < sGoldenCount; i++) {
if (strcmp(sGolden[i].name, name) == 0) {
@@ -645,6 +1038,19 @@ void fakeDrvTone(uint32_t hz) {
}
+void fakeSynthEvent(const char *synth, const char *event) {
+ (void)synth;
+ (void)event;
+}
+
+
+// The fake MIDI port's input: nothing comes here.
+bool fakeSynthGet(uint8_t *byte) {
+ (void)byte;
+ return false;
+}
+
+
bool fakeSynthPut(uint8_t byte) {
if (sMidiCount >= MIDI_BYTES_MAX) {
return false;
@@ -663,6 +1069,61 @@ void fakeSynthWrite(uint32_t reg, uint8_t value) {
}
+bool fakeWtActive(uint32_t voice) {
+ return (sWtActive >> voice) & 1;
+}
+
+
+void fakeWtRelease(uint32_t voice) {
+ sWtReleased |= 1u << voice;
+}
+
+
+void fakeWtStart(uint32_t voice, const AudioWtVoiceT *v) {
+ sWtVoice[voice] = *v;
+ sWtActive |= 1u << voice;
+ sWtReleased &= ~(1u << voice);
+ sWtLast = voice;
+ sWtStarts++;
+}
+
+
+void fakeWtStop(uint32_t voice) {
+ sWtActive &= ~(1u << voice);
+}
+
+
+void fakeWtUpdate(uint32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ (void)what;
+ sWtVoice[voice] = *v;
+}
+
+
+// Frames into the fake memory as 8-bit (the top byte) or 16-bit
+// little-endian samples.
+bool fakeWtUpload(uint32_t addr, const int16_t *frames, uint32_t count, uint32_t bits) {
+ uint32_t bytes = bits / 8;
+
+ if (addr > WT_FAKE_MEMORY || count > (WT_FAKE_MEMORY - addr) / bytes) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < count; i++) {
+ uint16_t v = (uint16_t)frames[i];
+
+ if (bytes == 1) {
+ sWtMemory[addr + i] = (uint8_t)(v >> 8);
+ } else {
+ sWtMemory[addr + i * 2] = (uint8_t)v;
+ sWtMemory[addr + i * 2 + 1] = (uint8_t)(v >> 8);
+ }
+ }
+
+ sWtUploads++;
+ return true;
+}
+
+
static void fmTests(void) {
static RecBackT back;
static FmSynthT synth;
@@ -821,6 +1282,118 @@ static void fmTests(void) {
}
+// The General MIDI SoundFont add-on as mksf made it from FluidR3_GM.
+static void gmBankTests(void) {
+ static const uint8_t song[] = { 0, 4, 24, 32, 33, 40, 48, 56, 61, 73 };
+ static const uint8_t drums[] = { 35, 36, 38, 40, 42, 44, 46, 49, 51 };
+ WaveBankT bank;
+ WaveCacheT cache;
+ AudioCtxT *ctx;
+ uint8_t *need;
+ uint32_t missing = 0;
+ int32_t mono = 0;
+ int32_t layered = 0;
+ int32_t wide = 0;
+ int32_t guards = 0;
+ int32_t loops = 0;
+
+ tCase("dvxgm: every instrument and the standard kit");
+ TEST_EQ(sf2Load(MUSIC_DVXGM, &bank), MUSIC_OK);
+ TEST_EQ(bank.presetCount, GM_PROGRAMS + 1);
+
+ for (uint32_t p = 0; p < GM_PROGRAMS; p++) {
+ const WavePresetT *preset = waveBankPreset(&bank, 0, (uint8_t)p);
+
+ TEST_TRUE(preset && preset->bank == 0 && preset->program == p);
+ wide += preset && preset->count > GM_REGIONS_MAX;
+ }
+
+ TEST_EQ(wide, 0);
+ TEST_EQ(waveBankPreset(&bank, SF_DRUM_BANK, 0)->bank, SF_DRUM_BANK);
+
+ tCase("dvxgm: mono, one layer, loops that run on");
+ for (uint32_t i = 0; i < bank.sampleCount; i++) {
+ const WaveSampleT *s = &bank.samples[i];
+ int16_t at[GM_GUARD];
+ int16_t after[GM_GUARD];
+
+ mono += s->type == SF_SAMPLE_MONO && s->rate <= GM_RATE_MAX;
+
+ if (s->loopEnd + GM_GUARD <= s->end) {
+ loops++;
+ guards += waveBankRead(&bank, s->loopStart, GM_GUARD, at) && waveBankRead(&bank, s->loopEnd, GM_GUARD, after) && memcmp(at, after, sizeof(at)) == 0;
+ }
+ }
+
+ for (uint32_t i = 0; i < bank.regionCount; i++) {
+ layered += bank.regions[i].velLo != 0 || bank.regions[i].velHi != MUSIC_MIDI_DATA_MAX;
+ }
+
+ TEST_EQ(mono, (int32_t)bank.sampleCount);
+ TEST_EQ(layered, 0);
+ TEST_TRUE(loops > 0);
+ TEST_EQ(guards, loops);
+
+ tCase("dvxgm: a song of ten instruments and nine drums fits 512 KB at full quality");
+ need = (uint8_t *)calloc(bank.sampleCount, 1);
+ TEST_TRUE(need != NULL);
+
+ for (uint32_t i = 0; i < sizeof(song); i++) {
+ waveNeedsPreset(&bank, 0, song[i], need);
+ }
+
+ const WavePresetT *kit = waveBankPreset(&bank, SF_DRUM_BANK, 0);
+
+ for (uint32_t i = 0; i < kit->count; i++) {
+ const WaveRegionT *r = &bank.regions[kit->first + i];
+
+ for (uint32_t d = 0; d < sizeof(drums); d++) {
+ need[r->sample] |= drums[d] >= r->keyLo && drums[d] <= r->keyHi;
+ }
+ }
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 524288\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(missing, 0);
+
+ for (uint32_t i = 0; i < bank.sampleCount; i++) {
+ TEST_TRUE(!need[i] || (cache.loaded[i].bits == 16 && cache.loaded[i].shift == 0));
+ }
+
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+ free(need);
+ waveBankFree(&bank);
+
+ tCase("dvxgm: a flute's A4, E5, and A5 in software, each at its time");
+ static const uint8_t flute3[] = { 0x00, 0xC0, FLUTE_PROGRAM, 0x00, 0x90, 69, 110, 0x83, 0x10, 0x80, 69, 0, 0x82, 0x2C, 0x90, 76, 110, 0x83, 0x10, 0x80, 76, 0, 0x82, 0x2C, 0x90, 81, 110, 0x83, 0x10, 0x80, 81, 0, 0x81, 0x48, 0xFF, 0x2F, 0x00 };
+ static const double hz[] = { 440.0, 659.3, 880.0 };
+ static uint8_t smf[BUF_MAX];
+ const uint8_t *const tracks[] = { flute3 };
+ const uint32_t lens[] = { sizeof(flute3) };
+ char text[DVX_MAX_PATH + LINE_MAX_LEN];
+ MusicT *m = NULL;
+
+ snprintf(text, sizeof(text), "midi = softwave\nsoundFont = %s\n", MUSIC_DVXGM);
+ setIni(text);
+ TEST_EQ(musicLoadMem(smf, buildSmf(smf, 0, FLUTE3_DIVISION, tracks, lens, 1), &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ pump(RATE_DEV * 2);
+
+ for (int32_t n = 0; n < DVX_ARRAY_LEN(hz); n++) {
+ uint32_t from = (uint32_t)(n * FLUTE3_APART_MS + FLUTE3_SETTLE_MS) * RATE_DEV / 1000;
+ double own = outTone(from, FLUTE3_WINDOW, hz[n]);
+
+ for (int32_t o = 0; o < DVX_ARRAY_LEN(hz); o++) {
+ TEST_TRUE(o == n || own > 4.0 * outTone(from, FLUTE3_WINDOW, hz[o]));
+ }
+ }
+
+ musicFree(m);
+}
+
+
static uint64_t hash16(const int16_t *p, uint32_t count) {
uint64_t h = FNV_OFFSET;
@@ -866,48 +1439,6 @@ static void loadGoldens(void) {
}
-// A ProTracker MOD: one 32-byte square-wave sample (looping), one
-// pattern: C-3 (period 428) on channel 1, row 16 a note cut, E-3 (339)
-// on channel 2 at row 32, speed 6 (default tempo), then the song ends.
-static uint32_t makeMod(uint8_t *buf) {
- const uint32_t sampleWords = 16;
- uint32_t at;
-
- memset(buf, 0, 1084 + 1024 + sampleWords * 2);
- memcpy(buf, "dvx test", 8);
-
- // Sample 1: length (words), finetune, volume, loop start, loop length.
- buf[20 + 22] = 0;
- buf[20 + 23] = (uint8_t)sampleWords;
- buf[20 + 25] = 64;
- buf[20 + 29] = (uint8_t)sampleWords;
- buf[950] = 1; // song length
- buf[951] = 127;
- buf[952] = 0; // order 0: pattern 0
- memcpy(buf + 1080, "M.K.", 4);
- at = 1084;
-
- // Row 0 channel 0: sample 1, period 428
- buf[at] = 0x01;
- buf[at + 1] = 0xAC;
- buf[at + 2] = 0x10;
- // Row 16 channel 0: note cut (EC0)
- buf[at + 16 * 16 + 2] = 0x0E;
- buf[at + 16 * 16 + 3] = 0xC0;
- // Row 32 channel 1: sample 1, period 339
- buf[at + 32 * 16 + 4] = 0x01;
- buf[at + 32 * 16 + 4 + 1] = 0x53;
- buf[at + 32 * 16 + 4 + 2] = 0x10;
- at += 1024;
-
- for (uint32_t i = 0; i < sampleWords * 2; i++) {
- buf[at + i] = (uint8_t)(i < sampleWords ? 100 : (uint8_t)-100);
- }
-
- return at + sampleWords * 2;
-}
-
-
// A Scream Tracker 3 module: one instrument (a 64-byte sine-ish loop),
// one pattern with two notes and a note cut, eight channels.
static uint32_t makeS3m(uint8_t *buf) {
@@ -979,6 +1510,207 @@ static uint32_t makeS3m(uint8_t *buf) {
}
+// An Extended Module: two channels, one pattern with a C-4 at row 0 (the
+// rest empty), one instrument whose 8-bit sample has a ping-pong loop.
+static uint32_t makeXm(uint8_t *buf) {
+ uint32_t at = 60 + XM_HEADER_SIZE;
+
+ memset(buf, 0, BUF_MAX);
+ memcpy(buf, "Extended Module: ", 17);
+ memcpy(buf + 17, "dvx xm", 6);
+ buf[37] = 0x1A;
+ put16le(buf + 58, 0x0104);
+ put32le(buf + 60, XM_HEADER_SIZE);
+ put16le(buf + 64, 1); // song length
+ put16le(buf + 68, 2); // channels
+ put16le(buf + 70, 1); // patterns
+ put16le(buf + 72, 1); // instruments
+ put16le(buf + 74, 1); // linear frequencies
+ put16le(buf + 76, 6); // speed
+ put16le(buf + 78, 125); // BPM
+
+ // The pattern: row 0 channel 0 a note and the instrument, every other
+ // cell empty (packed)
+ put32le(buf + at, 9);
+ put16le(buf + at + 5, XM_ROWS);
+ put16le(buf + at + 7, 3 + XM_ROWS * 2 - 1);
+ at += 9;
+ buf[at] = 0x83;
+ buf[at + 1] = XM_NOTE_C4;
+ buf[at + 2] = 1;
+ at += 3;
+
+ for (uint32_t i = 1; i < XM_ROWS * 2; i++) {
+ buf[at++] = 0x80;
+ }
+
+ // The instrument (every key on its one sample), then the sample's
+ // header and its frames as deltas
+ put32le(buf + at, XM_INSTRUMENT_SIZE);
+ put16le(buf + at + 27, 1);
+ put32le(buf + at + 29, XM_SAMPLE_HEADER);
+ at += XM_INSTRUMENT_SIZE;
+ put32le(buf + at, XM_FRAMES);
+ put32le(buf + at + 4, XM_LOOP_START);
+ put32le(buf + at + 8, XM_LOOP_LEN);
+ buf[at + 12] = 64; // volume
+ buf[at + 14] = XM_PINGPONG;
+ buf[at + 15] = 128; // pan
+ at += XM_SAMPLE_HEADER;
+
+ for (uint32_t i = 0; i < XM_FRAMES; i++) {
+ buf[at + i] = i ? XM_FRAME_STEP : 0;
+ }
+
+ return at + XM_FRAMES;
+}
+
+
+// MIDI recordings: messages kept in time order (a time before the last
+// held to it), what is not a channel message refused, the saved file
+// read back (times, one-byte messages, a note still held let go at the
+// end) and loaded as music, a file that cannot be written, MusicMidiSend,
+// BASIC's numbers and packed messages, and a program's end freeing its
+// recordings.
+static void midiRecordTests(void) {
+ MusicMidiRecordT *rec = NULL;
+ MusicT *m = NULL;
+ MidiFileT file;
+ MusicInfoT info;
+ int32_t len = 0;
+
+ tCase("midi record: messages, and the ones refused");
+ sAppId = APP_A;
+ TEST_EQ(musicMidiRecordNew(&rec), MUSIC_OK);
+ TEST_EQ(musicMidiRecordCount(rec), 0);
+ TEST_EQ(musicMidiRecordLength(rec), 0);
+ TEST_EQ(musicMidiRecordAdd(rec, 0, MIDI_PROGRAM | MIDIREC_CHANNEL, MIDIREC_FLUTE, MIDIREC_VELOCITY), MUSIC_OK);
+ TEST_EQ(musicMidiRecordAdd(rec, MIDIREC_FIRST_MS, MIDI_NOTE_ON | MIDIREC_CHANNEL, MIDIREC_NOTE, MIDIREC_VELOCITY), MUSIC_OK);
+ TEST_EQ(musicMidiRecordAdd(rec, MIDIREC_EARLY_MS, MIDI_NOTE_ON | MIDIREC_CHANNEL, MIDIREC_NOTE2, MIDIREC_VELOCITY), MUSIC_OK);
+ TEST_EQ(musicMidiRecordAdd(rec, MIDIREC_OFF_MS, MIDI_NOTE_ON | MIDIREC_CHANNEL, MIDIREC_NOTE, 0), MUSIC_OK);
+ TEST_EQ(musicMidiRecordAdd(rec, MIDIREC_LAST_MS, MIDI_PITCH_BEND | MIDIREC_CHANNEL, 0, MIDI_CENTER), MUSIC_OK);
+ TEST_EQ(musicMidiRecordAdd(rec, MIDIREC_LAST_MS, MIDI_DATA_MASK, 0, 0), MUSIC_ERR_ARG);
+ TEST_EQ(musicMidiRecordAdd(rec, MIDIREC_LAST_MS, MIDIREC_CLOCK, 0, 0), MUSIC_ERR_ARG);
+ TEST_EQ(musicMidiRecordAdd(rec, MIDIREC_LAST_MS, MIDI_NOTE_ON, MUSIC_MIDI_DATA_MAX + 1, 0), MUSIC_ERR_ARG);
+ TEST_EQ(musicMidiRecordAdd(NULL, 0, MIDI_NOTE_ON, MIDIREC_NOTE, MIDIREC_VELOCITY), MUSIC_ERR_ARG);
+ TEST_EQ(musicMidiRecordCount(rec), 5);
+ TEST_EQ(musicMidiRecordLength(rec), MIDIREC_LAST_MS);
+
+ tCase("midi record: the file read back, and played");
+ TEST_EQ(musicMidiRecordSave(rec, MIDIREC_FILE), MUSIC_OK);
+
+ char *data = platformReadFile(MIDIREC_FILE, &len);
+
+ TEST_TRUE(data != NULL);
+ memset(&file, 0, sizeof(file));
+ TEST_EQ(midiParse((const uint8_t *)data, (uint32_t)len, &file), MUSIC_OK);
+ TEST_EQ(file.length, MIDIREC_LAST_MS * US_PER_MS);
+ TEST_EQ(file.count, 6);
+
+ if (file.count == 6) {
+ TEST_EQ(file.events[0].status, MIDI_PROGRAM | MIDIREC_CHANNEL);
+ TEST_EQ(file.events[0].data1, MIDIREC_FLUTE);
+ TEST_EQ(file.events[1].status, MIDI_NOTE_ON | MIDIREC_CHANNEL);
+ TEST_EQ(file.events[1].time, MIDIREC_FIRST_MS * US_PER_MS);
+ TEST_EQ(file.events[2].data1, MIDIREC_NOTE2);
+ TEST_EQ(file.events[2].time, MIDIREC_FIRST_MS * US_PER_MS);
+ TEST_EQ(file.events[3].status, MIDI_NOTE_OFF | MIDIREC_CHANNEL);
+ TEST_EQ(file.events[3].data1, MIDIREC_NOTE);
+ TEST_EQ(file.events[3].time, MIDIREC_OFF_MS * US_PER_MS);
+ TEST_EQ(file.events[4].status, MIDI_PITCH_BEND | MIDIREC_CHANNEL);
+ TEST_EQ(file.events[4].data2, MIDI_CENTER);
+ TEST_EQ(file.events[5].status, MIDI_NOTE_OFF | MIDIREC_CHANNEL);
+ TEST_EQ(file.events[5].data1, MIDIREC_NOTE2);
+ TEST_EQ(file.events[5].time, MIDIREC_LAST_MS * US_PER_MS);
+ }
+
+ midiFileFree(&file);
+ free(data);
+ TEST_EQ(musicLoad(MIDIREC_FILE, &m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.kind, MUSIC_KIND_MIDI);
+ TEST_EQ(info.lengthMs, MIDIREC_LAST_MS);
+ musicFree(m);
+
+ tCase("midi record: an empty recording; a file that cannot be written");
+ musicMidiRecordFree(rec);
+ musicMidiRecordFree(NULL);
+ TEST_EQ(musicMidiRecordNew(&rec), MUSIC_OK);
+ TEST_EQ(musicMidiRecordSave(rec, MIDIREC_FILE), MUSIC_OK);
+ data = platformReadFile(MIDIREC_FILE, &len);
+ memset(&file, 0, sizeof(file));
+ TEST_EQ(midiParse((const uint8_t *)data, (uint32_t)len, &file), MUSIC_OK);
+ TEST_EQ(file.count, 0);
+ midiFileFree(&file);
+ free(data);
+ TEST_EQ(musicMidiRecordSave(rec, MIDIREC_NO_DIR), MUSIC_ERR_WRITE);
+ TEST_STR_EQ(musicErrorText(MUSIC_ERR_WRITE), "The file could not be written");
+ TEST_EQ(musicMidiRecordSave(rec, ""), MUSIC_ERR_ARG);
+ TEST_EQ(musicMidiRecordSave(NULL, MIDIREC_FILE), MUSIC_ERR_ARG);
+ musicMidiRecordFree(rec);
+
+ tCase("midi record: MusicMidiSend plays a keyboard's messages");
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ TEST_EQ(musicMidiSend(m, MIDI_NOTE_ON | MIDIREC_CHANNEL, MIDIREC_NOTE, MIDIREC_VELOCITY), MUSIC_OK);
+ TEST_EQ(musicMidiSend(m, MIDI_PROGRAM, MIDIREC_FLUTE, MUSIC_MIDI_DATA_MAX + 1), MUSIC_OK);
+ TEST_EQ(musicMidiSend(m, MIDI_DATA_MASK, 0, 0), MUSIC_ERR_ARG);
+ TEST_EQ(musicMidiSend(m, MIDIREC_CLOCK, 0, 0), MUSIC_ERR_ARG);
+ TEST_EQ(musicMidiSend(NULL, MIDI_NOTE_ON, MIDIREC_NOTE, MIDIREC_VELOCITY), MUSIC_ERR_ARG);
+ musicFree(m);
+
+ tCase("midi record: BASIC numbers and packed messages");
+ int32_t r = MidiRecordNew();
+ int32_t live = MusicMidiOpen();
+
+ TEST_TRUE(r > 0 && live > 0);
+ MidiRecordAdd(r, MIDIREC_FIRST_MS, MIDI_NOTE_ON | (MIDIREC_NOTE << AUDIO_MIDI_PACKED_DATA1_SHIFT) | (MIDIREC_VELOCITY << AUDIO_MIDI_PACKED_DATA2_SHIFT));
+ TEST_STR_EQ(MusicError(), "No error");
+ MidiRecordAdd(r, -1, MIDI_NOTE_OFF | (MIDIREC_NOTE << AUDIO_MIDI_PACKED_DATA1_SHIFT));
+ TEST_EQ(MidiRecordCount(r), 2);
+ TEST_EQ(MidiRecordLength(r), MIDIREC_FIRST_MS);
+ MidiRecordAdd(r, MIDIREC_LAST_MS, AUDIO_MIDI_PACKED_NONE);
+ TEST_STR_EQ(MusicError(), "Invalid argument");
+ TEST_EQ(MidiRecordCount(r), 2);
+ TEST_EQ(MidiRecordSave(r, MIDIREC_FILE), -1);
+ TEST_EQ(MidiRecordSave(r, MIDIREC_NO_DIR), 0);
+ TEST_STR_EQ(MusicError(), "The file could not be written");
+ MusicMidiSend(live, MIDI_NOTE_ON | (MIDIREC_NOTE << AUDIO_MIDI_PACKED_DATA1_SHIFT) | (MIDIREC_VELOCITY << AUDIO_MIDI_PACKED_DATA2_SHIFT));
+ TEST_STR_EQ(MusicError(), "No error");
+ MusicMidiSend(live, MIDIREC_CLOCK);
+ TEST_STR_EQ(MusicError(), "Invalid argument");
+
+ // Another program's number does not work; a freed one neither
+ sAppId = APP_B;
+ TEST_EQ(MidiRecordCount(r), 0);
+ TEST_STR_EQ(MusicError(), "Invalid argument");
+ sAppId = APP_A;
+ MidiRecordFree(r);
+ TEST_EQ(MidiRecordCount(r), 0);
+ TEST_STR_EQ(MusicError(), "Invalid argument");
+
+ // Every number taken: no room
+ for (int32_t i = 0; i < MIDIREC_BAS_SLOTS; i++) {
+ TEST_TRUE(MidiRecordNew() > 0);
+ }
+
+ TEST_EQ(MidiRecordNew(), 0);
+ TEST_STR_EQ(MusicError(), "Not enough memory");
+
+ // The program's end frees its recordings (LeakSanitizer would say)
+ // and forgets their numbers
+ sAppId = APP_B;
+ TEST_EQ(musicMidiRecordNew(&rec), MUSIC_OK);
+ sAppId = 0;
+ musicAppEnded(APP_A);
+ musicAppEnded(APP_B);
+ sAppId = APP_A;
+ TEST_TRUE(MidiRecordNew() == 1);
+ musicAppEnded(APP_A);
+ sAppId = 0;
+ unlink(MIDIREC_FILE);
+}
+
+
static void mmlPlayTests(void) {
MusicMmlT *m = NULL;
char bg[MML_BG_NOTES + 8];
@@ -1196,6 +1928,253 @@ static void mmlTests(void) {
}
+// Modules on the fake wavetable synthesizer's voices: the samples there,
+// the frames the timer plays, pause, volume, seek, the end, giving the
+// synthesizer up to MIDI, a ping-pong loop unrolled, and a stream when
+// the module cannot play there.
+static void modHwTests(void) {
+ static uint8_t mod[BUF_MAX];
+ static uint8_t xm[BUF_MAX];
+ static uint8_t sf[SF_TEST_MAX];
+ static uint8_t smf[BUF_MAX];
+ static const uint8_t track[] = { 0x00, 0xC0, 0, 0x00, 0x90, 40, 127, 0x60, 0x80, 40, 0, 0x00, 0xFF, 0x2F, 0x00 };
+ const uint8_t *const tracks[] = { track };
+ const uint32_t lens[] = { sizeof(track) };
+ SfSpecT spec = { 2, SF_TEST_FRAMES, 0 };
+ static uint8_t twice[BUF_MAX];
+ uint32_t modSize = makeMod(mod);
+ uint32_t xmSize = makeXm(xm);
+ uint32_t smfSize = buildSmf(smf, 0, 96, tracks, lens, 1);
+ MusicInfoT info;
+ AudioWtInfoT wt;
+ MusicT *m = NULL;
+ MusicT *m2 = NULL;
+ int32_t starts;
+ int32_t atten;
+ uint32_t at;
+
+ // The module with its pattern played twice (a seek goes to the start
+ // of a pattern)
+ memcpy(twice, mod, modSize);
+ twice[950] = 2;
+ musicShutdown();
+ TEST_EQ(platformMkdirRecursive(DVX_USER_DIR "/" DVX_BANKS_DIR), 0);
+ writeFile(SF_TEST_FILE, sf, sfBuild(&spec, sf));
+
+ tCase("modhw: a module on the wavetable synthesizer's voices");
+ setIni("fakeWtVoices = 8\n");
+ sWtStarts = 0;
+ sWtUploads = 0;
+ TEST_EQ(musicLoadMem(mod, modSize, &m), MUSIC_OK);
+ musicSetEndFn(m, onEnd, NULL);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ TEST_STR_EQ(info.output, "Fake wavetable (8 voices)");
+ TEST_EQ(sWtUploads, 1);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.memory - wt.freeMemory, MODHW_SAMPLE_BYTES);
+ pump(RATE_DEV * MODHW_FRAME_MS / 1000);
+ TEST_EQ(sWtStarts, 1);
+ TEST_TRUE(sWtActive != 0);
+ TEST_EQ(sWtVoice[sWtLast].pitch, MODHW_PITCH_428);
+ TEST_EQ(sWtVoice[sWtLast].attenuation, MODHW_ATTEN_FULL);
+ TEST_TRUE(sWtVoice[sWtLast].pan < 0);
+ TEST_EQ(sWtVoice[sWtLast].loop, AUDIO_WT_LOOP_ON);
+ TEST_EQ(sWtVoice[sWtLast].loopStart, 0);
+ TEST_EQ(sWtVoice[sWtLast].loopEnd, 32);
+ TEST_EQ(sWtVoice[sWtLast].end, 32);
+ TEST_EQ(sWtVoice[sWtLast].bits, 16);
+ TEST_EQ(modHwWord(sWtVoice[sWtLast].addr, 0), 100 << 8);
+ TEST_EQ(modHwWord(sWtVoice[sWtLast].addr, 16), -100 * 256);
+ TEST_EQ(modHwWord(sWtVoice[sWtLast].addr, 32), 100 << 8);
+
+ tCase("modhw: the note cut silences the voice; the next note starts one");
+ pump(RATE_DEV * (MODHW_CUT_MS - MODHW_FRAME_MS) / 1000);
+ TEST_EQ(sWtStarts, 1);
+ TEST_EQ(sWtVoice[sWtLast].attenuation, AUDIO_WT_ATTEN_MAX);
+ pump(RATE_DEV * (MODHW_NOTE2_MS - MODHW_CUT_MS) / 1000);
+ TEST_EQ(sWtStarts, 2);
+ TEST_EQ(sWtVoice[sWtLast].pitch, MODHW_PITCH_339);
+ TEST_TRUE(sWtVoice[sWtLast].pan > 0);
+ at = musicGetPosition(m);
+ TEST_TRUE(at >= MODHW_NOTE2_MS - MODHW_FRAME_MS && at <= MODHW_NOTE2_MS + MODHW_FRAME_MS);
+
+ tCase("modhw: pause stops the voices; resume starts them again");
+ TEST_EQ(musicPause(m), MUSIC_OK);
+ TEST_EQ(sWtActive, 0);
+ at = musicGetPosition(m);
+ pump(RATE_DEV / 10);
+ TEST_EQ(musicGetPosition(m), at);
+ starts = sWtStarts;
+ TEST_EQ(musicResume(m), MUSIC_OK);
+ pump(RATE_DEV / 100);
+ TEST_TRUE(sWtStarts > starts);
+ TEST_EQ(sWtVoice[sWtLast].pitch, MODHW_PITCH_339);
+ TEST_TRUE(sWtActive != 0);
+
+ tCase("modhw: the music's volume reaches the voices at the next frame");
+ atten = sWtVoice[sWtLast].attenuation;
+ TEST_EQ(musicSetVolume(m, FULL / 2), MUSIC_OK);
+ pump(RATE_DEV * 2 * MODHW_FRAME_MS / 1000);
+ TEST_EQ(sWtVoice[sWtLast].attenuation, atten + MODHW_ATTEN_HALF);
+ TEST_EQ(musicSetVolume(m, FULL), MUSIC_OK);
+
+ tCase("modhw: the song ends; played again it starts over");
+ sEnds = 0;
+ pump(RATE_DEV * 5);
+ TEST_EQ(sEnds, 1);
+ TEST_TRUE(!musicIsPlaying(m));
+ TEST_EQ(sWtActive, 0);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ TEST_TRUE(musicGetPosition(m) < MODHW_FRAME_MS);
+ starts = sWtStarts;
+ pump(RATE_DEV * MODHW_FRAME_MS / 1000);
+ TEST_EQ(sWtStarts, starts + 1);
+ TEST_EQ(sWtVoice[sWtLast].pitch, MODHW_PITCH_428);
+
+ tCase("modhw: a looping song goes on past its end; a stop");
+ TEST_EQ(musicStop(m), MUSIC_OK);
+ TEST_EQ(musicGetPosition(m), 0);
+ TEST_EQ(sWtActive, 0);
+ TEST_EQ(musicPlay(m, true), MUSIC_OK);
+ sEnds = 0;
+ pump(RATE_DEV * 9);
+ TEST_TRUE(musicIsPlaying(m));
+ TEST_EQ(sEnds, 0);
+ musicFree(m);
+
+ tCase("modhw: a seek; one made while stopped is where the play starts");
+ TEST_EQ(musicLoadMem(twice, modSize, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ pump(RATE_DEV * MODHW_FRAME_MS / 1000);
+ TEST_EQ(musicSetPosition(m, MODHW_SEEK_MS), MUSIC_OK);
+ at = musicGetPosition(m);
+ TEST_TRUE(at + MODHW_SEEK_TOL_MS >= MODHW_ORDER_MS && at <= MODHW_ORDER_MS + MODHW_SEEK_TOL_MS);
+ starts = sWtStarts;
+ pump(RATE_DEV * MODHW_FRAME_MS / 1000);
+ TEST_EQ(sWtStarts, starts + 1);
+ TEST_EQ(musicStop(m), MUSIC_OK);
+ TEST_EQ(musicSetPosition(m, MODHW_SEEK_MS), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ TEST_TRUE(musicGetPosition(m) + MODHW_SEEK_TOL_MS >= MODHW_ORDER_MS);
+ TEST_EQ(musicStop(m), MUSIC_OK);
+
+ tCase("modhw: a stopped module gives the synthesizer up to MIDI");
+ setIni("fakeWtVoices = 8\nsoundFont = " SF_TEST_NAME "\n");
+ TEST_EQ(musicLoadMem(smf, smfSize, &m2), MUSIC_OK);
+ TEST_EQ(musicPlay(m2, false), MUSIC_OK);
+ musicGetInfo(m2, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_NONE);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.memory - wt.freeMemory, WT_RAMP_BYTES);
+
+ // ... and while MIDI plays there, the module plays on a stream
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_STREAM);
+ musicFree(m2);
+ musicFree(m);
+ musicShutdown();
+
+ tCase("modhw: the SoundFont kept in the synthesizer gives way to a module");
+ setIni("fakeWtVoices = 8\nfakeWtMemory = 256\nsoundFont = " SF_TEST_NAME "\n");
+ TEST_EQ(musicLoadMem(smf, smfSize, &m2), MUSIC_OK);
+ TEST_EQ(musicPlay(m2, false), MUSIC_OK);
+ musicFree(m2);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.memory - wt.freeMemory, WT_RAMP_BYTES);
+ TEST_EQ(musicLoadMem(mod, modSize, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.memory - wt.freeMemory, MODHW_SAMPLE_BYTES);
+ musicFree(m);
+ musicShutdown();
+
+ tCase("modhw: a chip louder than digital sound plays a module's voices that much quieter");
+ setIni("fakeWtVoices = 8\nfakeWtLevel = 76\n");
+ TEST_EQ(musicLoadMem(mod, modSize, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ pump(RATE_DEV * MODHW_FRAME_MS / 1000);
+ TEST_EQ(sWtVoice[sWtLast].attenuation, MODHW_ATTEN_FULL + 76);
+ musicFree(m);
+ musicShutdown();
+
+ tCase("modhw: an 8-bit sample stays 8-bit where the chip plays those");
+ setIni("fakeWtVoices = 8\nfakeWtFlags = 19\n");
+ TEST_EQ(musicLoadMem(mod, modSize, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ pump(RATE_DEV * MODHW_FRAME_MS / 1000);
+ TEST_EQ(sWtVoice[sWtLast].bits, 8);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.memory - wt.freeMemory, MODHW_SAMPLE_BYTES / 2);
+ musicFree(m);
+ musicShutdown();
+
+ tCase("modhw: a ping-pong loop unrolled forward");
+ setIni("fakeWtVoices = 8\n");
+ TEST_EQ(musicLoadMem(xm, xmSize, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ pump(RATE_DEV * MODHW_FRAME_MS / 1000);
+ TEST_EQ(sWtVoice[sWtLast].loop, AUDIO_WT_LOOP_ON);
+ TEST_EQ(sWtVoice[sWtLast].loopStart, XM_LOOP_START);
+ TEST_EQ(sWtVoice[sWtLast].loopEnd, XM_STORED);
+ TEST_EQ(sWtVoice[sWtLast].end, XM_STORED);
+ TEST_EQ(sWtVoice[sWtLast].pitch, MODHW_PITCH_XM_C4);
+
+ // There: frames 0 .. 11, then libxmp's way back (11 read twice at
+ // the turn, down to 5), then the loop again as the guard
+ static const uint8_t stored[XM_STORED + 8] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 11, 11, 10, 9, 8, 7, 6, 5, 4, 5, 6, 7, 8, 9, 10, 11 };
+
+ for (uint32_t i = 0; i < sizeof(stored); i++) {
+ TEST_EQ(modHwWord(sWtVoice[sWtLast].addr, i), stored[i] * XM_FRAME_STEP * 256);
+ }
+
+ musicFree(m);
+ musicShutdown();
+
+ tCase("modhw: DVX.INI modules = software, too few voices, or too little memory: a stream");
+ setIni("fakeWtVoices = 8\nmodules = software\n");
+ TEST_EQ(musicLoadMem(mod, modSize, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_STREAM);
+ musicFree(m);
+ musicShutdown();
+ setIni("fakeWtVoices = 2\n");
+ TEST_EQ(musicLoadMem(twice, modSize, &m), MUSIC_OK);
+ TEST_EQ(musicSetPosition(m, MODHW_SEEK_MS), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_STREAM);
+ TEST_TRUE(musicGetPosition(m) + MODHW_SEEK_TOL_MS >= MODHW_ORDER_MS);
+ musicFree(m);
+ musicShutdown();
+ setIni("fakeWtVoices = 8\nfakeWtMemory = 64\n");
+ TEST_EQ(musicLoadMem(mod, modSize, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_STREAM);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.freeMemory, wt.memory);
+ musicFree(m);
+ musicShutdown();
+}
+
+
+// Frame frame of the 16-bit sample at addr in the fake synthesizer's
+// memory.
+static int16_t modHwWord(uint32_t addr, uint32_t frame) {
+ return (int16_t)(sWtMemory[addr + frame * 2] | (sWtMemory[addr + frame * 2 + 1] << 8));
+}
+
+
static void mt32Tests(void) {
static MpuSinkT mpu;
@@ -1371,9 +2350,15 @@ static void op2Tests(void) {
// The largest sample (either side) of the rendered 16-bit stereo frames.
static int32_t outPeak(uint32_t frames) {
+ return outPeakFrom(0, frames);
+}
+
+
+// outPeak of frames frames from frame from.
+static int32_t outPeakFrom(uint32_t from, uint32_t frames) {
int32_t peak = 0;
- for (uint32_t i = 0; i < frames * 2; i++) {
+ for (uint32_t i = from * 2; i < (from + frames) * 2; i++) {
int32_t v = sOut[i] < 0 ? -sOut[i] : sOut[i];
peak = v > peak ? v : peak;
@@ -1395,6 +2380,209 @@ static double outTone(uint32_t from, uint32_t n, double hz) {
}
+// Gravis UltraSound patch sets the tests write: the bank patLoadSet
+// makes of them (programs, keys, pitch, loops, the drum kit, the data
+// read from the files), the INI's forms and mistakes, ULTRADIR's own
+// set, and its notes played at their pitch.
+static void patTests(void) {
+ static const PatWaveSpecT sine[] = { { PAT_SINE_FRAMES, PAT_SINE_PERIOD, PAT_SINE_LOOP, PAT_LOW_ALL, PAT_HIGH_ALL, PAT_SINE_ROOT, PAT_SEMI, PAT_MODE_16BIT | PAT_MODE_LOOP } };
+ static const PatWaveSpecT two[] = {
+ { 200, 20, 0, PAT_LOW_ALL, PAT_SPLIT_HIGH, PAT_SPLIT_HIGH, PAT_SEMI, PAT_MODE_UNSIGNED },
+ { 200, 10, 0, PAT_SPLIT_HIGH, PAT_HIGH_ALL, PAT_HIGH_ALL, PAT_SEMI, PAT_MODE_UNSIGNED }
+ };
+ static const PatWaveSpecT kick[] = { { 300, 30, 0, PAT_LOW_ALL, PAT_HIGH_ALL, PAT_SINE_ROOT, 0, PAT_MODE_16BIT } };
+ char cwd[DVX_MAX_PATH];
+ char dir[DVX_MAX_PATH];
+ char ini[DVX_MAX_PATH];
+ char path[DVX_MAX_PATH];
+ char text[PAT_FILE_MAX];
+ int16_t frames[4];
+ WaveBankT bank;
+ MusicInfoT info;
+ MusicT *m = NULL;
+
+ TEST_TRUE(getcwd(cwd, sizeof(cwd)) != NULL);
+ TEST_EQ(platformMkdirRecursive(PAT_DIR "/MIDI"), 0);
+ TEST_EQ(platformMkdirRecursive(PAT_DIR "/OTHER"), 0);
+ TEST_EQ(platformMkdirRecursive(PAT_LEGACY_DIR "/MIDI"), 0);
+ TEST_EQ(platformMkdirRecursive(PAT_EMPTY_DIR), 0);
+ TEST_EQ(platformMkdirRecursive(PAT_NONE_DIR), 0);
+ patWrite(PAT_DIR "/MIDI/SINE.PAT", sine, 1);
+ patWrite(PAT_DIR "/MIDI/TWO.PAT", two, 2);
+ patWrite(PAT_DIR "/MIDI/KICK.PAT", kick, 1);
+ patWrite(PAT_DIR "/OTHER/OTHER.PAT", sine, 1);
+ patWrite(PAT_LEGACY_DIR "/MIDI/SINE.PAT", sine, 1);
+ patWrite(PAT_LEGACY_DIR "/MIDI/KICK.PAT", kick, 1);
+ writeFile(PAT_DIR "/MIDI/DAMAGED.PAT", "GF1PATCH?? not a patch", 22);
+ snprintf(text, sizeof(text), "[Ultrasound]\nPatchDir=%s/" PAT_DIR "/OTHER\n[Melodic Bank 0]\n0=SINE\n1=TWO\n2=MISSING\n3=DAMAGED\n4=BLANK\n5=OTHER\n[Drum Bank 0]\n%d=KICK\n", cwd, PAT_KICK_KEY);
+ writeFile(PAT_DIR "/" MUSIC_PATCH_SET_INI, text, (uint32_t)strlen(text));
+ snprintf(text, sizeof(text), "[Melodic Patches]\n0=SINE\n[Drum Patches]\n%d=KICK\n", PAT_KICK_KEY);
+ writeFile(PAT_LEGACY_DIR "/" MUSIC_PATCH_SET_INI, text, (uint32_t)strlen(text));
+ snprintf(text, sizeof(text), "[Melodic Bank 0]\n0=MISSING\n");
+ writeFile(PAT_EMPTY_DIR "/" MUSIC_PATCH_SET_INI, text, (uint32_t)strlen(text));
+
+ tCase("pat: a patch set's programs and kit");
+ TEST_EQ(patLoadSet(PAT_DIR "/" MUSIC_PATCH_SET_INI, &bank), MUSIC_OK);
+ TEST_EQ(bank.presetCount, 4);
+ TEST_EQ(bank.sampleCount, 5);
+ TEST_EQ(bank.fileCount, 4);
+
+ if (bank.presetCount == 4) {
+ const WavePresetT *p = bank.presets;
+
+ TEST_EQ(p[0].program, 0);
+ TEST_EQ(p[1].program, 1);
+ TEST_EQ(p[2].program, 5);
+ TEST_EQ(p[3].bank, SF_DRUM_BANK);
+ TEST_STR_EQ(p[0].name, "SINE.PAT");
+
+ tCase("pat: one wave over every key, its pitch and loop");
+ const WaveRegionT *r = &bank.regions[p[0].first];
+ const WaveSampleT *w = &bank.samples[r->sample];
+
+ TEST_EQ(p[0].count, 1);
+ TEST_EQ(r->keyLo, 0);
+ TEST_EQ(r->keyHi, PAT_KEY_TOP);
+ TEST_EQ(w->rate, PAT_RATE);
+ TEST_EQ(w->rootKey, PAT_PIVOT);
+ TEST_EQ(w->end - w->start, PAT_SINE_FRAMES);
+ TEST_EQ(w->loopStart - w->start, PAT_SINE_LOOP);
+ TEST_EQ(w->loopEnd - w->start, PAT_SINE_FRAMES);
+ TEST_EQ(r->gen[SF_GEN_SAMPLE_MODES], SF_LOOP_ON);
+ TEST_EQ(r->gen[SF_GEN_SCALE_TUNING], 100);
+ int32_t tune = r->gen[SF_GEN_COARSE_TUNE] * 100 + r->gen[SF_GEN_FINE_TUNE];
+
+ TEST_TRUE(tune >= PAT_SINE_CENTS - 1 && tune <= PAT_SINE_CENTS + 1);
+ TEST_TRUE(r->gen[SF_GEN_FINE_TUNE] > -100 && r->gen[SF_GEN_FINE_TUNE] < 100);
+ TEST_TRUE(waveBankRead(&bank, w->start, 4, frames));
+ TEST_EQ(frames[0], 0);
+ TEST_EQ(frames[1], (int16_t)lround(PAT_AMP_16 * sin(2.0 * M_PI / PAT_SINE_PERIOD)));
+
+ tCase("pat: two waves split at their frequencies, 8-bit unsigned");
+ r = &bank.regions[p[1].first];
+ TEST_EQ(p[1].count, 2);
+ TEST_EQ(r[0].keyLo, 0);
+ TEST_EQ(r[0].keyHi, PAT_PIVOT);
+ TEST_EQ(r[1].keyLo, PAT_PIVOT + 1);
+ TEST_EQ(r[1].keyHi, PAT_KEY_TOP);
+ TEST_EQ(r[0].gen[SF_GEN_SAMPLE_MODES], SF_LOOP_NONE);
+ TEST_TRUE(waveBankRead(&bank, bank.samples[r[0].sample].start, 4, frames));
+ TEST_EQ(frames[0], 0);
+ TEST_EQ(frames[1], (int16_t)(((int32_t)lround(PAT_AMP_8 * sin(2.0 * M_PI / 20)) & 0xFF) << 8));
+
+ tCase("pat: PatchDir's patch, the kit's key, unscaled");
+ TEST_EQ(bank.regions[p[2].first].keyHi, PAT_KEY_TOP);
+ r = &bank.regions[p[3].first];
+ TEST_EQ(p[3].count, 1);
+ TEST_EQ(r->keyLo, PAT_KICK_KEY);
+ TEST_EQ(r->keyHi, PAT_KICK_KEY);
+ TEST_EQ(r->gen[SF_GEN_SCALE_TUNING], 0);
+ }
+
+ waveBankFree(&bank);
+
+ tCase("pat: the older sections, and an INI with nothing to play");
+ TEST_EQ(patLoadSet(PAT_LEGACY_DIR "/" MUSIC_PATCH_SET_INI, &bank), MUSIC_OK);
+ TEST_EQ(bank.presetCount, 2);
+ waveBankFree(&bank);
+ TEST_EQ(patLoadSet(PAT_EMPTY_DIR "/" MUSIC_PATCH_SET_INI, &bank), MUSIC_ERR_BANK);
+ TEST_EQ(patLoadSet(PAT_NONE_DIR "/" MUSIC_PATCH_SET_INI, &bank), MUSIC_ERR_OPEN);
+
+ tCase("pat: ULTRADIR's patch set");
+ unsetenv(MUSIC_PATCH_SET_ENV);
+ TEST_TRUE(!musicPatchSetPath(path, sizeof(path)));
+ snprintf(dir, sizeof(dir), "%s/" PAT_NONE_DIR, cwd);
+ setenv(MUSIC_PATCH_SET_ENV, dir, 1);
+ TEST_TRUE(!musicPatchSetPath(path, sizeof(path)));
+ snprintf(dir, sizeof(dir), "%s/" PAT_DIR, cwd);
+ snprintf(ini, sizeof(ini), "%s/" MUSIC_PATCH_SET_INI, dir);
+ setenv(MUSIC_PATCH_SET_ENV, dir, 1);
+ TEST_TRUE(musicPatchSetPath(path, sizeof(path)));
+ TEST_STR_EQ(path, ini);
+ TEST_STR_EQ(MusicPatchSet(), ini);
+
+ tCase("pat: in place of the standard SoundFont, at its pitch");
+ TEST_TRUE(!platformKindFind(path, sizeof(path), DvxKindBanksE, MUSIC_SOUND_FONT_DEFAULT));
+ setIni("midi = softwave\n");
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_SOFTWAVE);
+ TEST_EQ(musicMidiNoteOn(m, 0, PAT_A4_KEY, 127), MUSIC_OK);
+ pump(RATE_DEV / 2);
+
+ double a4 = outTone(RATE_DEV / 10, RATE_DEV / 4, PAT_A4_HZ);
+
+ TEST_TRUE(a4 > 4.0 * outTone(RATE_DEV / 10, RATE_DEV / 4, PAT_A4_HZ * 0.75));
+ TEST_TRUE(a4 > 4.0 * outTone(RATE_DEV / 10, RATE_DEV / 4, PAT_A4_HZ * 1.25));
+ TEST_EQ(musicMidiNoteOn(m, MIDI_PERCUSSION, PAT_KICK_KEY, 127), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ TEST_TRUE(outPeak(RATE_DEV / 4) > 0);
+ musicFree(m);
+
+ tCase("pat: named by DVX.INI and by musicSetBank");
+ unsetenv(MUSIC_PATCH_SET_ENV);
+ snprintf(text, sizeof(text), "midi = softwave\nsoundFont = %s\n", ini);
+ setIni(text);
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ TEST_EQ(musicMidiNoteOn(m, 0, PAT_A4_KEY, 127), MUSIC_OK);
+ pump(RATE_DEV / 2);
+ TEST_TRUE(outTone(RATE_DEV / 10, RATE_DEV / 4, PAT_A4_HZ) > 4.0 * outTone(RATE_DEV / 10, RATE_DEV / 4, PAT_A4_HZ * 0.75));
+ musicFree(m);
+ setIni("midi = softwave\n");
+ TEST_EQ(musicLoadMacro("T120 L4 O3 A", &m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, ini), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_SOFTWAVE);
+ musicFree(m);
+ TEST_STR_EQ(MusicPatchSet(), "");
+}
+
+
+// A patch of count waves (see PatWaveSpecT) at path.
+static void patWrite(const char *path, const PatWaveSpecT *waves, int32_t count) {
+ static uint8_t buf[BUF_MAX];
+ uint32_t len = PAT_HEAD_BYTES + PAT_SKIP_BYTES;
+
+ memset(buf, 0, sizeof(buf));
+ memcpy(buf, "GF1PATCH110\0ID#000002", 21);
+ put16le(buf + PAT_WAVES_AT, (uint32_t)count);
+
+ for (int32_t i = 0; i < count; i++) {
+ const PatWaveSpecT *w = &waves[i];
+ uint32_t bytes = w->modes & PAT_MODE_16BIT ? 2 : 1;
+ uint8_t *h = buf + len;
+
+ put32le(h + PAT_W_SIZE, w->frames * bytes);
+ put32le(h + PAT_W_LOOP_START, w->loopStart * bytes);
+ put32le(h + PAT_W_LOOP_END, w->frames * bytes);
+ put16le(h + PAT_W_RATE, PAT_RATE);
+ put32le(h + PAT_W_LOW, w->low);
+ put32le(h + PAT_W_HIGH, w->high);
+ put32le(h + PAT_W_ROOT, w->root);
+ h[PAT_W_BALANCE] = 7;
+ h[PAT_W_MODES] = w->modes;
+ put16le(h + PAT_W_SCALE_KEY, PAT_PIVOT);
+ put16le(h + PAT_W_SCALE_FACTOR, w->scaleFactor);
+ len += PAT_WAVE_HEAD;
+
+ for (uint32_t f = 0; f < w->frames; f++) {
+ double v = sin(2.0 * M_PI * f / w->period);
+
+ if (bytes == 2) {
+ put16le(buf + len, (uint32_t)(uint16_t)(int16_t)lround(PAT_AMP_16 * v));
+ } else {
+ buf[len] = (uint8_t)(PAT_SILENCE_8 + lround(PAT_AMP_8 * v));
+ }
+
+ len += bytes;
+ }
+ }
+
+ writeFile(path, buf, len);
+}
+
+
static void playTests(void) {
static uint8_t smf[BUF_MAX];
static uint8_t mod[BUF_MAX];
@@ -1403,16 +2591,13 @@ static void playTests(void) {
AudioMidiInfoT port;
MusicT *m = NULL;
MusicT *m2 = NULL;
- const uint8_t flute[] = { 0x00, 0xC0, 73, 0x00, 0x90, 69, 127, 0x60, 0x80, 69, 0, 0x00, 0xFF, 0x2F, 0x00 };
const uint8_t chord[] = { 0x00, 0xC0, 73, 0x00, 0x90, 60, 127, 0x00, 64, 127, 0x00, 67, 127, 0x60, 0x80, 60, 0, 0x00, 64, 0, 0x00, 67, 0, 0x00, 0xFF, 0x2F, 0x00 };
const uint8_t *tracks[1];
uint32_t lens[1];
uint32_t size;
uint32_t modSize = makeMod(mod);
- tracks[0] = flute;
- lens[0] = sizeof(flute);
- size = buildSmf(smf, 0, 96, tracks, lens, 1);
+ size = fluteSmf(smf);
tCase("play: without an instrument bank MIDI needs the MPU-401 port");
TEST_EQ(rename(MUSIC_BANK_DEFAULT, BANK_ASIDE), 0);
@@ -1553,9 +2738,7 @@ static void playTests(void) {
tCase("play: the MPU-401 port for one music; the next one uses the FM chip");
setIni("midi = mpu401\n");
- tracks[0] = flute;
- lens[0] = sizeof(flute);
- size = buildSmf(smf, 0, 96, tracks, lens, 1);
+ size = fluteSmf(smf);
sMidiCount = 0;
TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
TEST_EQ(musicLoadMem(smf, size, &m2), MUSIC_OK);
@@ -1683,26 +2866,6 @@ static void pump(uint32_t frames) {
}
-static void put16le(uint8_t *p, uint32_t v) {
- p[0] = (uint8_t)v;
- p[1] = (uint8_t)(v >> 8);
-}
-
-
-static void put32be(uint8_t *p, uint32_t v) {
- p[0] = (uint8_t)(v >> 24);
- p[1] = (uint8_t)(v >> 16);
- p[2] = (uint8_t)(v >> 8);
- p[3] = (uint8_t)v;
-}
-
-
-static void put32le(uint8_t *p, uint32_t v) {
- put16le(p, v);
- put16le(p + 2, v >> 16);
-}
-
-
static int32_t recAlloc(void *owner) {
RecBackT *b = (RecBackT *)owner;
@@ -1874,6 +3037,525 @@ static void setIni(const char *text) {
}
+// A SoundFont 1 bank: a ROM sample (SF1_ROM_*), then one of its own (a
+// ramp, SF1_RAM_*), each loop kept one and two frames lower; "Organ" 0:0
+// (a global zone of scale tuning 1 and reverb 255, a zone of pan 0 over
+// "Organ": samplePitch 69.50, sustain 100, attack 500 ms, pan 127, level
+// 64, coarse tune 2 keys on the RAM sample, looping) and "Rom" 0:1 over
+// "Rom" (the ROM sample). No modulator arrays.
+static uint32_t sf1Build(uint8_t *out) {
+ static const uint16_t pgen[][2] = { { 56, 1 }, { 16, 255 }, { 17, 0 }, { 41, 0 }, { 41, 1 }, { 0, 0 } };
+ static const uint16_t pbag[] = { 0, 2, 4, 5 };
+ static const uint16_t igen[][2] = { { 55, SF1_ROOT_PITCH }, { 37, 100 }, { 34, 500 }, { 17, 127 }, { 48, 64 }, { 51, 2 }, { 54, 1 }, { 53, 1 }, { 53, 0 }, { 0, 0 } };
+ static const uint16_t ibag[] = { 0, 8, 9 };
+ static const uint32_t shdr[][4] = { { SF1_ROM_START, SF1_ROM_END, SF1_ROM_LOOP - 1, SF1_ROM_LOOP_END - 2 }, { 0, SF1_RAM_END, SF1_RAM_LOOP - 1, SF1_RAM_LOOP_END - 2 }, { 0, 0, 0, 0 } };
+ static uint8_t body[SF_TEST_MAX];
+ static uint8_t list[SF_TEST_MAX];
+ uint32_t len;
+ uint32_t listLen;
+
+ memcpy(list, "INFO", 4);
+ listLen = 4;
+ put16le(body, 1);
+ put16le(body + 2, 0);
+ sfChunk(list, &listLen, "ifil", body, 4);
+ memcpy(out + 12, "LIST", 4);
+ put32le(out + 16, listLen);
+ memcpy(out + 20, list, listLen);
+ len = 20 + listLen;
+
+ memcpy(list, "sdta", 4);
+ listLen = 4;
+ memset(body, 0, sizeof(body));
+ memcpy(body, "RomSample", 9);
+ memcpy(body + 20, "RamSample", 9);
+ sfChunk(list, &listLen, "snam", body, 2 * 20);
+
+ for (int32_t i = 0; i < SF1_RAM_FRAMES; i++) {
+ put16le(body + i * 2, (uint32_t)(uint16_t)(int16_t)(i * 100));
+ }
+
+ sfChunk(list, &listLen, "smpl", body, SF1_RAM_FRAMES * 2);
+ memcpy(out + len, "LIST", 4);
+ put32le(out + len + 4, listLen);
+ memcpy(out + len + 8, list, listLen);
+ len += 8 + listLen;
+
+ memcpy(list, "pdta", 4);
+ listLen = 4;
+ memset(body, 0, sizeof(body));
+ memcpy(body, "Organ", 5);
+ put16le(body + 24, 0);
+ memcpy(body + 38, "Rom", 3);
+ put16le(body + 38 + 20, 1);
+ put16le(body + 38 + 24, 2);
+ memcpy(body + 76, "EOP", 3);
+ put16le(body + 76 + 24, 3);
+ sfChunk(list, &listLen, "phdr", body, 3 * 38);
+ memset(body, 0, sizeof(body));
+
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(pbag); i++) {
+ put16le(body + i * 4, pbag[i]);
+ }
+
+ sfChunk(list, &listLen, "pbag", body, DVX_ARRAY_LEN(pbag) * 4);
+
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(pgen); i++) {
+ put16le(body + i * 4, pgen[i][0]);
+ put16le(body + i * 4 + 2, pgen[i][1]);
+ }
+
+ sfChunk(list, &listLen, "pgen", body, DVX_ARRAY_LEN(pgen) * 4);
+ memset(body, 0, sizeof(body));
+ memcpy(body, "Organ", 5);
+ memcpy(body + 22, "Rom", 3);
+ put16le(body + 22 + 20, 1);
+ memcpy(body + 44, "EOI", 3);
+ put16le(body + 44 + 20, 2);
+ sfChunk(list, &listLen, "inst", body, 3 * 22);
+ memset(body, 0, sizeof(body));
+
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(ibag); i++) {
+ put16le(body + i * 4, ibag[i]);
+ }
+
+ sfChunk(list, &listLen, "ibag", body, DVX_ARRAY_LEN(ibag) * 4);
+
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(igen); i++) {
+ put16le(body + i * 4, igen[i][0]);
+ put16le(body + i * 4 + 2, igen[i][1]);
+ }
+
+ sfChunk(list, &listLen, "igen", body, DVX_ARRAY_LEN(igen) * 4);
+
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(shdr); i++) {
+ for (int32_t k = 0; k < 4; k++) {
+ put32le(body + i * 16 + k * 4, shdr[i][k]);
+ }
+ }
+
+ sfChunk(list, &listLen, "shdr", body, DVX_ARRAY_LEN(shdr) * 16);
+ memcpy(out + len, "LIST", 4);
+ put32le(out + len + 4, listLen);
+ memcpy(out + len + 8, list, listLen);
+ len += 8 + listLen;
+
+ memcpy(out, "RIFF", 4);
+ put32le(out + 4, len - 8);
+ memcpy(out + 8, "sfbk", 4);
+ return len;
+}
+
+
+// SoundFont 1 (sf1Build): its samples (ROM and its own, the loops put
+// back), a region's values made SoundFont 2's (the instrument's first,
+// the preset's where it sets none, the tuning and the sample's pitch),
+// ROM samples where the chip has the ROM and left out elsewhere, and its
+// notes in the software synthesizer.
+static void sf1Tests(void) {
+ static uint8_t data[SF_TEST_MAX];
+ uint32_t len = sf1Build(data);
+ WaveBankT bank;
+ WaveCacheT cache;
+ AudioWtInfoT wt;
+ AudioCtxT *ctx;
+ MusicT *m = NULL;
+ uint8_t need[2];
+ uint32_t missing = 0;
+
+ tCase("sf1: the samples, ROM first");
+ TEST_EQ(sf2Parse(data, len, &bank), MUSIC_OK);
+ TEST_EQ(bank.sampleCount, 2);
+ TEST_EQ(bank.presetCount, 2);
+
+ if (bank.sampleCount != 2 || bank.presetCount != 2) {
+ waveBankFree(&bank);
+ return;
+ }
+
+ const WaveSampleT *rom = &bank.samples[0];
+ const WaveSampleT *ram = &bank.samples[1];
+
+ TEST_TRUE((rom->type & SF_SAMPLE_ROM) != 0);
+ TEST_EQ(rom->start, SF1_ROM_START);
+ TEST_EQ(rom->loopStart, SF1_ROM_LOOP);
+ TEST_EQ(rom->loopEnd, SF1_ROM_LOOP_END);
+ TEST_EQ(ram->type, SF_SAMPLE_MONO);
+ TEST_EQ(ram->end, SF1_RAM_END);
+ TEST_EQ(ram->loopStart, SF1_RAM_LOOP);
+ TEST_EQ(ram->loopEnd, SF1_RAM_LOOP_END);
+ TEST_EQ(ram->rate, 44100);
+ TEST_EQ(ram->rootKey, 60);
+
+ tCase("sf1: a region's values in SoundFont 2's units");
+ const WaveRegionT *r = &bank.regions[bank.presets[0].first];
+
+ TEST_EQ(bank.presets[0].count, 1);
+ TEST_EQ(r->sample, 1);
+ TEST_EQ(r->gen[SF_GEN_ROOT_KEY], 70);
+ TEST_EQ(r->gen[SF_GEN_SCALE_TUNING], 50);
+ TEST_EQ(r->gen[SF_GEN_COARSE_TUNE] * 100 + r->gen[SF_GEN_FINE_TUNE], 150);
+ TEST_EQ(r->gen[SF_GEN_VOL_ENV_SUSTAIN], 0);
+ TEST_EQ(r->gen[SF_GEN_VOL_ENV_ATTACK], -1200);
+ TEST_EQ(r->gen[SF_GEN_PAN], 500);
+ TEST_EQ(r->gen[SF_GEN_REVERB_SEND], 1000);
+ TEST_TRUE(r->gen[SF_GEN_ATTENUATION] >= SF1_ATTEN_64 - 1 && r->gen[SF_GEN_ATTENUATION] <= SF1_ATTEN_64 + 1);
+ TEST_EQ(r->gen[SF_GEN_MOD_ENV_SUSTAIN], 1000);
+ TEST_EQ(r->gen[SF_GEN_MOD_LFO_FREQ], -725);
+ TEST_EQ(r->gen[SF_GEN_VIB_LFO_FREQ], -15600);
+ TEST_EQ(r->gen[SF_GEN_VOL_ENV_DECAY], -12000);
+ TEST_EQ(r->gen[SF_GEN_SAMPLE_MODES], SF_LOOP_ON);
+ TEST_EQ(bank.regions[bank.presets[1].first].sample, 0);
+
+ tCase("sf1: ROM samples on a chip with the ROM, left out elsewhere");
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 1024\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_TRUE(ctx != NULL);
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ need[0] = 1;
+ need[1] = 0;
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW | AUDIO_WT_ROM, &missing), MUSIC_OK);
+ TEST_EQ(missing, 0);
+ TEST_EQ(cache.loaded[0].addr, AUDIO_WT_ADDR_ROM | SF1_ROM_START * 2);
+ TEST_EQ(cache.loaded[0].frames, SF1_ROM_END - SF1_ROM_START);
+ TEST_EQ(cache.loaded[0].rateCents, 0);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.freeMemory, 1024);
+ waveCacheFree(ctx, &cache);
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_ERR_NO_DEVICE);
+ TEST_EQ(missing, 1);
+ need[1] = 1;
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(missing, 1);
+ TEST_EQ(cache.loaded[0].addr, WAVE_NOT_LOADED);
+ TEST_TRUE(cache.loaded[1].addr != WAVE_NOT_LOADED);
+ waveCacheFree(ctx, &cache);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.freeMemory, 1024);
+ audioClose(ctx);
+ waveBankFree(&bank);
+
+ tCase("sf1: in the software synthesizer, ROM notes on FM");
+ TEST_EQ(platformMkdirRecursive(DVX_USER_DIR "/" DVX_BANKS_DIR), 0);
+ writeFile(SF1_TEST_FILE, data, len);
+ setIni("midi = softwave\nsoundFont = " SF1_TEST_NAME "\n");
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ TEST_EQ(musicMidiNoteOn(m, 0, 60, 127), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ TEST_TRUE(outPeak(RATE_DEV / 4) > 0);
+ TEST_EQ(musicMidiNoteOff(m, 0, 60), MUSIC_OK);
+ TEST_EQ(musicMidiProgram(m, 1, 1), MUSIC_OK);
+ TEST_EQ(musicMidiNoteOn(m, 1, 60, 127), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ TEST_TRUE(outPeak(RATE_DEV / 4) > 0);
+ musicFree(m);
+ remove(SF1_TEST_FILE);
+
+ tCase("sf1: the AWE's ROM bank where SOUND says, in place of the standard SoundFont");
+ char cwd[DVX_MAX_PATH];
+ char dir[DVX_MAX_PATH];
+ char path[DVX_MAX_PATH];
+ char want[DVX_MAX_PATH];
+
+ TEST_TRUE(getcwd(cwd, sizeof(cwd)) != NULL);
+ snprintf(dir, sizeof(dir), "%s/" SF1_SOUND_DIR, cwd);
+ remove(SF1_SOUND_DIR "/" MUSIC_ROM_BANK_DIR "/SYNTHGM.SBK");
+ remove(SF1_SOUND_DIR "/" MUSIC_ROM_BANK_DIR "/SYNTHGM.SF2");
+ unsetenv(MUSIC_ROM_BANK_ENV);
+ TEST_TRUE(!musicRomBankPath(path, sizeof(path)));
+ TEST_STR_EQ(MusicRomBank(), "");
+ setenv(MUSIC_ROM_BANK_ENV, dir, 1);
+ TEST_TRUE(!musicRomBankPath(path, sizeof(path)));
+ TEST_EQ(platformMkdirRecursive(SF1_SOUND_DIR "/" MUSIC_ROM_BANK_DIR), 0);
+ writeFile(SF1_SOUND_DIR "/" MUSIC_ROM_BANK_DIR "/SYNTHGM.SBK", data, len);
+ snprintf(want, sizeof(want), "%s/" MUSIC_ROM_BANK_DIR "/SYNTHGM.SBK", dir);
+ TEST_TRUE(musicRomBankPath(path, sizeof(path)));
+ TEST_STR_EQ(path, want);
+ TEST_STR_EQ(MusicRomBank(), want);
+ TEST_TRUE(!platformKindFind(path, sizeof(path), DvxKindBanksE, MUSIC_SOUND_FONT_DEFAULT));
+ setIni("midi = softwave\n");
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ TEST_EQ(musicMidiNoteOn(m, 0, 60, 127), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ TEST_TRUE(outPeak(RATE_DEV / 4) > 0);
+ musicFree(m);
+
+ // An SF2 one first
+ writeFile(SF1_SOUND_DIR "/" MUSIC_ROM_BANK_DIR "/SYNTHGM.SF2", data, len);
+ snprintf(want, sizeof(want), "%s/" MUSIC_ROM_BANK_DIR "/SYNTHGM.SF2", dir);
+ TEST_TRUE(musicRomBankPath(path, sizeof(path)));
+ TEST_STR_EQ(path, want);
+ unsetenv(MUSIC_ROM_BANK_ENV);
+}
+
+
+// A SoundFont of two samples ("Ramp": frames 0-99, a loop 10-89;
+// "Hit": 100-199, unpitched), two instruments ("Keys": a global zone of
+// attenuation 100, Ramp below key 60 looping, Hit from 60; "Kit": Hit on
+// keys 35-81 rooted at 60), and three presets ("Piano" 0:0 over Keys, a
+// global zone of pan 100, its zone fine tune 10 and attenuation 2000;
+// "Narrow" 0:1 over Keys on keys 50-70; "Drums" 128:0 over Kit).
+static uint32_t sfBuild(const SfSpecT *spec, uint8_t *out) {
+ static const uint16_t igen[][2] = {
+ { 48, 100 }, { 43, 59 << 8 }, { 54, 1 }, { 53, 0 }, { 43, 60 | 127 << 8 }, { 53, 1 }, { 43, 35 | 81 << 8 }, { 58, 60 }, { 53, 1 }, { 0, 0 }
+ };
+ static const uint16_t ibag[] = { 0, 1, 4, 6, 9 };
+ static const uint16_t pbag[] = { 0, 1, 4, 6, 7 };
+ uint16_t pgen[][2] = {
+ { 17, 100 }, { 52, 10 }, { 48, 2000 }, { 41, spec->instrument }, { 43, 50 | 70 << 8 }, { 41, 0 }, { 41, 1 }, { 0, 0 }
+ };
+ static uint8_t body[SF_TEST_MAX];
+ static uint8_t list[SF_TEST_MAX];
+ uint32_t len = 0;
+ uint32_t listLen = 0;
+ uint32_t n;
+
+ // INFO: the version
+ memcpy(list, "INFO", 4);
+ listLen = 4;
+ put16le(body, spec->version);
+ put16le(body + 2, 1);
+ sfChunk(list, &listLen, "ifil", body, 4);
+ memcpy(out + 12, "LIST", 4);
+ put32le(out + 16, listLen);
+ memcpy(out + 20, list, listLen);
+ len = 20 + listLen;
+
+ // sdta: a ramp, then a different ramp
+ memcpy(list, "sdta", 4);
+ listLen = 4;
+
+ for (int32_t i = 0; i < SF_TEST_FRAMES; i++) {
+ put16le(body + i * 2, (uint32_t)(uint16_t)(int16_t)(i < 100 ? i * 100 : -i * 50));
+ }
+
+ sfChunk(list, &listLen, "smpl", body, SF_TEST_FRAMES * 2);
+ memcpy(out + len, "LIST", 4);
+ put32le(out + len + 4, listLen);
+ memcpy(out + len + 8, list, listLen);
+ len += 8 + listLen;
+
+ // pdta: the hydra
+ memcpy(list, "pdta", 4);
+ listLen = 4;
+ memset(body, 0, sizeof(body));
+ n = 0;
+ memcpy(body + n, "Piano", 5);
+ put16le(body + n + 20, 0);
+ put16le(body + n + 22, 0);
+ put16le(body + n + 24, 0);
+ n += 38;
+ memcpy(body + n, "Narrow", 6);
+ put16le(body + n + 20, 1);
+ put16le(body + n + 22, 0);
+ put16le(body + n + 24, 2);
+ n += 38;
+ memcpy(body + n, "Drums", 5);
+ put16le(body + n + 20, 0);
+ put16le(body + n + 22, 128);
+ put16le(body + n + 24, 3);
+ n += 38;
+ memcpy(body + n, "EOP", 3);
+ put16le(body + n + 24, 4);
+ n += 38;
+ sfChunk(list, &listLen, "phdr", body, n);
+ memset(body, 0, sizeof(body));
+
+ for (int32_t i = 0; i < 5; i++) {
+ put16le(body + i * 4, pbag[i]);
+ }
+
+ sfChunk(list, &listLen, "pbag", body, 5 * 4);
+ memset(body, 0, sizeof(body));
+ sfChunk(list, &listLen, "pmod", body, 10);
+
+ for (int32_t i = 0; i < 8; i++) {
+ put16le(body + i * 4, pgen[i][0]);
+ put16le(body + i * 4 + 2, pgen[i][1]);
+ }
+
+ sfChunk(list, &listLen, "pgen", body, 8 * 4);
+ memset(body, 0, sizeof(body));
+ memcpy(body, "Keys", 4);
+ put16le(body + 20, 0);
+ memcpy(body + 22, "Kit", 3);
+ put16le(body + 42, 3);
+ memcpy(body + 44, "EOI", 3);
+ put16le(body + 64, 4);
+ sfChunk(list, &listLen, "inst", body, 3 * 22);
+ memset(body, 0, sizeof(body));
+
+ for (int32_t i = 0; i < 5; i++) {
+ put16le(body + i * 4, ibag[i]);
+ }
+
+ sfChunk(list, &listLen, "ibag", body, 5 * 4);
+ memset(body, 0, sizeof(body));
+ sfChunk(list, &listLen, "imod", body, 10);
+
+ for (int32_t i = 0; i < 10; i++) {
+ put16le(body + i * 4, igen[i][0]);
+ put16le(body + i * 4 + 2, igen[i][1]);
+ }
+
+ sfChunk(list, &listLen, "igen", body, 10 * 4);
+ memset(body, 0, sizeof(body));
+ memcpy(body, "Ramp", 4);
+ put32le(body + 20, 0);
+ put32le(body + 24, 100);
+ put32le(body + 28, 10);
+ put32le(body + 32, 90);
+ put32le(body + 36, 22050);
+ body[40] = 60;
+ put16le(body + 44, 1);
+ memcpy(body + 46, "Hit", 3);
+ put32le(body + 66, 100);
+ put32le(body + 70, spec->hitEnd);
+ put32le(body + 74, 100);
+ put32le(body + 78, 200);
+ put32le(body + 82, 11025);
+ body[86] = 255;
+ body[87] = (uint8_t)(int8_t)-5;
+ put16le(body + 90, 1);
+ memcpy(body + 92, "EOS", 3);
+ sfChunk(list, &listLen, "shdr", body, 3 * 46);
+ memcpy(out + len, "LIST", 4);
+ put32le(out + len + 4, listLen);
+ memcpy(out + len + 8, list, listLen);
+ len += 8 + listLen;
+
+ memcpy(out, "RIFF", 4);
+ put32le(out + 4, len - 8);
+ memcpy(out + 8, "sfbk", 4);
+ return len;
+}
+
+
+// A chunk (id, length, body, a pad byte when odd) at the end of out.
+static void sfChunk(uint8_t *out, uint32_t *len, const char *id, const uint8_t *body, uint32_t bodyLen) {
+ memcpy(out + *len, id, 4);
+ put32le(out + *len + 4, bodyLen);
+ memcpy(out + *len + 8, body, bodyLen);
+ *len += 8 + bodyLen;
+
+ if (bodyLen & 1) {
+ out[(*len)++] = 0;
+ }
+}
+
+
+static void sfTests(void) {
+ static uint8_t data[SF_TEST_MAX];
+ static uint8_t bad[SF_TEST_MAX];
+ SfSpecT spec = { 2, SF_TEST_FRAMES, 0 };
+ WaveBankT bank;
+ uint32_t len = sfBuild(&spec, data);
+ int16_t frames[3];
+
+ tCase("sf2: presets, zones, and samples");
+ TEST_EQ(sf2Parse(data, len, &bank), MUSIC_OK);
+ TEST_EQ(bank.presetCount, 3);
+ TEST_EQ(bank.sampleCount, 2);
+ TEST_EQ(bank.dataFrames, SF_TEST_FRAMES);
+ TEST_STR_EQ(bank.presets[0].name, "Piano");
+ TEST_EQ(bank.samples[0].loopStart, 10);
+ TEST_EQ(bank.samples[0].loopEnd, 90);
+ TEST_EQ(bank.samples[0].rate, 22050);
+ TEST_EQ(bank.samples[1].rootKey, 60);
+ TEST_EQ(bank.samples[1].correction, -5);
+
+ // Piano: the instrument's global zone under each zone, the preset's
+ // values added (attenuation clamped at 1440), its global zone's pan
+ const WavePresetT *piano = waveBankPreset(&bank, 0, 0);
+
+ TEST_TRUE(piano == &bank.presets[0]);
+ TEST_EQ(piano->count, 2);
+
+ const WaveRegionT *low = &bank.regions[piano->first];
+ const WaveRegionT *high = &bank.regions[piano->first + 1];
+
+ TEST_EQ(low->keyLo, 0);
+ TEST_EQ(low->keyHi, 59);
+ TEST_EQ(low->sample, 0);
+ TEST_EQ(low->gen[SF_GEN_ATTENUATION], 1440);
+ TEST_EQ(low->gen[SF_GEN_PAN], 100);
+ TEST_EQ(low->gen[SF_GEN_FINE_TUNE], 10);
+ TEST_EQ(low->gen[SF_GEN_SAMPLE_MODES], SF_LOOP_ON);
+ TEST_EQ(low->gen[SF_GEN_SCALE_TUNING], 100);
+ TEST_EQ(low->gen[SF_GEN_FILTER_FC], 13500);
+ TEST_EQ(low->gen[SF_GEN_VOL_ENV_ATTACK], -12000);
+ TEST_EQ(low->gen[SF_GEN_ROOT_KEY], SF_ROOT_NONE);
+ TEST_EQ(high->keyLo, 60);
+ TEST_EQ(high->keyHi, 127);
+ TEST_EQ(high->sample, 1);
+ TEST_EQ(high->gen[SF_GEN_SAMPLE_MODES], SF_LOOP_NONE);
+
+ // Narrow: the preset's key range cuts both zones
+ const WavePresetT *narrow = waveBankPreset(&bank, 0, 1);
+
+ TEST_EQ(narrow->count, 2);
+ TEST_EQ(bank.regions[narrow->first].keyLo, 50);
+ TEST_EQ(bank.regions[narrow->first].keyHi, 59);
+ TEST_EQ(bank.regions[narrow->first + 1].keyLo, 60);
+ TEST_EQ(bank.regions[narrow->first + 1].keyHi, 70);
+ TEST_EQ(bank.regions[narrow->first].gen[SF_GEN_ATTENUATION], 100);
+ TEST_EQ(bank.regions[narrow->first].gen[SF_GEN_PAN], 0);
+
+ // Drums and the fallbacks
+ const WavePresetT *drums = waveBankPreset(&bank, SF_DRUM_BANK, 0);
+
+ TEST_TRUE(drums == &bank.presets[2]);
+ TEST_EQ(bank.regions[drums->first].keyLo, 35);
+ TEST_EQ(bank.regions[drums->first].keyHi, 81);
+ TEST_EQ(bank.regions[drums->first].gen[SF_GEN_ROOT_KEY], 60);
+ TEST_TRUE(waveBankPreset(&bank, SF_DRUM_BANK, 25) == drums);
+ TEST_TRUE(waveBankPreset(&bank, 1, 0) == piano);
+ TEST_TRUE(waveBankPreset(&bank, 0, 99) == NULL);
+
+ TEST_TRUE(waveBankRead(&bank, 1, 3, frames));
+ TEST_EQ(frames[0], 100);
+ TEST_EQ(frames[2], 300);
+ TEST_TRUE(!waveBankRead(&bank, SF_TEST_FRAMES - 1, 2, frames));
+ waveBankFree(&bank);
+
+ tCase("sf2: a bank in a file");
+ FILE *f = fopen("test.sf2", "wb");
+
+ TEST_TRUE(f && fwrite(data, 1, len, f) == len);
+
+ if (f) {
+ fclose(f);
+ }
+
+ TEST_EQ(sf2Load("test.sf2", &bank), MUSIC_OK);
+ TEST_EQ(bank.presetCount, 3);
+ TEST_TRUE(waveBankRead(&bank, 101, 1, frames));
+ TEST_EQ(frames[0], -101 * 50);
+ waveBankFree(&bank);
+ TEST_EQ(sf2Load("missing.sf2", &bank), MUSIC_ERR_OPEN);
+
+ tCase("sf2: damaged banks are refused");
+ memcpy(bad, data, len);
+ bad[3] = 'X';
+ TEST_EQ(sf2Parse(bad, len, &bank), MUSIC_ERR_BANK);
+ TEST_EQ(sf2Parse(data, len - 1, &bank), MUSIC_ERR_BANK);
+ TEST_EQ(sf2Parse(NULL, 0, &bank), MUSIC_ERR_BANK);
+ spec.version = 3;
+ len = sfBuild(&spec, bad);
+ TEST_EQ(sf2Parse(bad, len, &bank), MUSIC_ERR_BANK);
+ spec.version = 2;
+ spec.hitEnd = SF_TEST_FRAMES + 1;
+ len = sfBuild(&spec, bad);
+ TEST_EQ(sf2Parse(bad, len, &bank), MUSIC_ERR_BANK);
+ spec.hitEnd = SF_TEST_FRAMES;
+ spec.instrument = 7;
+ len = sfBuild(&spec, bad);
+ TEST_EQ(sf2Parse(bad, len, &bank), MUSIC_ERR_BANK);
+ TEST_TRUE(bank.presets == NULL && bank.regions == NULL);
+}
+
+
static void smfTests(void) {
static uint8_t smf[BUF_MAX];
static uint8_t cut[BUF_MAX];
@@ -2061,13 +3743,834 @@ static void smfTests(void) {
}
-static void writeFile(const char *path, const void *data, uint32_t size) {
- FILE *f = fopen(path, "wb");
- if (f) {
- fwrite(data, 1, size, f);
- fclose(f);
+// MIDI on the SoundFont software synthesizer (DVX.INI midi = softwave),
+// mixed into the music's stream.
+static void softwaveTests(void) {
+ static uint8_t data[SF_TEST_MAX];
+ static uint8_t flute[BUF_MAX];
+ SfSpecT spec = { 2, SF_TEST_FRAMES, 0 };
+ MusicInfoT info;
+ MusicT *m = NULL;
+ uint32_t fluteLen = fluteSmf(flute);
+
+ TEST_EQ(platformMkdirRecursive(DVX_USER_DIR "/" DVX_BANKS_DIR), 0);
+ writeFile(SF_TEST_FILE, data, sfBuild(&spec, data));
+
+ tCase("softwave: a live note at its pitch");
+ setIni("midi = softwave\nsoundFont = " SF_TEST_NAME "\n");
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_SOFTWAVE);
+ TEST_STR_EQ(info.output, "SoundFont software synthesizer");
+ TEST_EQ(musicMidiNoteOn(m, 0, SOFT_KEY, 127), MUSIC_OK);
+ pump(RATE_DEV / 2);
+ TEST_TRUE(outPeak(RATE_DEV / 2) > 0);
+
+ double tone = outTone(RATE_DEV / 10, RATE_DEV / 4, SOFT_HZ);
+
+ TEST_TRUE(tone > 4.0 * outTone(RATE_DEV / 10, RATE_DEV / 4, SOFT_HZ * 0.75));
+ TEST_TRUE(tone > 4.0 * outTone(RATE_DEV / 10, RATE_DEV / 4, SOFT_HZ * 1.25));
+
+ tCase("softwave: a program's samples loaded as it is chosen");
+ TEST_EQ(musicMidiProgram(m, 0, 1), MUSIC_OK);
+ TEST_EQ(musicMidiNoteOn(m, 0, 55, 127), MUSIC_OK);
+ pump(RATE_DEV / 4);
+ TEST_TRUE(outPeak(RATE_DEV / 4) > 0);
+ musicFree(m);
+
+ tCase("softwave: a note the bank lacks on the FM software synthesizer");
+ TEST_EQ(musicLoadMem(flute, fluteLen, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_SOFTWAVE);
+ pump(RATE_DEV / 4);
+
+ // The flute's A4 (FM) stands out
+ double a4 = outTone(RATE_DEV / 20, RATE_DEV / 8, 440.0);
+
+ TEST_TRUE(a4 > 4.0 * outTone(RATE_DEV / 20, RATE_DEV / 8, 330.0));
+ musicFree(m);
+ remove(SF_TEST_FILE);
+}
+
+
+// A position heard within SOUND_POS_TOL_MS of want.
+static bool soundNear(uint32_t ms, uint32_t want) {
+ return ms + SOUND_POS_TOL_MS >= want && ms <= want + SOUND_POS_TOL_MS;
+}
+
+
+// WAV and VOC files as music: a WAV played from the disk (its tone, the
+// position heard, pause, resume, seek, the end, again, stop, loop), 8-bit
+// stereo from memory, a VOC, damaged, cut-short, and compressed WAVs; a
+// MIDI file's title; a module's pause on a stream heard at once.
+static void soundTests(void) {
+ static uint8_t wav[SOUND_BYTES_MAX];
+ static uint8_t mod[BUF_MAX];
+ static uint8_t smf[BUF_MAX];
+ MusicInfoT info;
+ MusicT *m = NULL;
+ uint32_t size = wavBuild(wav, 16, 1, SOUND_FRAMES, WAV_TAG_PCM);
+ uint32_t pos;
+
+ tCase("sound: a WAV file played from the disk");
+ setIni("");
+ writeFile("TONE.WAV", wav, size);
+ TEST_EQ(musicLoad("TONE.WAV", &m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.kind, MUSIC_KIND_SOUND);
+ TEST_EQ(info.where, MUSIC_OUT_NONE);
+ TEST_EQ(info.lengthMs, SOUND_MS);
+ TEST_STR_EQ(info.title, "");
+ TEST_EQ(musicMidiNoteOn(m, 0, 60, 100), MUSIC_ERR_ARG);
+ musicSetEndFn(m, onEnd, NULL);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_STREAM);
+ TEST_STR_EQ(info.output, "Sound device");
+ pump(SOUND_STEP);
+ TEST_TRUE(outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_HZ_FIRST) > SOUND_TONE_RATIO * outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_HZ_SECOND));
+ TEST_TRUE(soundNear(musicGetPosition(m), SOUND_STEP_MS));
+
+ // Paused after the mixer took frames the fill has not replaced yet: the
+ // room is not filled with silence while paused.
+ tCase("sound: paused where it is, resumed, sought");
+ audioMixRender(sOut, RENDER_CHUNK);
+ TEST_EQ(musicPause(m), MUSIC_OK);
+ pos = musicGetPosition(m);
+ pump(SOUND_STEP);
+ TEST_EQ(outPeak(SOUND_STEP), 0);
+ TEST_EQ(musicGetPosition(m), pos);
+ TEST_EQ(musicResume(m), MUSIC_OK);
+ pump(SOUND_SHORT);
+ TEST_TRUE(outPeak(SOUND_HELD) > 0);
+ TEST_TRUE(soundNear(musicGetPosition(m), pos + SOUND_SHORT_MS));
+
+ // The second half's tone at once: what the stream held is dropped.
+ TEST_EQ(musicSetPosition(m, SOUND_SEEK_MS), MUSIC_OK);
+ TEST_EQ(musicGetPosition(m), SOUND_SEEK_MS);
+ pump(SOUND_SHORT);
+ TEST_TRUE(outTone(SOUND_HELD, SOUND_SHORT - SOUND_HELD, SOUND_HZ_SECOND) > SOUND_TONE_RATIO * outTone(SOUND_HELD, SOUND_SHORT - SOUND_HELD, SOUND_HZ_FIRST));
+ TEST_TRUE(soundNear(musicGetPosition(m), SOUND_SEEK_MS + SOUND_SHORT_MS));
+
+ tCase("sound: the end, played again, stopped, looped");
+ sEnds = 0;
+ pump(SOUND_STEP);
+ TEST_EQ(sEnds, 1);
+ TEST_TRUE(!musicIsPlaying(m));
+ TEST_EQ(musicGetPosition(m), SOUND_MS);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ TEST_EQ(musicGetPosition(m), 0);
+ pump(SOUND_SHORT);
+ TEST_TRUE(outTone(SOUND_HELD, SOUND_SHORT - SOUND_HELD, SOUND_HZ_FIRST) > SOUND_TONE_RATIO * outTone(SOUND_HELD, SOUND_SHORT - SOUND_HELD, SOUND_HZ_SECOND));
+ TEST_TRUE(soundNear(musicGetPosition(m), SOUND_SHORT_MS));
+ TEST_EQ(musicStop(m), MUSIC_OK);
+ TEST_EQ(musicGetPosition(m), 0);
+ pump(SOUND_SHORT);
+ TEST_EQ(outPeakFrom(SOUND_HELD, SOUND_SHORT - SOUND_HELD), 0);
+ TEST_EQ(musicPlay(m, true), MUSIC_OK);
+ TEST_EQ(musicSetPosition(m, SOUND_LOOP_MS), MUSIC_OK);
+ pump(SOUND_STEP);
+ TEST_TRUE(musicIsPlaying(m));
+ TEST_EQ(sEnds, 1);
+ TEST_TRUE(soundNear(musicGetPosition(m), SOUND_LOOP_MS + SOUND_STEP_MS - SOUND_MS));
+ musicFree(m);
+
+ tCase("sound: 8-bit stereo from memory");
+ size = wavBuild(wav, 8, 2, SOUND_FRAMES, WAV_TAG_PCM);
+ TEST_EQ(musicLoadMem(wav, size, &m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.kind, MUSIC_KIND_SOUND);
+ TEST_EQ(info.lengthMs, SOUND_MS);
+ musicPlay(m, false);
+ pump(SOUND_STEP);
+ TEST_TRUE(outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_HZ_FIRST) > SOUND_TONE_RATIO * outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_HZ_SECOND));
+
+ int32_t right = 0;
+
+ for (uint32_t i = 0; i < SOUND_STEP; i++) {
+ right = abs(sOut[i * 2 + 1]) > right ? abs(sOut[i * 2 + 1]) : right;
}
+
+ TEST_EQ(right, 0);
+ musicFree(m);
+
+ tCase("sound: a VOC file");
+ size = vocBuild(wav, SOUND_VOC_FRAMES);
+ writeFile("TONE.VOC", wav, size);
+ TEST_EQ(musicLoad("TONE.VOC", &m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.kind, MUSIC_KIND_SOUND);
+ TEST_EQ(info.lengthMs, SOUND_MS);
+ musicPlay(m, false);
+ pump(SOUND_STEP);
+ TEST_TRUE(outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_VOC_HZ_FIRST) > SOUND_TONE_RATIO * outTone(SOUND_HELD, SOUND_STEP - SOUND_HELD, SOUND_VOC_HZ_SECOND));
+ musicFree(m);
+
+ tCase("sound: damaged, cut-short, and compressed WAV files");
+ size = wavBuild(wav, 16, 1, SOUND_FRAMES, WAV_TAG_PCM);
+ writeFile("CUT.WAV", wav, SOUND_CUT_BYTES);
+ TEST_EQ(musicLoad("CUT.WAV", &m), MUSIC_ERR_CORRUPT);
+ TEST_TRUE(m == NULL);
+ TEST_EQ(musicLoadMem(wav, SOUND_CUT_BYTES, &m), MUSIC_ERR_CORRUPT);
+ TEST_EQ(musicLoad("NOSUCH.WAV", &m), MUSIC_ERR_OPEN);
+ writeFile("SHORT.WAV", wav, WAV_HEADER_BYTES + SOUND_RATE * SOUND_CUT_MS / MS_SECOND * sizeof(int16_t));
+ TEST_EQ(musicLoad("SHORT.WAV", &m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.lengthMs, SOUND_CUT_MS);
+ musicSetEndFn(m, onEnd, NULL);
+ sEnds = 0;
+ musicPlay(m, false);
+ pump(RATE_DEV);
+ TEST_EQ(sEnds, 1);
+ TEST_EQ(musicGetPosition(m), SOUND_CUT_MS);
+ musicFree(m);
+ size = wavBuild(wav, 16, 1, SOUND_FRAMES, WAV_TAG_ADPCM);
+ writeFile("ADPCM.WAV", wav, size);
+ TEST_EQ(musicLoad("ADPCM.WAV", &m), MUSIC_ERR_UNSUPPORTED);
+ TEST_EQ(musicLoadMem(wav, size, &m), MUSIC_ERR_UNSUPPORTED);
+ TEST_STR_EQ(musicErrorText(MUSIC_ERR_UNSUPPORTED), audioErrorText(AUDIO_ERR_UNSUPPORTED));
+ TEST_STR_EQ(musicErrorText(-MUSIC_ERR_COUNT), "Unknown error");
+
+ tCase("sound: a MIDI file's title is its first track's name");
+ {
+ const uint8_t named[] = { 0x00, 0xFF, 0x03, 0x08, 'D', 'V', 'X', '\t', 'T', 'u', 'n', 'e', 0x00, 0xFF, 0x2F, 0x00 };
+ const uint8_t other[] = { 0x00, 0xFF, 0x03, 0x05, 'O', 't', 'h', 'e', 'r', 0x00, 0xFF, 0x2F, 0x00 };
+ const uint8_t *tracks[] = { named, other };
+ uint32_t lens[] = { sizeof(named), sizeof(other) };
+
+ size = buildSmf(smf, 1, 96, tracks, lens, 2);
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_STR_EQ(info.title, "DVX Tune");
+ musicFree(m);
+ size = buildSmf(smf, 1, 96, tracks + 1, lens + 1, 1);
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_STR_EQ(info.title, "Other");
+ musicFree(m);
+ }
+
+ tCase("sound: a module paused on a stream is silent at once");
+ size = makeMod(mod);
+ TEST_EQ(musicLoadMem(mod, size, &m), MUSIC_OK);
+ musicPlay(m, false);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_STREAM);
+ pump(SOUND_STEP);
+ TEST_TRUE(outPeak(SOUND_STEP) > 0);
+ musicPause(m);
+ pump(SOUND_SHORT);
+ TEST_EQ(outPeak(SOUND_SHORT), 0);
+ musicResume(m);
+ pump(SOUND_SHORT);
+ TEST_TRUE(outPeak(SOUND_SHORT) > 0);
+ musicFree(m);
+}
+
+
+// SoundFont samples in the fake wavetable driver's memory, through
+// audio.lib: what a song needs, each quality in turn, what is left out,
+// what stays, and what makes room.
+static void waveLoadTests(void) {
+ static uint8_t data[SF_TEST_MAX];
+ static uint8_t smf[BUF_MAX];
+ static const uint8_t keys[] = { 0x00, 0xC0, 0, 0x00, 0x90, 40, 100, 0x60, 0x80, 40, 0, 0x00, 0xFF, 0x2F, 0x00 };
+ static const uint8_t both[] = { 0x00, 0xC0, 0, 0x00, 0x90, 40, 100, 0x00, 0x99, 40, 100, 0x60, 0x80, 40, 0, 0x00, 0xFF, 0x2F, 0x00 };
+ static const uint8_t none[] = { 0x00, 0xC0, 1, 0x00, 0x90, 40, 100, 0x60, 0x80, 40, 0, 0x00, 0xFF, 0x2F, 0x00 };
+ const uint8_t *tracks[1];
+ uint32_t lens[1];
+ SfSpecT spec = { 2, SF_TEST_FRAMES, 0 };
+ WaveBankT bank;
+ WaveCacheT cache;
+ MidiFileT f;
+ AudioWtInfoT wt;
+ AudioCtxT *ctx;
+ uint8_t need[2];
+ uint32_t missing = 0;
+ uint32_t addr;
+
+ TEST_EQ(sf2Parse(data, sfBuild(&spec, data), &bank), MUSIC_OK);
+
+ tCase("waveload: the samples a song plays");
+ tracks[0] = keys;
+ lens[0] = sizeof(keys);
+ TEST_EQ(midiParse(smf, buildSmf(smf, 0, 96, tracks, lens, 1), &f), MUSIC_OK);
+ waveNeedsFile(&bank, &f, need);
+ TEST_EQ(need[0], 1);
+ TEST_EQ(need[1], 0);
+ midiFileFree(&f);
+ tracks[0] = both;
+ lens[0] = sizeof(both);
+ TEST_EQ(midiParse(smf, buildSmf(smf, 0, 96, tracks, lens, 1), &f), MUSIC_OK);
+ waveNeedsFile(&bank, &f, need);
+ TEST_EQ(need[0], 1);
+ TEST_EQ(need[1], 1);
+ midiFileFree(&f);
+
+ // Narrow holds no key 40
+ tracks[0] = none;
+ lens[0] = sizeof(none);
+ TEST_EQ(midiParse(smf, buildSmf(smf, 0, 96, tracks, lens, 1), &f), MUSIC_OK);
+ waveNeedsFile(&bank, &f, need);
+ TEST_EQ(need[0] + need[1], 0);
+ midiFileFree(&f);
+
+ // A preset at every key: how many it adds
+ TEST_EQ(waveNeedsPreset(&bank, 0, 0, need), 2);
+ TEST_EQ(waveNeedsPreset(&bank, SF_DRUM_BANK, 0, need), 0);
+ TEST_EQ(waveNeedsPreset(&bank, 0, 99, need), 0);
+
+ tCase("waveload: full quality when they fit");
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 1024\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_TRUE(ctx != NULL);
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ need[0] = 1;
+ need[1] = 1;
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(missing, 0);
+ TEST_EQ(cache.loaded[0].bits, 16);
+ TEST_EQ(cache.loaded[0].shift, 0);
+ TEST_EQ(cache.loaded[0].frames, WT_RAMP_FRAMES);
+ TEST_EQ(cache.loaded[0].rateCents, WT_RAMP_CENTS);
+ TEST_EQ(cache.loaded[1].frames, WT_HIT_FRAMES);
+ TEST_EQ(cache.loaded[1].rateCents, WT_HIT_CENTS);
+
+ // Ramp's third frame (200) little-endian; its guard frames are Hit's
+ // first (-5000)
+ addr = cache.loaded[0].addr;
+ TEST_EQ(sWtMemory[addr + 4], 200);
+ TEST_EQ(sWtMemory[addr + 5], 0);
+ TEST_EQ((int16_t)(sWtMemory[addr + 200] | sWtMemory[addr + 201] << 8), -5000);
+
+ // Given back with the cache
+ waveCacheFree(ctx, &cache);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.freeMemory, 1024);
+ audioClose(ctx);
+
+ tCase("waveload: 8-bit when 16-bit does not fit");
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 300\nfakeWtFlags = 19\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW | AUDIO_WT_8BIT, &missing), MUSIC_OK);
+ TEST_EQ(cache.loaded[0].bits, 8);
+ TEST_EQ(cache.loaded[1].bits, 8);
+ TEST_EQ(cache.loaded[0].shift, 0);
+
+ // Frame 99 (9900) as its top byte
+ TEST_EQ(sWtMemory[cache.loaded[0].addr + 99], 9900 >> 8);
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+
+ tCase("waveload: half the rate, then a quarter");
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 300\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(cache.loaded[0].bits, 16);
+ TEST_EQ(cache.loaded[0].shift, 1);
+ TEST_EQ(cache.loaded[0].frames, WT_RAMP_FRAMES / 2);
+ TEST_EQ(cache.loaded[0].rateCents, WT_RAMP_CENTS - 1200);
+ TEST_EQ(cache.loaded[1].rateCents, WT_HIT_CENTS - 1200);
+
+ // Frame 1 is frames 2 and 3 averaged: 250
+ TEST_EQ(sWtMemory[cache.loaded[0].addr + 2], 250);
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 130\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(cache.loaded[0].shift, 2);
+ TEST_EQ(cache.loaded[1].shift, 2);
+ TEST_EQ(cache.loaded[0].rateCents, WT_RAMP_CENTS - 2400);
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+
+ tCase("waveload: what still does not fit is left out");
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 64\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(missing, 1);
+ TEST_TRUE(cache.loaded[0].addr != WAVE_NOT_LOADED);
+ TEST_EQ(cache.loaded[1].addr, WAVE_NOT_LOADED);
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 32\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_ERR_NO_DEVICE);
+ TEST_EQ(missing, 2);
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+
+ tCase("waveload: what a song loads stays while it fits");
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 1024\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ need[1] = 0;
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ addr = cache.loaded[0].addr;
+ need[0] = 0;
+ need[1] = 1;
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(cache.loaded[0].addr, addr);
+ TEST_TRUE(cache.loaded[1].addr != WAVE_NOT_LOADED);
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+
+ tCase("waveload: samples a song does not need make room");
+ setIni("fakeWtVoices = 4\nfakeWtMemory = 230\nfakeWtFlags = 3\n");
+ ctx = audioOpen();
+ TEST_EQ(waveCacheInit(&cache, &bank), MUSIC_OK);
+ need[0] = 1;
+ need[1] = 0;
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_TRUE(cache.loaded[0].addr != WAVE_NOT_LOADED);
+ need[0] = 0;
+ need[1] = 1;
+ TEST_EQ(waveLoadNeeded(ctx, &bank, &cache, need, WT_FLAGS_HW, &missing), MUSIC_OK);
+ TEST_EQ(cache.loaded[0].addr, WAVE_NOT_LOADED);
+ TEST_EQ(cache.loaded[1].shift, 0);
+ waveCacheFree(ctx, &cache);
+ audioClose(ctx);
+ waveBankFree(&bank);
+}
+
+
+// Whole musics on the fake wavetable synthesizer (fakeWtVoices) with the
+// test SoundFont in the user's banks folder.
+static void wavePlayTests(void) {
+ static uint8_t data[SF_TEST_MAX];
+ static uint8_t smf[BUF_MAX];
+ static uint8_t flute[BUF_MAX];
+ static const uint8_t track[] = { 0x00, 0xC0, 0, 0x00, 0x90, 40, 127, 0x60, 0x80, 40, 0, 0x00, 0xFF, 0x2F, 0x00 };
+ const uint8_t *const tracks[] = { track };
+ const uint32_t lens[] = { sizeof(track) };
+ SfSpecT spec = { 2, SF_TEST_FRAMES, 0 };
+ MusicInfoT info;
+ AudioWtInfoT wt;
+ MusicT *m = NULL;
+ MusicT *m2 = NULL;
+ MusicT *m3 = NULL;
+ uint32_t size = buildSmf(smf, 0, 96, tracks, lens, 1);
+ uint32_t fluteLen = fluteSmf(flute);
+ int32_t starts;
+ int32_t uploads;
+
+ TEST_EQ(platformMkdirRecursive(DVX_USER_DIR "/" DVX_BANKS_DIR), 0);
+ writeFile(SF_TEST_FILE, data, sfBuild(&spec, data));
+
+ tCase("waveplay: MIDI on the wavetable synthesizer, played by the timer");
+ setIni("fakeWtVoices = 8\nsoundFont = " SF_TEST_NAME "\n");
+ sWtStarts = 0;
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ musicSetEndFn(m, onEnd, NULL);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ TEST_STR_EQ(info.output, "Fake wavetable (8 voices)");
+ hostClockAdvance(10);
+ TEST_EQ(sWtStarts, 1);
+ TEST_EQ(sWtVoice[sWtLast].pitch, WT_PIANO_PITCH);
+ TEST_EQ(sWtVoice[sWtLast].attenuation, WT_PIANO_ATTEN);
+ TEST_EQ(sWtVoice[sWtLast].pan, 100);
+
+ // Only the song's sample (Ramp) is in the synthesizer
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.memory - wt.freeMemory, WT_RAMP_BYTES);
+
+ // The music's volume reaches the voice
+ TEST_EQ(musicSetVolume(m, FULL / 2), MUSIC_OK);
+ TEST_TRUE(sWtVoice[sWtLast].attenuation > WT_PIANO_ATTEN);
+ TEST_EQ(musicSetVolume(m, FULL), MUSIC_OK);
+ sEnds = 0;
+ hostClockAdvance(500);
+ TEST_TRUE(sWtReleased & (1u << sWtLast));
+ musicIdle(NULL);
+ TEST_EQ(sEnds, 1);
+
+ tCase("waveplay: the samples stay for the next music a while");
+ musicFree(m);
+ audioWtGetInfo(&wt);
+ TEST_TRUE(wt.present);
+ TEST_EQ(wt.memory - wt.freeMemory, WT_RAMP_BYTES);
+ uploads = sWtUploads;
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ TEST_EQ(sWtUploads, uploads);
+ musicFree(m);
+ hostClockAdvance(MUSIC_FONT_KEEP_SEC * 1000 - 1000);
+ musicIdle(NULL);
+ audioWtGetInfo(&wt);
+ TEST_TRUE(wt.present);
+ hostClockAdvance(1000);
+ musicIdle(NULL);
+ audioWtGetInfo(&wt);
+ TEST_TRUE(!wt.present);
+
+ // musicSuspend, the library's platform suspend hook
+ tCase("waveplay: a DOS program run stops the music and lets the samples go at once");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ hostClockAdvance(10);
+ musicSuspend(true);
+ TEST_TRUE(!musicIsPlaying(m));
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_NONE);
+ musicSuspend(false);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ musicFree(m);
+ audioWtGetInfo(&wt);
+ TEST_TRUE(wt.present);
+ musicSuspend(true);
+ audioWtGetInfo(&wt);
+ TEST_TRUE(!wt.present);
+ musicSuspend(false);
+
+ tCase("waveplay: notes the SoundFont lacks play on the FM chip");
+ setIni("fakeWtVoices = 8\nfakeFmVoices = 9\nsoundFont = " SF_TEST_NAME "\n");
+ memset(sFmRegs, 0, sizeof(sFmRegs));
+ starts = sWtStarts;
+ TEST_EQ(musicLoadMem(flute, fluteLen, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, true), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ hostClockAdvance(10);
+ TEST_EQ(sWtStarts, starts);
+
+ // Flute: A4, 580 at block 4, on channel 0
+ TEST_EQ(sFmRegs[0xA0], 580 & 0xFF);
+ TEST_EQ(sFmRegs[0xB0], AUDIO_FM_KEY_ON | (4 << 2) | (580 >> 8));
+
+ tCase("waveplay: one music at a time; a stopped one gives it up");
+ TEST_EQ(musicLoadMem(smf, size, &m2), MUSIC_OK);
+ TEST_EQ(musicPlay(m2, false), MUSIC_OK);
+ musicGetInfo(m2, &info);
+ TEST_EQ(info.where, MUSIC_OUT_FM);
+ TEST_EQ(musicStop(m), MUSIC_OK);
+ TEST_EQ(musicLoadMem(smf, size, &m3), MUSIC_OK);
+ TEST_EQ(musicPlay(m3, true), MUSIC_OK);
+ musicGetInfo(m3, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_NONE);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_FM);
+ musicFree(m);
+ musicFree(m2);
+ musicFree(m3);
+
+ tCase("waveplay: midi = fm, or a missing SoundFont: the FM chip");
+ setIni("midi = fm\nfakeWtVoices = 8\nfakeFmVoices = 9\nsoundFont = " SF_TEST_NAME "\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_FM);
+ musicFree(m);
+ setIni("fakeWtVoices = 8\nfakeFmVoices = 9\nsoundFont = NOPE.SF2\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_FM);
+ musicFree(m);
+
+ tCase("waveplay: musicSetBank chooses a SoundFont");
+ setIni("fakeWtVoices = 8\nfakeFmVoices = 9\n");
+ TEST_EQ(musicLoadMem(smf, size, &m), MUSIC_OK);
+ TEST_EQ(musicSetBank(m, SF_TEST_NAME), MUSIC_OK);
+ TEST_EQ(musicPlay(m, false), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ TEST_EQ(musicLoadMem(smf, size, &m2), MUSIC_OK);
+ TEST_EQ(musicSetBank(m2, "NOPE.SF2"), MUSIC_ERR_BANK);
+ musicFree(m);
+ musicFree(m2);
+
+ tCase("waveplay: live MIDI");
+ setIni("fakeWtVoices = 8\nsoundFont = " SF_TEST_NAME "\n");
+ starts = sWtStarts;
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ musicGetInfo(m, &info);
+ TEST_EQ(info.where, MUSIC_OUT_WAVE);
+ TEST_EQ(musicMidiNoteOn(m, 0, 40, 127), MUSIC_OK);
+ TEST_EQ(sWtStarts, starts + 1);
+ TEST_EQ(sWtVoice[sWtLast].pitch, WT_PIANO_PITCH);
+ TEST_EQ(musicMidiProgram(m, 9, 0), MUSIC_OK);
+ TEST_EQ(musicMidiNoteOn(m, 9, 40, 100), MUSIC_OK);
+ TEST_EQ(sWtVoice[sWtLast].pitch, WT_DRUM_PITCH);
+ musicFree(m);
+
+ // A program's samples loaded as it is chosen (the notes sounding
+ // stopped first): LIVE.SF2's first program needs only Hit, Narrow
+ // Ramp as well
+ spec.instrument = 1;
+ writeFile(SF_LIVE_FILE, data, sfBuild(&spec, data));
+ setIni("fakeWtVoices = 8\nsoundFont = " SF_LIVE_NAME "\n");
+ TEST_EQ(musicMidiOpen(&m), MUSIC_OK);
+ audioWtGetInfo(&wt);
+ TEST_EQ(wt.memory - wt.freeMemory, WT_HIT_FRAMES * 2);
+ TEST_EQ(musicMidiNoteOn(m, 0, 40, 127), MUSIC_OK);
+ TEST_TRUE(sWtActive & (1u << sWtLast));
+ uploads = sWtUploads;
+ TEST_EQ(musicMidiProgram(m, 0, 1), MUSIC_OK);
+ TEST_EQ(sWtUploads, uploads + 1);
+ TEST_EQ(sWtActive, 0);
+ TEST_EQ(musicMidiNoteOn(m, 0, 55, 127), MUSIC_OK);
+ TEST_EQ(sWtVoice[sWtLast].loop, AUDIO_WT_LOOP_ON);
+ TEST_EQ(musicMidiProgram(m, 0, 0), MUSIC_OK);
+ TEST_EQ(sWtUploads, uploads + 1);
+ musicFree(m);
+ remove(SF_LIVE_FILE);
+
+ // The context goes after a while with no music
+ hostClockAdvance(MUSIC_FONT_KEEP_SEC * 1000);
+ musicIdle(NULL);
+ audioWtGetInfo(&wt);
+ TEST_TRUE(!wt.present);
+ remove(SF_TEST_FILE);
+}
+
+
+// General MIDI on wavetable voices: a recording backend, the test
+// SoundFont, its two samples where the loaded table says.
+static void waveTests(void) {
+ static uint8_t data[SF_TEST_MAX];
+ static RecWaveT rec;
+ static WaveSynthT synth;
+ static const uint8_t gmOn[] = { 0xF0, 0x7E, 0x7F, 0x09, 0x01, 0xF7 };
+ WaveLoadedT loaded[] = { { WT_RAMP_ADDR, WT_RAMP_FRAMES, WT_RAMP_CENTS, 16, 0 }, { WT_HIT_ADDR, WT_HIT_FRAMES, WT_HIT_CENTS, 16, 0 } };
+ SfSpecT spec = { 2, SF_TEST_FRAMES, 0 };
+ WaveBackendT be = { wrecAlloc, wrecFree, wrecStart, wrecUpdate, wrecRelease, wrecActive, &rec, WT_FLAGS_HW };
+ WaveBankT bank;
+ RecSinkT over;
+ int32_t id;
+ int32_t frees;
+ int32_t starts;
+
+ TEST_EQ(sf2Parse(data, sfBuild(&spec, data), &bank), MUSIC_OK);
+
+ tCase("wave: rates in cents");
+ TEST_EQ(waveRatioCents(44100, 44100), 0);
+ TEST_EQ(waveRatioCents(22050, 44100), -1200);
+ TEST_EQ(waveRatioCents(88200, 44100), 1200);
+
+ // 1200 log2(32000 / 44100) = -555.4
+ int32_t cents = waveRatioCents(32000, 44100);
+
+ TEST_TRUE(cents >= -556 && cents <= -555);
+
+ tCase("wave: a note's sample, loop, pitch, loudness, and pan");
+ memset(&rec, 0, sizeof(rec));
+ rec.limit = WT_VOICES_MAX;
+ recSinkInit(&over);
+ waveSynthInit(&synth, &be, &bank, loaded, &over.sink, FULL);
+ synth.sink.message(&synth.sink, 0x90, 40, 127);
+ TEST_EQ(rec.starts, 1);
+ id = rec.last;
+
+ const AudioWtVoiceT *v = &rec.voice[id];
+
+ TEST_EQ(v->addr, WT_RAMP_ADDR);
+ TEST_EQ(v->end, 100);
+ TEST_EQ(v->loopStart, 10);
+ TEST_EQ(v->loopEnd, 90);
+ TEST_EQ(v->loop, AUDIO_WT_LOOP_ON);
+ TEST_EQ(v->bits, 16);
+ TEST_EQ(v->pitch, WT_PIANO_PITCH);
+ TEST_EQ(v->attenuation, WT_PIANO_ATTEN);
+ TEST_EQ(v->pan, 100);
+ TEST_EQ(v->filterFc, 13500);
+ TEST_EQ(over.count, 0);
+
+ tCase("wave: bend, volume, pan, and the bend range");
+ synth.sink.message(&synth.sink, 0xE0, 0x7F, 0x7F);
+ TEST_EQ(rec.what[id], AUDIO_WT_SET_PITCH);
+ TEST_EQ(v->pitch, WT_PIANO_PITCH + WT_BEND_UP);
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_VOLUME, 127);
+ TEST_EQ(rec.what[id], AUDIO_WT_SET_VOLUME);
+ TEST_EQ(v->attenuation, WT_PIANO_ATTEN - WT_CC7_ATTEN);
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_PAN, 0);
+ TEST_EQ(rec.what[id], AUDIO_WT_SET_PAN);
+ TEST_EQ(v->pan, 100 - AUDIO_WT_PAN_MAX);
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_RPN_MSB, 0);
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_RPN_LSB, MIDI_RPN_BEND_RANGE);
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_DATA, 12);
+ TEST_EQ(v->pitch, WT_PIANO_PITCH + 8191 * 12 * 100 / 8192);
+
+ // All of it passed on (to keep the overflow's channels in step)
+ TEST_EQ(over.count, 6);
+
+ tCase("wave: note off and the sustain pedal");
+ synth.sink.message(&synth.sink, 0x80, 40, 0);
+ TEST_TRUE(rec.released[id]);
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_SUSTAIN, 127);
+ synth.sink.message(&synth.sink, 0x90, 41, 100);
+
+ int32_t held = rec.last;
+
+ synth.sink.message(&synth.sink, 0x80, 41, 0);
+ TEST_TRUE(!rec.released[held]);
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_SUSTAIN, 0);
+ TEST_TRUE(rec.released[held]);
+
+ tCase("wave: voices that fall silent are given back");
+ rec.active[id] = false;
+ waveSynthTick(&synth, MUSIC_TICK_US);
+ TEST_TRUE(!rec.used[id]);
+ TEST_TRUE(rec.used[held]);
+ synth.sink.silence(&synth.sink);
+ TEST_TRUE(!rec.used[held]);
+ TEST_EQ(over.silences, 1);
+
+ tCase("wave: drums on channel 10; exclusive classes");
+ synth.sink.message(&synth.sink, 0x99, 40, 100);
+ v = &rec.voice[rec.last];
+ TEST_EQ(v->addr, WT_HIT_ADDR);
+ TEST_EQ(v->pitch, WT_DRUM_PITCH);
+ TEST_EQ(v->attenuation, WT_DRUM_ATTEN);
+ TEST_EQ(v->loop, AUDIO_WT_LOOP_NONE);
+ TEST_EQ(v->end, 100);
+ frees = rec.frees;
+ synth.sink.message(&synth.sink, 0x99, 42, 100);
+ TEST_EQ(rec.frees, frees);
+ synth.sink.silence(&synth.sink);
+
+ // A class (an open and a closed hi-hat): each note stops the last
+ const WavePresetT *kit = waveBankPreset(&bank, SF_DRUM_BANK, 0);
+
+ bank.regions[kit->first].gen[SF_GEN_EXCLUSIVE_CLASS] = 1;
+ synth.sink.message(&synth.sink, 0x99, 42, 100);
+ frees = rec.frees;
+ synth.sink.message(&synth.sink, 0x99, 44, 100);
+ TEST_EQ(rec.frees, frees + 1);
+ bank.regions[kit->first].gen[SF_GEN_EXCLUSIVE_CLASS] = 0;
+ synth.sink.silence(&synth.sink);
+
+ tCase("wave: with no voice free, a released one, else the oldest");
+ rec.limit = 2;
+ synth.sink.message(&synth.sink, 0x90, 40, 127);
+ synth.sink.message(&synth.sink, 0x90, 41, 127);
+ TEST_EQ(rec.last, 2);
+ synth.sink.message(&synth.sink, 0x80, 41, 0);
+ frees = rec.frees;
+ synth.sink.message(&synth.sink, 0x90, 43, 127);
+ TEST_EQ(rec.frees, frees + 1);
+ TEST_EQ(rec.last, 2);
+ synth.sink.message(&synth.sink, 0x90, 45, 127);
+ TEST_EQ(rec.frees, frees + 2);
+ TEST_EQ(rec.last, 1);
+ synth.sink.silence(&synth.sink);
+ rec.limit = WT_VOICES_MAX;
+
+ tCase("wave: notes the bank cannot play go to the overflow");
+ loaded[1].addr = WAVE_NOT_LOADED;
+ recSinkInit(&over);
+ starts = rec.starts;
+ synth.sink.message(&synth.sink, 0x90, 70, 100);
+ TEST_EQ(rec.starts, starts);
+ TEST_EQ(over.count, 1);
+ TEST_EQ(over.msgs[0], 0x90u | 70u << 8 | 100u << 16);
+ synth.sink.message(&synth.sink, 0xC0, 99, 0);
+ synth.sink.message(&synth.sink, 0x90, 40, 100);
+ TEST_EQ(rec.starts, starts);
+ TEST_EQ(over.count, 3);
+ synth.sink.message(&synth.sink, 0xC0, 0, 0);
+ synth.sink.message(&synth.sink, 0x90, 40, 100);
+ TEST_EQ(rec.starts, starts + 1);
+ TEST_EQ(over.count, 4);
+ loaded[1].addr = WT_HIT_ADDR;
+ synth.sink.silence(&synth.sink);
+
+ tCase("wave: GM System On resets the channels");
+ synth.sink.message(&synth.sink, 0xB0, MIDI_CC_VOLUME, 0);
+ synth.sink.sysex(&synth.sink, gmOn, sizeof(gmOn));
+ TEST_EQ(over.sysexCount, 1);
+ synth.sink.message(&synth.sink, 0x90, 40, 127);
+ TEST_EQ(rec.voice[rec.last].attenuation, WT_PIANO_ATTEN);
+ synth.sink.silence(&synth.sink);
+
+ tCase("wave: envelopes run here when the backend has none");
+ be.flags = 0;
+ waveSynthInit(&synth, &be, &bank, loaded, NULL, FULL);
+ synth.sink.message(&synth.sink, 0x90, 40, 127);
+ id = rec.last;
+ TEST_EQ(rec.voice[id].attenuation, AUDIO_WT_ATTEN_MAX);
+
+ for (int32_t i = 0; i < 10; i++) {
+ waveSynthTick(&synth, MUSIC_TICK_US);
+ }
+
+ TEST_EQ(rec.voice[id].attenuation, WT_PIANO_ATTEN);
+ frees = rec.frees;
+ synth.sink.message(&synth.sink, 0x80, 40, 0);
+
+ for (int32_t i = 0; i < 10; i++) {
+ waveSynthTick(&synth, MUSIC_TICK_US);
+ }
+
+ TEST_EQ(rec.frees, frees + 1);
+ TEST_TRUE(!rec.used[id]);
+ waveBankFree(&bank);
+}
+
+
+static bool wrecActive(void *owner, int32_t voice) {
+ return ((RecWaveT *)owner)->active[voice];
+}
+
+
+static int32_t wrecAlloc(void *owner) {
+ RecWaveT *r = (RecWaveT *)owner;
+
+ for (int32_t v = 1; v <= r->limit; v++) {
+ if (!r->used[v]) {
+ r->used[v] = true;
+ return v;
+ }
+ }
+
+ return AUDIO_ERR_NO_VOICE;
+}
+
+
+static void wrecFree(void *owner, int32_t voice) {
+ RecWaveT *r = (RecWaveT *)owner;
+
+ r->used[voice] = false;
+ r->active[voice] = false;
+ r->frees++;
+}
+
+
+static void wrecRelease(void *owner, int32_t voice) {
+ ((RecWaveT *)owner)->released[voice] = true;
+}
+
+
+static void wrecStart(void *owner, int32_t voice, const AudioWtVoiceT *v) {
+ RecWaveT *r = (RecWaveT *)owner;
+
+ r->voice[voice] = *v;
+ r->active[voice] = true;
+ r->released[voice] = false;
+ r->last = voice;
+ r->starts++;
+}
+
+
+static void wrecUpdate(void *owner, int32_t voice, const AudioWtVoiceT *v, uint32_t what) {
+ RecWaveT *r = (RecWaveT *)owner;
+
+ r->voice[voice] = *v;
+ r->what[voice] = what;
}
@@ -2115,6 +4618,7 @@ static void xmpTests(void) {
}
TEST_TRUE(xmpLength(p) > 0);
+ TEST_EQ(xmpBidiAdjust(p), strcmp(names[i], "it") == 0 ? 1U : 0U);
memset(pcm, 0, sizeof(pcm));
xmpRender(p, pcm, RATE_DEV, false);
@@ -2158,12 +4662,23 @@ int main(void) {
smfTests();
seqTests();
op2Tests();
+ sfTests();
+ sf1Tests();
fmTests();
+ waveTests();
mt32Tests();
cmpTests();
xmpTests();
playTests();
+ soundTests();
+ waveLoadTests();
+ wavePlayTests();
+ modHwTests();
+ softwaveTests();
+ patTests();
+ gmBankTests();
basicTests();
+ midiRecordTests();
mmlTests();
mmlPlayTests();
musicShutdown();
diff --git a/src/test/music/playerTest.c b/src/test/music/playerTest.c
new file mode 100644
index 00000000..5373eb1c
--- /dev/null
+++ b/src/test/music/playerTest.c
@@ -0,0 +1,635 @@
+// 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.
+
+// playerTest.c -- the Media Player program (src/apps/kpunch/player),
+// compiled from its project and run in the shell fixture with music.lib,
+// audio.lib, and fileops linked in, on the fake sound driver (pcm.drv:
+// 22,050 Hz, mixed here a tick at a time) and the bundled FM bank.
+//
+// Covered: files from the command line (labels, sound, time), pause and
+// resume, the position and volume sliders, the end moving on, Next and
+// Previous, Repeat and Shuffle, Move / Remove / Clear, a file that cannot
+// be played (on its own and in the middle of the list), a second start
+// handing over its files (NewArgs), Open and Add Folder through their
+// dialogs, and the settings and list kept in PLAYER.INI.
+
+#include "shellFixture.h"
+#include "formrt/formrt.h"
+#include "audioInt.h"
+#include "musicFiles.h"
+#include "musicInt.h"
+#include "dvxPlat.h"
+#include "dvxPlatHost.h"
+#include "dvxWm.h"
+#include "listBox/listBox.h"
+#include "slider/slider.h"
+
+#include
+#include
+#include
+#include
+
+#define APP_PLAYER "apps/kpunch/player/player.app"
+#define PLAYER_DBP "src/apps/kpunch/player/player.dbp" // under fixRepoRoot
+#define PLAYER_INI "PLAYER.INI" // in the program's user folder
+#define PLAYER_TITLE "Media Player"
+#define START_FRAMES_MAX 400
+#define MODE_DIR 0755
+#define TEXT_MAX 256
+
+#define RATE_DEV 22050
+#define TICK_MS 100
+#define TICK_FRAMES (RATE_DEV * TICK_MS / 1000)
+#define HELD_FRAMES 4 // the mixer's current and next frames, heard after a stop
+#define TONE_SECONDS 3
+#define TONE_FRAMES (SOUND_RATE * TONE_SECONDS)
+#define TONE_BYTES_MAX (WAV_HEADER_BYTES + TONE_FRAMES * 2)
+#define MEDIA_BYTES_MAX 8192
+#define END_TICKS_MAX 120 // longer than any file here (the module's 7.7 s)
+#define SETTLE_TICKS 6 // the slider's seek: two timer ticks of 250 ms, and a little
+
+#define TONE_FILE "TONE.WAV"
+#define TUNE_FILE "TUNE.MOD"
+#define FLUTE_FILE "FLUTE.MID"
+#define BAD_FILE "BAD.WAV"
+#define MEDIA_DIR "MEDIA"
+#define TUNE_TITLE "dvx test"
+#define TONE_INFO "Sound (WAV) - Sound device"
+#define TUNE_INFO "Module (MOD) - Sound device"
+#define FLUTE_INFO "MIDI music (MID) - FM software synthesizer"
+#define NOTHING_PLAYING "Nothing is playing"
+#define PLAY_CAPTION "Pl&ay"
+#define PAUSE_CAPTION "P&ause"
+
+#define MENU_OPEN "&Open..."
+#define MENU_ADD_FOLDER "Add Fol&der..."
+#define MENU_MOVE_UP "Move &Up"
+#define MENU_MOVE_DOWN "Move Do&wn"
+#define MENU_CLEAR "&Clear List"
+#define MENU_PLAY_PAUSE "&Play / Pause"
+#define MENU_STOP "&Stop"
+#define MENU_PREV "Pre&vious"
+#define MENU_NEXT "&Next"
+#define MENU_REPEAT_OFF "Repeat O&ff"
+#define MENU_REPEAT_LIST "Repeat &List"
+#define MENU_REPEAT_TRACK "Repeat &Track"
+#define MENU_SHUFFLE "S&huffle"
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static void audioHooks(bool on);
+static void caseErrorsAndNewArgs(void);
+static void caseList(void);
+static void caseNextPrevious(void);
+static void caseOpenAndFolder(void);
+static void casePauseSeekVolume(void);
+static void caseRepeatShuffle(void);
+static void caseSettings(void);
+static void caseStart(void);
+void fakeDrvEvent(const char *driver, const char *event);
+void fakeDrvTone(uint32_t hz);
+static bool fileHas(const char *path, const char *text);
+static int32_t heard(void);
+static const char *infoText(void);
+static int32_t launch(const char *args);
+static WidgetT *listOf(void);
+static void makeMedia(void);
+static void menu(const char *caption);
+static bool menuChecked(const char *caption);
+static void pressKey(const char *widget, int32_t key);
+static const char *text(const char *widget);
+static void tick(int32_t n);
+static const char *titleText(void);
+static bool untilTitle(const char *title);
+
+static int16_t sOut[TICK_FRAMES * 2];
+static int32_t sId; // the program running
+static WindowT *sWin; // its window
+
+
+// The shell's idle time and program ends for the two libraries, as their
+// shell hooks (musicShell.c, audioShell.c, not linked here) do.
+static void audioHooks(bool on) {
+ if (on) {
+ shellRegisterIdle(audioIdle, NULL);
+ shellRegisterIdle(musicIdle, NULL);
+ shellRegisterAppCleanup(audioAppEnded);
+ shellRegisterAppCleanup(musicAppEnded);
+ return;
+ }
+
+ shellUnregisterAppCleanup(musicAppEnded);
+ shellUnregisterAppCleanup(audioAppEnded);
+ shellUnregisterIdle(musicIdle, NULL);
+ shellUnregisterIdle(audioIdle, NULL);
+}
+
+
+// A file that cannot be played says why, on its own and when the list
+// reaches it (the list goes on past it); a second start of the program
+// hands its files over, a file already in the list played where it is.
+static void caseErrorsAndNewArgs(void) {
+ tCase("player: a second start hands its files over; a file that cannot be played");
+ launch(FLUTE_FILE " " BAD_FILE " " TONE_FILE);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 3);
+ TEST_STR_EQ(titleText(), FLUTE_FILE);
+ TEST_STR_EQ(infoText(), FLUTE_INFO);
+ tick(1);
+ TEST_TRUE(heard() > 0);
+
+ // The flute ends, BAD.WAV is said to be unplayable, TONE.WAV plays.
+ fixDialogsOn("OK");
+ fixDialogs = 0;
+ TEST_TRUE(untilTitle(TONE_FILE));
+ fixDialogsOff();
+ TEST_EQ(fixDialogs, 1);
+ TEST_EQ(wgtListBoxGetSelected(listOf()), 2);
+
+ // Chosen by itself: the reason stays shown.
+ wgtListBoxSetSelected(listOf(), 1);
+ fixDialogsOn("OK");
+ fixDialogs = 0;
+ pressKey("lstTracks", KEY_ENTER);
+ fixDialogsOff();
+ TEST_EQ(fixDialogs, 1);
+ TEST_STR_EQ(titleText(), BAD_FILE);
+ TEST_STR_EQ(infoText(), audioErrorText(AUDIO_ERR_UNSUPPORTED));
+ TEST_STR_EQ(text("btnPlay"), PLAY_CAPTION);
+
+ // A file already listed is not added again.
+ launch(FLUTE_FILE);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 3);
+ TEST_EQ(wgtListBoxGetSelected(listOf()), 0);
+ menu(MENU_CLEAR);
+}
+
+
+// Move Up and Move Down (the track playing follows its file), Del, the
+// track playing taken out, Clear List.
+static void caseList(void) {
+ tCase("player: move, remove, clear");
+ launch(TONE_FILE " " TUNE_FILE " " FLUTE_FILE);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 3);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+
+ menu(MENU_MOVE_DOWN);
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 0), TUNE_FILE);
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 1), TONE_FILE);
+ TEST_EQ(wgtListBoxGetSelected(listOf()), 1);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+ menu(MENU_MOVE_DOWN);
+ menu(MENU_MOVE_DOWN);
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 2), TONE_FILE);
+ menu(MENU_MOVE_UP);
+ menu(MENU_MOVE_UP);
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 0), TONE_FILE);
+
+ // Next follows the new order.
+ menu(MENU_NEXT);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+
+ wgtListBoxSetSelected(listOf(), 2);
+ pressKey("lstTracks", KEY_DELETE);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 2);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ wgtListBoxSetSelected(listOf(), 1);
+ pressKey("lstTracks", KEY_DELETE);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 1);
+ TEST_STR_EQ(titleText(), NOTHING_PLAYING);
+ TEST_STR_EQ(sWin->title, PLAYER_TITLE);
+ tick(1);
+ TEST_EQ(heard(), 0);
+ menu(MENU_CLEAR);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 0);
+}
+
+
+// The end of a track plays the next; Next at the end of the list does
+// nothing; Previous near a track's start goes back, later starts it again;
+// the end of the list stops.
+static void caseNextPrevious(void) {
+ tCase("player: the end goes on, Next, Previous, the end of the list");
+ TEST_TRUE(untilTitle(TUNE_TITLE));
+ TEST_STR_EQ(infoText(), TUNE_INFO);
+ TEST_EQ(wgtListBoxGetSelected(listOf()), 1);
+ menu(MENU_NEXT);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ menu(MENU_PREV);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+ tick(SETTLE_TICKS);
+
+ // Before the first track is the first track.
+ menu(MENU_PREV);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+ TEST_STR_EQ(text("lblTime"), "0:00 / 0:03");
+ menu(MENU_NEXT);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+
+ // Four seconds in, Previous starts the module again.
+ tick(40);
+ TEST_STR_EQ(text("lblTime"), "0:04 / 0:07");
+ menu(MENU_PREV);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ TEST_STR_EQ(text("lblTime"), "0:00 / 0:07");
+
+ // The end of the list: stopped at the start of the last track.
+ TEST_TRUE(untilTitle(NULL));
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ TEST_STR_EQ(text("btnPlay"), PLAY_CAPTION);
+ TEST_STR_EQ(text("lblTime"), "0:00 / 0:07");
+ tick(1);
+ TEST_EQ(heard(), 0);
+
+ // Play goes on with the track selected.
+ wgtListBoxSetSelected(listOf(), 0);
+ menu(MENU_PLAY_PAUSE);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+ TEST_STR_EQ(text("btnPlay"), PAUSE_CAPTION);
+}
+
+
+// Open's file dialog plays the file chosen (the list's own entry when it
+// is there); Add Folder adds the folder's music and sound files only.
+static void caseOpenAndFolder(void) {
+ tCase("player: Open and Add Folder");
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 0);
+ fixDialogsOn("Open");
+ fixDialogsType(TUNE_FILE, "Open");
+ menu(MENU_OPEN);
+ fixDialogsOff();
+ tick(1);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 1);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ TEST_TRUE(heard() > 0);
+
+ // The folder dialog starts where Open was: MEDIA is in it.
+ fixDialogsOn("OK");
+ fixDialogsType(MEDIA_DIR, "OK");
+ menu(MENU_ADD_FOLDER);
+ fixDialogsOff();
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 3);
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 1), "A.WAV");
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 2), "B.MID");
+ menu(MENU_CLEAR);
+}
+
+
+// Pause and resume (the button and the menu), the position slider (the
+// music goes there once the slider stands still), the volume slider.
+static void casePauseSeekVolume(void) {
+ tCase("player: pause, resume, seek, volume");
+ tWidgetClick(tFind(sWin, "btnPlay"));
+ TEST_STR_EQ(text("btnPlay"), PLAY_CAPTION);
+ tick(1);
+ TEST_EQ(heard(), 0);
+
+ const char *at = text("lblTime");
+ char held[TEXT_MAX];
+
+ snprintf(held, sizeof(held), "%s", at);
+ tick(SETTLE_TICKS);
+ TEST_STR_EQ(text("lblTime"), held);
+ menu(MENU_PLAY_PAUSE);
+ TEST_STR_EQ(text("btnPlay"), PAUSE_CAPTION);
+ tick(1);
+ TEST_TRUE(heard() > 0);
+
+ // One step right of the start: two seconds in once it settles.
+ menu(MENU_PREV);
+ pressKey("sldPosition", KEY_RIGHT);
+ pressKey("sldPosition", KEY_RIGHT);
+ TEST_STR_EQ(text("lblTime"), "0:02 / 0:03");
+ tick(SETTLE_TICKS);
+ TEST_STR_EQ(text("lblTime"), "0:02 / 0:03");
+ TEST_EQ(wgtSliderGetValue(tFind(sWin, "sldPosition")), 2);
+
+ pressKey("sldVolume", KEY_HOME);
+ tick(1);
+ TEST_EQ(heard(), 0);
+ pressKey("sldVolume", KEY_END);
+ tick(1);
+ TEST_TRUE(heard() > 0);
+}
+
+
+// Repeat List goes round; Repeat Track plays the track again; Shuffle
+// with two tracks takes the other one next.
+static void caseRepeatShuffle(void) {
+ tCase("player: Repeat List, Repeat Track, Shuffle");
+ menu(MENU_REPEAT_LIST);
+ TEST_TRUE(menuChecked(MENU_REPEAT_LIST));
+ TEST_TRUE(!menuChecked(MENU_REPEAT_OFF));
+ menu(MENU_NEXT);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ menu(MENU_NEXT);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+
+ menu(MENU_REPEAT_TRACK);
+ tick(TONE_SECONDS * 1000 / TICK_MS + SETTLE_TICKS);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+ TEST_STR_EQ(text("btnPlay"), PAUSE_CAPTION);
+ tick(1);
+ TEST_TRUE(heard() > 0);
+
+ menu(MENU_REPEAT_LIST);
+ menu(MENU_SHUFFLE);
+ TEST_TRUE(menuChecked(MENU_SHUFFLE));
+ menu(MENU_NEXT);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ menu(MENU_NEXT);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+ menu(MENU_SHUFFLE);
+ menu(MENU_REPEAT_OFF);
+ menu(MENU_STOP);
+ TEST_STR_EQ(text("btnPlay"), PLAY_CAPTION);
+ TEST_STR_EQ(text("lblTime"), "0:00 / 0:03");
+ menu(MENU_CLEAR);
+}
+
+
+// Volume, Repeat, Shuffle, the list, and the selected track kept in
+// PLAYER.INI and read back by the next start.
+static void caseSettings(void) {
+ char dir[TEXT_MAX];
+ char ini[TEXT_MAX];
+
+ tCase("player: settings and the list kept");
+ TEST_TRUE(platformAppUserDir(dir, sizeof(dir), APP_PLAYER));
+ snprintf(ini, sizeof(ini), "%s/%s", dir, PLAYER_INI);
+ launch(TONE_FILE " " TUNE_FILE);
+ menu(MENU_REPEAT_LIST);
+ menu(MENU_SHUFFLE);
+ pressKey("sldVolume", KEY_END);
+
+ for (int32_t i = 0; i < 10; i++) {
+ pressKey("sldVolume", KEY_LEFT);
+ }
+
+ wgtListBoxSetSelected(listOf(), 1);
+ TEST_TRUE(fixCloseApp(sWin));
+ TEST_TRUE(fileHas(ini, "Volume = 90"));
+ TEST_TRUE(fileHas(ini, "Repeat = list"));
+ TEST_TRUE(fileHas(ini, "Shuffle = true"));
+ TEST_TRUE(fileHas(ini, "Track = 1"));
+ TEST_TRUE(fileHas(ini, "File1 = " TONE_FILE));
+ TEST_TRUE(fileHas(ini, "File2 = " TUNE_FILE));
+
+ launch(NULL);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 2);
+ TEST_EQ(wgtListBoxGetSelected(listOf()), 1);
+ TEST_STR_EQ(titleText(), NOTHING_PLAYING);
+ TEST_EQ(wgtSliderGetValue(tFind(sWin, "sldVolume")), 90);
+ TEST_TRUE(menuChecked(MENU_REPEAT_LIST));
+ TEST_TRUE(menuChecked(MENU_SHUFFLE));
+ tick(1);
+ TEST_EQ(heard(), 0);
+ menu(MENU_PLAY_PAUSE);
+ TEST_STR_EQ(titleText(), TUNE_TITLE);
+ TEST_TRUE(fixCloseApp(sWin));
+}
+
+
+// Started with two files: both listed, the first playing, heard, its
+// time shown.
+static void caseStart(void) {
+ tCase("player: files from the command line");
+ launch(TONE_FILE " " TUNE_FILE);
+ TEST_TRUE(sWin != NULL);
+ TEST_EQ(wgtListBoxGetItemCount(listOf()), 2);
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 0), TONE_FILE);
+ TEST_STR_EQ(wgtListBoxGetItem(listOf(), 1), TUNE_FILE);
+ TEST_EQ(wgtListBoxGetSelected(listOf()), 0);
+ TEST_STR_EQ(titleText(), TONE_FILE);
+ TEST_STR_EQ(infoText(), TONE_INFO);
+ TEST_STR_EQ(sWin->title, TONE_FILE " - " PLAYER_TITLE);
+ TEST_STR_EQ(text("btnPlay"), PAUSE_CAPTION);
+ tick(SETTLE_TICKS * 2);
+ TEST_TRUE(heard() > 0);
+ TEST_STR_EQ(text("lblTime"), "0:01 / 0:03");
+ TEST_EQ(wgtSliderGetValue(tFind(sWin, "sldPosition")), 1);
+}
+
+
+// The fake sound driver's calls back (pcm.drv): nothing to record here.
+void fakeDrvEvent(const char *driver, const char *event) {
+ (void)driver;
+ (void)event;
+}
+
+
+void fakeDrvTone(uint32_t hz) {
+ (void)hz;
+}
+
+
+static bool fileHas(const char *path, const char *want) {
+ int32_t len = 0;
+ char *data = platformReadFile(path, &len);
+ bool has = data && strstr(data, want) != NULL;
+
+ free(data);
+ return has;
+}
+
+
+// The loudest sample of the last tick, after the frames the mixer held.
+static int32_t heard(void) {
+ int32_t peak = 0;
+
+ for (int32_t i = HELD_FRAMES * 2; i < TICK_FRAMES * 2; i++) {
+ int32_t v = abs(sOut[i]);
+
+ peak = v > peak ? v : peak;
+ }
+
+ return peak;
+}
+
+
+static const char *infoText(void) {
+ return text("lblInfo");
+}
+
+
+// Start the program with args (NULL: none), or hand them to it when it
+// runs; its window afterwards.
+static int32_t launch(const char *args) {
+ sId = shellLoadAppWithArgs(tCtx(), APP_PLAYER, args ? args : "");
+
+ for (int32_t i = 0; i < START_FRAMES_MAX && !fixWindowForApp(sId); i++) {
+ fixPumpShell(1);
+ }
+
+ fixPumpShell(SETTLE_FRAMES);
+ sWin = fixWindowForApp(sId);
+ return sId;
+}
+
+
+static WidgetT *listOf(void) {
+ return tFind(sWin, "lstTracks");
+}
+
+
+// TONE.WAV (three seconds of the test sine), TUNE.MOD (7.7 s), FLUTE.MID
+// (half a second), BAD.WAV (compressed), and MEDIA\ with a sound, a MIDI
+// file, and a text file.
+static void makeMedia(void) {
+ static uint8_t tone[TONE_BYTES_MAX];
+ static uint8_t buf[MEDIA_BYTES_MAX];
+
+ writeFile(TONE_FILE, tone, wavBuild(tone, 16, 1, TONE_FRAMES, WAV_TAG_PCM));
+ writeFile(TUNE_FILE, buf, makeMod(buf));
+ writeFile(FLUTE_FILE, buf, fluteSmf(buf));
+ writeFile(BAD_FILE, buf, wavBuild(buf, 8, 1, MEDIA_BYTES_MAX / 2, WAV_TAG_ADPCM));
+ TEST_EQ(mkdir(MEDIA_DIR, MODE_DIR), 0);
+ writeFile(MEDIA_DIR "/A.WAV", tone, wavBuild(tone, 16, 1, SOUND_FRAMES, WAV_TAG_PCM));
+ writeFile(MEDIA_DIR "/B.MID", buf, fluteSmf(buf));
+ writeFile(MEDIA_DIR "/NOTE.TXT", "not music", 9);
+}
+
+
+// Choose a menu command by its caption, as the user would: a check item
+// turns over and a radio item is checked (as a click in the menu does),
+// then the program gets the command.
+static void menu(const char *caption) {
+ AppContextT *ctx = tCtx();
+ int32_t id = fixMenuId(sWin, caption);
+ int32_t saved = ctx->currentAppId;
+ MenuItemT *item = NULL;
+
+ TEST_TRUE(id >= 0);
+
+ if (id < 0) {
+ return;
+ }
+
+ for (int32_t i = 0; !item && i < sWin->menuBar->menuCount; i++) {
+ item = wmMenuFindItemInMenu(sWin->menuBar->menus[i], id);
+ }
+
+ if (item && item->type != MenuItemNormalE) {
+ wmMenuItemSetChecked(sWin->menuBar, id, item->type == MenuItemRadioE || !item->checked);
+ }
+
+ ctx->currentAppId = sWin->appId;
+ sWin->onMenu(sWin, id);
+ ctx->currentAppId = saved;
+ fixPumpShell(SETTLE_FRAMES);
+}
+
+
+static bool menuChecked(const char *caption) {
+ return wmMenuItemIsChecked(sWin->menuBar, fixMenuId(sWin, caption));
+}
+
+
+// A key to a control, as the user would press it with the control focused.
+static void pressKey(const char *widget, int32_t key) {
+ wgtSetFocused(tFind(sWin, widget));
+ tWidgetKey(sWin, key, 0);
+ fixPumpShell(SETTLE_FRAMES);
+}
+
+
+static const char *text(const char *widget) {
+ WidgetT *w = tFind(sWin, widget);
+
+ return w ? wgtGetText(w) : "";
+}
+
+
+// n ticks of TICK_MS: the clock (the program's timer, MIDI), the mixer
+// (into sOut, the last tick kept), the shell (the libraries' idle time,
+// the program).
+static void tick(int32_t n) {
+ for (int32_t i = 0; i < n; i++) {
+ tAdvanceClock(TICK_MS);
+ audioMixRender(sOut, TICK_FRAMES);
+ fixPumpShell(1);
+ }
+}
+
+
+static const char *titleText(void) {
+ return text("lblTitle");
+}
+
+
+// Tick until the title is title (NULL: until nothing plays); true when it
+// came.
+static bool untilTitle(const char *title) {
+ for (int32_t i = 0; i < END_TICKS_MAX; i++) {
+ if (title ? strcmp(titleText(), title) == 0 : strcmp(text("btnPlay"), PLAY_CAPTION) == 0) {
+ return true;
+ }
+
+ tick(1);
+ }
+
+ return false;
+}
+
+
+int main(void) {
+ char ini[TEXT_MAX];
+
+ tSuiteBegin("player");
+
+ if (!fixProcessInit("playerTest.log")) {
+ return 1;
+ }
+
+ basNativeHostSetLive(true);
+ fixShellUp(SCREEN_BPP);
+
+ // The sound driver and the FM bank where the libraries look, and
+ // DVX.INI choosing them.
+ TEST_EQ(platformMkdirRecursive(AUDIO_DRV_DIR), 0);
+ TEST_EQ(platformMkdirRecursive("libs/kpunch/music"), 0);
+ TEST_EQ(platformMkdirRecursive(DVX_USER_DIR), 0);
+ TEST_TRUE(fixCopyFileTo(PLAYER_DRV_DIR "/pcm.drv", AUDIO_DRV_DIR "/pcm.drv"));
+ TEST_TRUE(fixCopyFileTo(PLAYER_BANK, MUSIC_BANK_DEFAULT));
+ snprintf(ini, sizeof(ini), "[audio]\ndriver = pcm\nmidi = software\n");
+ TEST_TRUE(tWriteFile(platformDvxIniPath(), ini, (int32_t)strlen(ini)));
+ TEST_TRUE(fixInstallBasicApp(SHELLTEST_BASCOMP, SHELLTEST_BASSTUB_SO, PLAYER_DBP, APP_PLAYER));
+ makeMedia();
+ audioHooks(true);
+
+ caseStart();
+ casePauseSeekVolume();
+ caseNextPrevious();
+ caseRepeatShuffle();
+ caseList();
+ caseErrorsAndNewArgs();
+ caseOpenAndFolder();
+ caseSettings();
+
+ fixKillAllApps();
+ audioHooks(false);
+ musicShutdown();
+ audioShutdown();
+ fixShellDown();
+ return tSuiteEnd();
+}
diff --git a/src/test/music/recorderTest.c b/src/test/music/recorderTest.c
new file mode 100644
index 00000000..ae9eae4f
--- /dev/null
+++ b/src/test/music/recorderTest.c
@@ -0,0 +1,601 @@
+// 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.
+
+// recorderTest.c -- the Sound Recorder program (src/apps/kpunch/recorder),
+// compiled from its project and run in the shell fixture with audio.lib
+// and fileops linked in, on the fake sound driver (pcm.drv), which
+// records: the test hands it the blocks a card's interrupt would
+// (audioRecPush).
+//
+// Covered: the window at start, a recording (the state, the length, the
+// level, the buttons), playing it back to its end, Save As, the quality
+// and the input (what the card is asked for), a recording not saved
+// asked about (New), Open, the settings kept in RECORDER.INI, and a MIDI
+// keyboard recorded through the fake MIDI port (midifake.drv: what the
+// test feeds its input, what comes out of it), heard as it is played,
+// played back, saved, and opened.
+
+#include "shellFixture.h"
+#include "formrt/formrt.h"
+#include "audioInt.h"
+#include "musicFiles.h"
+#include "musicInt.h"
+#include "dvxPlat.h"
+#include "dvxPlatHost.h"
+#include "dvxWm.h"
+#include "progressBar/progress.h"
+
+#include
+#include