MOD and SFX support.
This commit is contained in:
parent
3e9c7f4926
commit
af6230e696
36 changed files with 3009 additions and 93 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.mod filter=lfs diff=lfs merge=lfs -text
|
||||
*.sfx filter=lfs diff=lfs merge=lfs -text
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -12,13 +12,7 @@ build/
|
|||
*.bin
|
||||
|
||||
# Toolchain installations (everything fetched/built by install.sh)
|
||||
toolchains/iigs/*
|
||||
toolchains/amiga/*
|
||||
toolchains/atarist/*
|
||||
toolchains/dos/*
|
||||
toolchains/emulators/*
|
||||
toolchains/.install_state/*
|
||||
toolchains/cache/*
|
||||
toolchains/*
|
||||
|
||||
# Disk images
|
||||
*.adf
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ make
|
|||
```
|
||||
|
||||
This builds `libjoey.a` for every target whose toolchain is installed,
|
||||
plus the `examples/hello` program for each.
|
||||
plus the example programs (`hello`, `pattern`, `keys`, `joy`, `sprite`,
|
||||
`audio`) for each.
|
||||
|
||||
|
||||
## Building for a single target
|
||||
|
|
@ -49,7 +50,8 @@ src/core/ portable library code
|
|||
src/codegen/ runtime sprite codegen (per-CPU emitters)
|
||||
src/port/<plat>/ per-platform HAL implementations
|
||||
src/shared68k/ assembly shared by Amiga and Atari ST
|
||||
tools/joeytool/ asset pipeline and packaging
|
||||
tools/joeyasset/ sprite and tile asset converter
|
||||
tools/joeymod/ Protracker .MOD converter (passthrough or .NTP)
|
||||
examples/ example programs
|
||||
toolchains/ self-contained cross-build tools
|
||||
make/ per-target Makefile fragments
|
||||
|
|
|
|||
BIN
assets/test.mod
(Stored with Git LFS)
Normal file
BIN
assets/test.mod
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/test.sfx
(Stored with Git LFS)
Normal file
BIN
assets/test.sfx
(Stored with Git LFS)
Normal file
Binary file not shown.
120
examples/audio/audio.c
Normal file
120
examples/audio/audio.c
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// Audio demo: starts an embedded .MOD on entry, triggers a short
|
||||
// digital SFX every time SPACE is tapped, and exits on ESC. The MOD
|
||||
// and SFX bytes live in test_assets.h, generated from assets/test.mod
|
||||
// and assets/test.sfx by the audio-asset build pipeline.
|
||||
//
|
||||
// On platforms where the audio HAL is still a stub (DOS / ST / IIgs
|
||||
// at the moment), joeyAudioInit returns false, every audio call is a
|
||||
// quiet no-op, and the demo runs as a silent input loop -- you can
|
||||
// confirm the build links and the input plumbing still works without
|
||||
// hearing anything.
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <joey/joey.h>
|
||||
|
||||
#include "test_assets.h"
|
||||
|
||||
#define SFX_SLOT 0
|
||||
#define SFX_RATE_HZ 8000
|
||||
|
||||
#define COLOR_BG 0
|
||||
#define COLOR_HINT 1
|
||||
#define COLOR_BAR 2
|
||||
|
||||
#define BAR_X 16
|
||||
#define BAR_Y 88
|
||||
#define BAR_W (SURFACE_WIDTH - 32)
|
||||
#define BAR_H 16
|
||||
|
||||
|
||||
static void buildPalette(SurfaceT *screen) {
|
||||
uint16_t colors[SURFACE_COLORS_PER_PALETTE];
|
||||
uint16_t i;
|
||||
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
colors[i] = 0x0000;
|
||||
}
|
||||
colors[COLOR_BG] = 0x0000;
|
||||
colors[COLOR_HINT] = 0x0444;
|
||||
colors[COLOR_BAR] = 0x00F0;
|
||||
|
||||
paletteSet(screen, 0, colors);
|
||||
}
|
||||
|
||||
|
||||
// Visual feedback for "audio is running, but you cannot tell on a
|
||||
// stub HAL": pulse a horizontal bar between the hint color and the
|
||||
// active color whenever SFX has fired this frame.
|
||||
static void initialPaint(SurfaceT *screen, bool audioOk) {
|
||||
surfaceClear(screen, COLOR_BG);
|
||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H,
|
||||
audioOk ? COLOR_HINT : COLOR_BG);
|
||||
surfacePresent(screen);
|
||||
}
|
||||
|
||||
|
||||
int main(void) {
|
||||
JoeyConfigT config;
|
||||
SurfaceT *screen;
|
||||
bool audioOk;
|
||||
int16_t flashFrames;
|
||||
|
||||
config.hostMode = HOST_MODE_TAKEOVER;
|
||||
config.codegenBytes = 32 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64 * 1024;
|
||||
config.assetBytes = 128 * 1024;
|
||||
|
||||
if (!joeyInit(&config)) {
|
||||
fprintf(stderr, "joeyInit failed: %s\n", joeyLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
screen = surfaceGetScreen();
|
||||
if (screen == NULL) {
|
||||
fprintf(stderr, "surfaceGetScreen returned NULL\n");
|
||||
joeyShutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
audioOk = joeyAudioInit();
|
||||
if (audioOk) {
|
||||
joeyAudioPlayMod(gTestMod, gTestMod_len, true);
|
||||
}
|
||||
|
||||
buildPalette(screen);
|
||||
scbSetRange(screen, 0, SURFACE_HEIGHT - 1, 0);
|
||||
initialPaint(screen, audioOk);
|
||||
|
||||
flashFrames = 0;
|
||||
for (;;) {
|
||||
joeyWaitVBL();
|
||||
joeyInputPoll();
|
||||
joeyAudioFrameTick();
|
||||
if (joeyKeyPressed(KEY_ESCAPE)) {
|
||||
break;
|
||||
}
|
||||
if (joeyKeyPressed(KEY_SPACE)) {
|
||||
joeyAudioPlaySfx(SFX_SLOT, gTestSfx, gTestSfx_len, SFX_RATE_HZ);
|
||||
flashFrames = 8;
|
||||
}
|
||||
|
||||
if (flashFrames > 0) {
|
||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_BAR);
|
||||
surfacePresentRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H);
|
||||
flashFrames--;
|
||||
if (flashFrames == 0) {
|
||||
fillRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H, COLOR_HINT);
|
||||
surfacePresentRect(screen, BAR_X, BAR_Y, BAR_W, BAR_H);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (audioOk) {
|
||||
joeyAudioStopMod();
|
||||
joeyAudioShutdown();
|
||||
}
|
||||
joeyShutdown();
|
||||
return 0;
|
||||
}
|
||||
169
examples/audio/test_assets.h
Normal file
169
examples/audio/test_assets.h
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// Generated by examples/audio/build (do not edit by hand).
|
||||
// Source assets: assets/test.mod, assets/test.sfx.
|
||||
//
|
||||
// .MOD is consumed by Amiga/DOS/ST audio HALs directly. The IIgs
|
||||
// HAL takes a .NTP blob -- a separate header for the IIgs build
|
||||
// will be generated by joeymod once the IIgs port is wired up.
|
||||
|
||||
#ifndef JOEY_AUDIO_TEST_ASSETS_H
|
||||
#define JOEY_AUDIO_TEST_ASSETS_H
|
||||
|
||||
static const unsigned char gTestMod[] = {
|
||||
0x4a, 0x6f, 0x65, 0x79, 0x41, 0x75, 0x64, 0x69, 0x6f, 0x54, 0x65, 0x73, 0x74, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x73, 0x71, 0x75, 0x61, 0x72, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x40, 0x00, 0x00,
|
||||
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x2e, 0x4b, 0x2e, 0x03, 0x58, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xfa, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xa6, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x80, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x58, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xa6, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x80, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xfa, 0x10, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x7f, 0x7f, 0x7f,
|
||||
0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x80, 0x80, 0x80, 0x80,
|
||||
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
|
||||
};
|
||||
static const unsigned int gTestMod_len = 2140;
|
||||
|
||||
static const unsigned char gTestSfx[] = {
|
||||
0x00, 0x31, 0x5a, 0x74, 0x7c, 0x71, 0x53, 0x29, 0xf9, 0xca, 0xa4, 0x8d, 0x88, 0x96, 0xb5, 0xdf,
|
||||
0x0d, 0x3a, 0x5d, 0x71, 0x73, 0x63, 0x43, 0x19, 0xec, 0xc2, 0xa2, 0x91, 0x92, 0xa4, 0xc4, 0xee,
|
||||
0x19, 0x41, 0x5e, 0x6c, 0x69, 0x55, 0x34, 0x0b, 0xe2, 0xbc, 0xa2, 0x97, 0x9d, 0xb2, 0xd3, 0xfb,
|
||||
0x23, 0x46, 0x5d, 0x65, 0x5d, 0x47, 0x26, 0x00, 0xd9, 0xb9, 0xa5, 0x9f, 0xa8, 0xc0, 0xe1, 0x06,
|
||||
0x2a, 0x48, 0x59, 0x5d, 0x52, 0x39, 0x18, 0xf5, 0xd3, 0xb8, 0xa9, 0xa8, 0xb5, 0xcd, 0xee, 0x10,
|
||||
0x30, 0x48, 0x54, 0x53, 0x45, 0x2c, 0x0d, 0xec, 0xcf, 0xb9, 0xaf, 0xb2, 0xc1, 0xda, 0xf9, 0x17,
|
||||
0x33, 0x46, 0x4e, 0x49, 0x39, 0x20, 0x03, 0xe6, 0xcd, 0xbc, 0xb6, 0xbc, 0xcd, 0xe5, 0x01, 0x1d,
|
||||
0x34, 0x42, 0x46, 0x3f, 0x2d, 0x15, 0xfb, 0xe1, 0xcd, 0xc0, 0xbe, 0xc7, 0xd8, 0xef, 0x08, 0x20,
|
||||
0x33, 0x3d, 0x3d, 0x34, 0x23, 0x0c, 0xf5, 0xdf, 0xcf, 0xc6, 0xc7, 0xd1, 0xe2, 0xf8, 0x0e, 0x21,
|
||||
0x30, 0x36, 0x34, 0x2a, 0x19, 0x04, 0xf0, 0xdf, 0xd2, 0xcd, 0xd1, 0xdb, 0xec, 0xff, 0x11, 0x21,
|
||||
0x2b, 0x2f, 0x2a, 0x20, 0x10, 0xff, 0xee, 0xe0, 0xd7, 0xd5, 0xda, 0xe5, 0xf4, 0x03, 0x12, 0x1f,
|
||||
0x26, 0x26, 0x21, 0x17, 0x09, 0xfb, 0xee, 0xe3, 0xde, 0xde, 0xe3, 0xed, 0xfa, 0x06, 0x12, 0x1b,
|
||||
0x1f, 0x1e, 0x18, 0x0f, 0x04, 0xf9, 0xef, 0xe8, 0xe5, 0xe6, 0xec, 0xf4, 0xfe, 0x07, 0x10, 0x15,
|
||||
0x17, 0x15, 0x10, 0x09, 0x00, 0xf9, 0xf2, 0xee, 0xed, 0xef, 0xf3, 0xfa, 0x00, 0x07, 0x0c, 0x0f,
|
||||
0x0f, 0x0d, 0x09, 0x04, 0xff, 0xfa, 0xf6, 0xf5, 0xf5, 0xf7, 0xfa, 0xfe, 0x01, 0x04, 0x07, 0x08,
|
||||
0x07, 0x06, 0x03, 0x01, 0xff, 0xfd, 0xfc, 0xfc, 0xfd, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
static const unsigned int gTestSfx_len = 256;
|
||||
|
||||
#endif
|
||||
73
include/joey/audio.h
Normal file
73
include/joey/audio.h
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Audio: 4-channel Protracker .MOD music with digital one-shot SFX.
|
||||
//
|
||||
// Authors compose music as .MOD modules. The host-side asset pipeline
|
||||
// (tools/joeymod) converts each module into the runtime form the
|
||||
// target platform expects:
|
||||
//
|
||||
// Apple IIgs -- NinjaTrackerPlus .NTP, played by Ninjaforce's
|
||||
// 65816 replayer linked into the binary.
|
||||
// Amiga -- raw .MOD, played by Frank Wille's PTPlayer.
|
||||
// DOS -- raw .MOD, played by libxmp-lite over SB DMA.
|
||||
// Atari ST -- raw .MOD, played by libxmp-lite (parser only) plus
|
||||
// a hand-rolled 68k 4-channel mixer that outputs via
|
||||
// YM2149 4-bit PWM at ~12.5 kHz.
|
||||
//
|
||||
// Game code always calls the same five entry points; the per-port
|
||||
// HAL hides the engine choice. A failed audio init is non-fatal --
|
||||
// joeyInit still succeeds and audio calls become no-ops.
|
||||
|
||||
#ifndef JOEYLIB_AUDIO_H
|
||||
#define JOEYLIB_AUDIO_H
|
||||
|
||||
#include "platform.h"
|
||||
#include "types.h"
|
||||
|
||||
#define JOEY_AUDIO_SFX_SLOTS 4
|
||||
|
||||
// Initialize audio. Returns true if the platform has a working audio
|
||||
// engine and was able to start it. Returns false silently otherwise;
|
||||
// the rest of the API stays callable but produces no sound.
|
||||
bool joeyAudioInit(void);
|
||||
|
||||
// Tear down the engine. Safe to call when audio is not initialized.
|
||||
void joeyAudioShutdown(void);
|
||||
|
||||
// Begin module playback. data points at the platform-native module
|
||||
// blob (.MOD on most platforms, .NTP on IIgs); the asset pipeline
|
||||
// produces the right form for each target. If a module is already
|
||||
// playing, it is replaced.
|
||||
//
|
||||
// loop=true plays forever. loop=false stops at song end, but Amiga
|
||||
// requires the module to contain an E8FF effect at song end for the
|
||||
// stop to fire (PTPlayer has no native song-end signal). The
|
||||
// `joeymod` tool's .amod output extension injects that marker
|
||||
// automatically; ship .amod for Amiga and .mod for the other ports.
|
||||
// loop=false on a .mod (no E8FF) loops anyway on Amiga.
|
||||
void joeyAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
|
||||
|
||||
// Stop the current module (if any). The playhead is reset so the next
|
||||
// joeyAudioPlayMod starts from the top.
|
||||
void joeyAudioStopMod(void);
|
||||
|
||||
// True if a module is currently producing output (false during silence
|
||||
// after StopMod or before the first PlayMod, and on platforms where
|
||||
// audio init failed).
|
||||
bool joeyAudioIsPlayingMod(void);
|
||||
|
||||
// Trigger a one-shot digital SFX on the given slot (0..JOEY_AUDIO_SFX_SLOTS-1).
|
||||
// sample points at raw signed 8-bit PCM. rateHz is the playback rate
|
||||
// the sample was recorded at; the engine pitches as needed for its
|
||||
// own output rate. If the slot is currently playing, the new sample
|
||||
// replaces it.
|
||||
void joeyAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz);
|
||||
|
||||
// Stop a SFX slot early. No-op if the slot is already idle.
|
||||
void joeyAudioStopSfx(uint8_t slot);
|
||||
|
||||
// Hook the engine into the game loop. Most platforms drive their
|
||||
// engines off a hardware IRQ and ignore this call, but it's safe to
|
||||
// invoke once per frame regardless and required for any port that
|
||||
// runs its mixer in user-thread context.
|
||||
void joeyAudioFrameTick(void);
|
||||
|
||||
#endif
|
||||
|
|
@ -15,5 +15,6 @@
|
|||
#include "draw.h"
|
||||
#include "present.h"
|
||||
#include "input.h"
|
||||
#include "audio.h"
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -7,8 +7,21 @@ BUILD := $(REPO_DIR)/build/$(PLATFORM)
|
|||
LIBDIR := $(BUILD)/lib
|
||||
BINDIR := $(BUILD)/bin
|
||||
|
||||
CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA -m68000 -fomit-frame-pointer -noixemul
|
||||
ASFLAGS := -Felf -m68000 -quiet
|
||||
# PTPlayer is staged by toolchains/install.sh into PTPLAYER_DIR; we
|
||||
# reference its ptplayer.asm + header from there rather than copying
|
||||
# them into the source tree, so install.sh can refresh / version it
|
||||
# independently. -I on $(SRC_PORT)/amiga lets ptplayer.h resolve
|
||||
# <SDI_compiler.h> from the port-local shim alongside our HAL code.
|
||||
PTPLAYER_DIR := $(REPO_DIR)/toolchains/amiga/ptplayer
|
||||
CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_AMIGA -m68000 -fomit-frame-pointer -noixemul -D__OSCOMPAT -I$(SRC_PORT)/amiga -I$(PTPLAYER_DIR)
|
||||
# OSCOMPAT=1 selects PTPlayer's audio.device-friendly variant (uses
|
||||
# CIA-B + audio.device interrupts via the OS rather than taking over
|
||||
# Paula directly), matching the way our HAL cooperates with Intuition.
|
||||
# -Fhunk matches Bebbo gcc-amigaos's AmigaOS HUNK object format. ELF
|
||||
# objects from vasm cannot be linked into a HUNK libjoey.a (the ar
|
||||
# "file format not recognized" failure mode is silent at archive time
|
||||
# and surfaces as undefined references at every binary's link step).
|
||||
ASFLAGS := -Fhunk -m68000 -quiet -DOSCOMPAT=1
|
||||
# --allow-multiple-definition lets our user-space tzset stub
|
||||
# (src/port/amiga/libinit.c) win over libnix's version in
|
||||
# __gmtoffset.o. libnix's tzset dereferences a possibly-NULL
|
||||
|
|
@ -19,11 +32,18 @@ PORT_C_SRCS := $(wildcard $(SRC_PORT)/amiga/*.c)
|
|||
PORT_S_SRCS := $(wildcard $(SRC_PORT)/amiga/*.s)
|
||||
SHARED_S := $(wildcard $(SRC_68K)/*.s)
|
||||
|
||||
# Amiga uses PTPlayer's mt_playfx for SFX, not the libxmp+overlay
|
||||
# combo that DOS and ST share, so the shared SFX overlay mixer in
|
||||
# src/core/audioSfxMix.c is unused here. Filter it out to keep dead
|
||||
# code out of every Amiga binary.
|
||||
CORE_C_SRCS_AMIGA := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
|
||||
|
||||
LIB_OBJS := \
|
||||
$(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS)) \
|
||||
$(patsubst $(SRC_CORE)/%.c,$(BUILD)/obj/core/%.o,$(CORE_C_SRCS_AMIGA)) \
|
||||
$(patsubst $(SRC_PORT)/amiga/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
|
||||
$(patsubst $(SRC_PORT)/amiga/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
|
||||
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S))
|
||||
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
|
||||
$(BUILD)/obj/port/ptplayer.o
|
||||
|
||||
LIB := $(LIBDIR)/libjoey.a
|
||||
|
||||
|
|
@ -37,9 +57,11 @@ JOY_SRC := $(EXAMPLES)/joy/joy.c
|
|||
JOY_BIN := $(BINDIR)/Joy
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
SPRITE_BIN := $(BINDIR)/Sprite
|
||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||
AUDIO_BIN := $(BINDIR)/Audio
|
||||
|
||||
.PHONY: all amiga clean-amiga
|
||||
all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN)
|
||||
all amiga: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN)
|
||||
|
||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -53,6 +75,10 @@ $(BUILD)/obj/port/%.o: $(SRC_PORT)/amiga/%.s
|
|||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_AS) $(ASFLAGS) $< -o $@
|
||||
|
||||
$(BUILD)/obj/port/ptplayer.o: $(PTPLAYER_DIR)/ptplayer.asm
|
||||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_AS) $(ASFLAGS) $< -o $@
|
||||
|
||||
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
|
||||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_AS) $(ASFLAGS) $< -o $@
|
||||
|
|
@ -81,5 +107,9 @@ $(SPRITE_BIN): $(SPRITE_SRC) $(LIB)
|
|||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
|
||||
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(AMIGA_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
|
||||
clean-amiga:
|
||||
rm -rf $(BUILD)
|
||||
|
|
|
|||
|
|
@ -7,10 +7,27 @@ BUILD := $(REPO_DIR)/build/$(PLATFORM)
|
|||
LIBDIR := $(BUILD)/lib
|
||||
BINDIR := $(BUILD)/bin
|
||||
|
||||
CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST -m68000 -fomit-frame-pointer
|
||||
ASFLAGS := -Felf -m68000 -quiet
|
||||
CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_ATARIST -m68000 -fomit-frame-pointer -I$(REPO_DIR)/toolchains/audio/libxmp-lite/include -I$(REPO_DIR)/toolchains/atarist/include-shim
|
||||
# m68k-atari-mint gcc emits a.out (.o files report "a.out SunOS"),
|
||||
# not ELF. vasm has to match or `ld: file in wrong format` fires at
|
||||
# link time.
|
||||
ASFLAGS := -Faout -m68000 -quiet
|
||||
LDFLAGS :=
|
||||
|
||||
# libxmp-lite shared with the DOS port. Built as a static archive that
|
||||
# provides the Protracker pattern decoder for the ST audio HAL; the
|
||||
# 68k 4-channel mixer + YM2149 PWM output stage layer on top.
|
||||
LIBXMP_DIR := $(REPO_DIR)/toolchains/audio/libxmp-lite
|
||||
LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c))
|
||||
LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite
|
||||
LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC)) $(LIBXMP_OBJDIR)/math_shim.o
|
||||
LIBXMP_AR := $(LIBDIR)/libxmplite.a
|
||||
# LIBXMP_CORE_DISABLE_IT skips the IT-format filter code so libxmp-lite
|
||||
# doesn't reach for math.h / libm -- mintlib doesn't ship either. We
|
||||
# target MOD as the authoring format anyway, so dropping IT filter
|
||||
# support has no impact on JoeyLib's audio.
|
||||
LIBXMP_CFLAGS := -DLIBXMP_CORE_PLAYER -DLIBXMP_CORE_DISABLE_IT -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-unused-parameter
|
||||
|
||||
PORT_C_SRCS := $(wildcard $(SRC_PORT)/atarist/*.c)
|
||||
PORT_S_SRCS := $(wildcard $(SRC_PORT)/atarist/*.s)
|
||||
SHARED_S := $(wildcard $(SRC_68K)/*.s)
|
||||
|
|
@ -33,9 +50,11 @@ JOY_SRC := $(EXAMPLES)/joy/joy.c
|
|||
JOY_BIN := $(BINDIR)/JOY.PRG
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
SPRITE_BIN := $(BINDIR)/SPRITE.PRG
|
||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||
AUDIO_BIN := $(BINDIR)/AUDIO.PRG
|
||||
|
||||
.PHONY: all atarist clean-atarist
|
||||
all atarist: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN)
|
||||
all atarist: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN)
|
||||
|
||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -57,25 +76,41 @@ $(LIB): $(LIB_OBJS)
|
|||
@mkdir -p $(dir $@)
|
||||
$(ST_AR) rcs $@ $^
|
||||
|
||||
$(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $(LIBXMP_CFLAGS) -c $< -o $@
|
||||
|
||||
$(LIBXMP_OBJDIR)/math_shim.o: $(REPO_DIR)/toolchains/atarist/include-shim/math_shim.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(LIBXMP_AR): $(LIBXMP_OBJS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_AR) rcs $@ $^
|
||||
|
||||
$(HELLO_BIN): $(HELLO_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
$(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
$(KEYS_BIN): $(KEYS_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) -o $@ $(LDFLAGS)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(ST_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@ $(LDFLAGS)
|
||||
|
||||
clean-atarist:
|
||||
rm -rf $(BUILD)
|
||||
|
|
|
|||
40
make/dos.mk
40
make/dos.mk
|
|
@ -7,10 +7,21 @@ BUILD := $(REPO_DIR)/build/$(PLATFORM)
|
|||
LIBDIR := $(BUILD)/lib
|
||||
BINDIR := $(BUILD)/bin
|
||||
|
||||
CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_DOS -march=i386 -m32
|
||||
CFLAGS := $(COMMON_CFLAGS) -DJOEYLIB_PLATFORM_DOS -march=i386 -m32 -I$(REPO_DIR)/toolchains/audio/libxmp-lite/include
|
||||
ASFLAGS := -f coff
|
||||
LDFLAGS :=
|
||||
|
||||
# libxmp-lite (MIT, libxmp.org) -- 4-channel-Protracker-and-friends
|
||||
# replayer + SMIX SFX overlay. Staged at toolchains/audio/libxmp-lite/
|
||||
# (currently checked in; install.sh wiring is a follow-up). Built as
|
||||
# a static archive that the DOS audio HAL links against.
|
||||
LIBXMP_DIR := $(REPO_DIR)/toolchains/audio/libxmp-lite
|
||||
LIBXMP_SRC := $(filter-out %/win32.c, $(wildcard $(LIBXMP_DIR)/src/*.c) $(wildcard $(LIBXMP_DIR)/src/loaders/*.c))
|
||||
LIBXMP_OBJDIR := $(BUILD)/obj/libxmp-lite
|
||||
LIBXMP_OBJS := $(patsubst $(LIBXMP_DIR)/src/%.c,$(LIBXMP_OBJDIR)/%.o,$(LIBXMP_SRC))
|
||||
LIBXMP_AR := $(LIBDIR)/libxmplite.a
|
||||
LIBXMP_CFLAGS := -DJOEY_LIBXMP_LITE -DLIBXMP_CORE_PLAYER -DHAVE_FNMATCH=0 -I$(LIBXMP_DIR)/include -I$(LIBXMP_DIR)/include/libxmp-lite -I$(LIBXMP_DIR)/src -Wno-unused-parameter
|
||||
|
||||
PORT_C_SRCS := $(wildcard $(SRC_PORT)/dos/*.c)
|
||||
PORT_S_SRCS := $(wildcard $(SRC_PORT)/dos/*.asm)
|
||||
|
||||
|
|
@ -31,9 +42,11 @@ JOY_SRC := $(EXAMPLES)/joy/joy.c
|
|||
JOY_BIN := $(BINDIR)/JOY.EXE
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
SPRITE_BIN := $(BINDIR)/SPRITE.EXE
|
||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||
AUDIO_BIN := $(BINDIR)/AUDIO.EXE
|
||||
|
||||
.PHONY: all dos clean-dos
|
||||
all dos: $(LIB) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN)
|
||||
all dos: $(LIB) $(LIBXMP_AR) $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN)
|
||||
|
||||
$(BUILD)/obj/core/%.o: $(SRC_CORE)/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
|
|
@ -51,29 +64,42 @@ $(LIB): $(LIB_OBJS)
|
|||
@mkdir -p $(dir $@)
|
||||
$(DOS_AR) rcs $@ $^
|
||||
|
||||
$(LIBXMP_OBJDIR)/%.o: $(LIBXMP_DIR)/src/%.c
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $(LIBXMP_CFLAGS) -c $< -o $@
|
||||
|
||||
$(LIBXMP_AR): $(LIBXMP_OBJS)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_AR) rcs $@ $^
|
||||
|
||||
$(HELLO_BIN): $(HELLO_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) -o $@
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(PATTERN_BIN): $(PATTERN_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) -o $@
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(KEYS_BIN): $(KEYS_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) -o $@
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) -o $@
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) -o $@
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB)
|
||||
@mkdir -p $(dir $@)
|
||||
$(DOS_CC) $(CFLAGS) $< $(LIB) $(LIBXMP_AR) -o $@
|
||||
$(DOS_EMBED_DPMI) $@
|
||||
|
||||
clean-dos:
|
||||
|
|
|
|||
92
make/iigs.mk
92
make/iigs.mk
|
|
@ -12,9 +12,36 @@ PLATFORM := iigs
|
|||
BUILD := $(REPO_DIR)/build/$(PLATFORM)
|
||||
BINDIR := $(BUILD)/bin
|
||||
|
||||
PORT_C_SRCS := $(wildcard $(SRC_PORT)/iigs/*.c)
|
||||
PORT_C_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.c)
|
||||
|
||||
LIB_SRCS := $(CORE_C_SRCS) $(PORT_C_SRCS)
|
||||
# audio.c is the no-op stub linked into every demo. audio_full.c is the
|
||||
# real implementation (NewHandle / fopen / JSL trampoline) and links
|
||||
# only into AUDIO -- the IIgs build is monolithic, so pulling Memory
|
||||
# Manager + ORCA stdio into every binary blows the linker's
|
||||
# "Expression too complex" budget. The two files define the same
|
||||
# halAudio* symbols; iigs/audio.c is filtered out of the AUDIO source
|
||||
# set, audio_full.c is filtered out of the everyone-else set.
|
||||
PORT_C_SRCS := $(filter-out %/audio_full.c, $(PORT_C_SRCS_ALL))
|
||||
PORT_C_SRCS_AUDIO := $(filter-out %/audio.c, $(PORT_C_SRCS_ALL))
|
||||
|
||||
# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that
|
||||
# DOS and ST share, so src/core/audioSfxMix.c is unused here. Filter
|
||||
# it out so the ORCA Linker doesn't pull its dead code into every
|
||||
# IIgs binary (the monolithic-link budget is tight).
|
||||
CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
|
||||
|
||||
LIB_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS)
|
||||
LIB_SRCS_AUDIO := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS_AUDIO)
|
||||
|
||||
# NinjaTrackerPlus replayer. Assembled with Merlin32 from the staged
|
||||
# source at toolchains/iigs/ntp/ninjatrackerplus.s. Output is a 34 KB
|
||||
# raw 65816 binary that the IIgs audio HAL loads at runtime via
|
||||
# Memory Manager into a fixed-bank handle. NTP source is bank-internal
|
||||
# (no JSL/JML cross-bank jumps) so the player works at any bank-start
|
||||
# load address even though it was assembled with `org $0F0000`.
|
||||
NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
|
||||
NTP_BIN := $(BUILD)/audio/ntpplayer.bin
|
||||
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
|
||||
|
||||
HELLO_SRC := $(EXAMPLES)/hello/hello.c
|
||||
HELLO_BIN := $(BINDIR)/HELLO
|
||||
|
|
@ -26,6 +53,13 @@ JOY_SRC := $(EXAMPLES)/joy/joy.c
|
|||
JOY_BIN := $(BINDIR)/JOY
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
SPRITE_BIN := $(BINDIR)/SPRITE
|
||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||
AUDIO_BIN := $(BINDIR)/AUDIO
|
||||
AUDIO_MOD := $(REPO_DIR)/assets/test.mod
|
||||
AUDIO_SFX := $(REPO_DIR)/assets/test.sfx
|
||||
AUDIO_NTP := $(BUILD)/audio/test.ntp
|
||||
AUDIO_HEADER := $(BUILD)/audio/test_assets.h
|
||||
JOEYMOD := $(REPO_DIR)/tools/joeymod/joeymod
|
||||
DISK_IMG := $(BINDIR)/joey.2mg
|
||||
|
||||
IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh
|
||||
|
|
@ -37,7 +71,13 @@ IIX_INCLUDES := \
|
|||
-I $(SRC_CORE)
|
||||
|
||||
.PHONY: all iigs iigs-disk clean-iigs
|
||||
all iigs: $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN)
|
||||
all iigs: $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(NTP_BIN)
|
||||
|
||||
$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
|
||||
@mkdir -p $(dir $@)
|
||||
@cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
|
||||
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
|
||||
mv $(BUILD)/audio/ntpplayer $@
|
||||
|
||||
# iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with
|
||||
# #pragma noroot). The example source supplies main(); libjoey sources
|
||||
|
|
@ -69,13 +109,55 @@ $(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_BUILD)
|
|||
$(IIGS_BUILD) $(IIX_INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
|
||||
# IIgs override of test_assets.h: gTestMod[] holds .NTP-converted bytes
|
||||
# (NinjaTrackerPlus runtime format) instead of raw .MOD. The SFX bytes
|
||||
# are platform-independent so we just embed the same test.sfx.
|
||||
#
|
||||
# .NTP conversion runs joeymod (which shells out to ntpconverter.php).
|
||||
# If PHP is missing we skip the override entirely and the regular .MOD
|
||||
# bytes get linked instead -- the IIgs binary still builds and the
|
||||
# trampoline still calls into NTP, but NTPprepare will reject the MOD
|
||||
# magic at runtime. Install php-cli to get real audio playback.
|
||||
HAVE_PHP := $(shell command -v php >/dev/null 2>&1 && echo 1)
|
||||
|
||||
ifeq ($(HAVE_PHP),1)
|
||||
$(AUDIO_NTP): $(AUDIO_MOD) $(JOEYMOD)
|
||||
@mkdir -p $(dir $@)
|
||||
$(JOEYMOD) $< $@
|
||||
|
||||
$(AUDIO_HEADER): $(AUDIO_NTP) $(AUDIO_SFX)
|
||||
@mkdir -p $(dir $@)
|
||||
@echo "// Generated by make/iigs.mk -- gTestMod is .NTP, gTestSfx is raw PCM." > $@
|
||||
@echo "#ifndef JOEY_AUDIO_TEST_ASSETS_H" >> $@
|
||||
@echo "#define JOEY_AUDIO_TEST_ASSETS_H" >> $@
|
||||
@printf "static const unsigned char gTestMod[] = {\n" >> $@
|
||||
@xxd -i < $(AUDIO_NTP) >> $@
|
||||
@printf "};\nstatic const unsigned int gTestMod_len = %d;\n" $$(wc -c < $(AUDIO_NTP)) >> $@
|
||||
@printf "static const unsigned char gTestSfx[] = {\n" >> $@
|
||||
@xxd -i < $(AUDIO_SFX) >> $@
|
||||
@printf "};\nstatic const unsigned int gTestSfx_len = %d;\n" $$(wc -c < $(AUDIO_SFX)) >> $@
|
||||
@echo "#endif" >> $@
|
||||
|
||||
AUDIO_HEADER_DEP := $(AUDIO_HEADER)
|
||||
AUDIO_HEADER_FLAGS := -I $(dir $(AUDIO_HEADER))
|
||||
else
|
||||
$(info iigs: php-cli not installed -- AUDIO demo will embed raw .MOD bytes; install php-cli for real IIgs audio playback)
|
||||
AUDIO_HEADER_DEP :=
|
||||
AUDIO_HEADER_FLAGS :=
|
||||
endif
|
||||
|
||||
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB_SRCS_AUDIO) $(AUDIO_HEADER_DEP) $(IIGS_BUILD)
|
||||
@mkdir -p $(dir $@)
|
||||
$(IIGS_BUILD) -b $(IIX_INCLUDES) $(AUDIO_HEADER_FLAGS) -I $(EXAMPLES)/audio -o $@ $(AUDIO_SRC) $(LIB_SRCS_AUDIO)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
|
||||
# Assemble an 800KB ProDOS 2img containing the examples, ready to
|
||||
# mount in GSplus alongside a GS/OS boot volume.
|
||||
iigs-disk: $(DISK_IMG)
|
||||
|
||||
$(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(IIGS_PACKAGE)
|
||||
$(DISK_IMG): $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(NTP_BIN) $(IIGS_PACKAGE)
|
||||
@mkdir -p $(dir $@)
|
||||
$(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN)
|
||||
$(IIGS_PACKAGE) $@ $(HELLO_BIN) $(PATTERN_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(NTP_BIN)
|
||||
|
||||
clean-iigs:
|
||||
rm -rf $(BUILD)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
# These build with the host's default `cc` (no cross-toolchain) into
|
||||
# build/tools/. Currently:
|
||||
# joeyasset -- PPM (P6) -> .jas converter
|
||||
# modloopend -- inject an E8FF stop-marker pattern into a .MOD so
|
||||
# the Amiga audio HAL can honor PlayMod(loop=false).
|
||||
# joeymod invokes this transparently for Amiga output.
|
||||
|
||||
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
|
||||
|
||||
|
|
@ -14,13 +17,19 @@ HOST_CFLAGS := -std=c99 -Wall -Wextra -O2
|
|||
|
||||
JOEYASSET_SRC := $(TOOLS_DIR)/joeyasset/joeyasset.c
|
||||
JOEYASSET_BIN := $(BUILD_DIR)/joeyasset
|
||||
MODLOOPEND_SRC := $(TOOLS_DIR)/modloopend/modloopend.c
|
||||
MODLOOPEND_BIN := $(BUILD_DIR)/modloopend
|
||||
|
||||
.PHONY: all tools clean-tools
|
||||
all tools: $(JOEYASSET_BIN)
|
||||
all tools: $(JOEYASSET_BIN) $(MODLOOPEND_BIN)
|
||||
|
||||
$(JOEYASSET_BIN): $(JOEYASSET_SRC)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) $(HOST_CFLAGS) $< -o $@
|
||||
|
||||
$(MODLOOPEND_BIN): $(MODLOOPEND_SRC)
|
||||
@mkdir -p $(dir $@)
|
||||
$(HOST_CC) $(HOST_CFLAGS) $< -o $@
|
||||
|
||||
clean-tools:
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ case $prog in
|
|||
keys) file=Keys ;;
|
||||
joy) file=Joy ;;
|
||||
sprite) file=Sprite ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&2; exit 2 ;;
|
||||
audio) file=Audio ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "$bin_dir/$file" ]]; then
|
||||
|
|
@ -60,6 +61,7 @@ cp "$bin_dir/Pattern" "$work/" 2>/dev/null || true
|
|||
cp "$bin_dir/Keys" "$work/" 2>/dev/null || true
|
||||
cp "$bin_dir/Joy" "$work/" 2>/dev/null || true
|
||||
cp "$bin_dir/Sprite" "$work/" 2>/dev/null || true
|
||||
cp "$bin_dir/Audio" "$work/" 2>/dev/null || true
|
||||
# ':' prefix anchors to the root of the current volume; otherwise
|
||||
# AmigaDOS looks in C: and the command is not found.
|
||||
echo ":$file" > "$work/s/startup-sequence"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ case $prog in
|
|||
keys) file=KEYS.PRG ;;
|
||||
joy) file=JOY.PRG ;;
|
||||
sprite) file=SPRITE.PRG ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&2; exit 2 ;;
|
||||
audio) file=AUDIO.PRG ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
tos=$repo/toolchains/emulators/support/emutos-512k.img
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ case $prog in
|
|||
keys) file=KEYS.EXE ;;
|
||||
joy) file=JOY.EXE ;;
|
||||
sprite) file=SPRITE.EXE ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&2; exit 2 ;;
|
||||
audio) file=AUDIO.EXE ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "$bin_dir/$file" ]]; then
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
# scripts/run-iigs.sh keys # boots, hints KEYS
|
||||
# scripts/run-iigs.sh joy # boots, hints JOY
|
||||
# scripts/run-iigs.sh sprite # boots, hints SPRITE
|
||||
# scripts/run-iigs.sh audio # boots, hints AUDIO
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -27,8 +28,8 @@ data_disk=$repo/build/iigs/bin/joey.2mg
|
|||
null_c600=$repo/toolchains/emulators/support/iigs-null-c600.rom
|
||||
|
||||
case $prog in
|
||||
hello|pattern|keys|joy|sprite) ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite]" >&2; exit 2 ;;
|
||||
hello|pattern|keys|joy|sprite|audio) ;;
|
||||
*) echo "usage: $0 [hello|pattern|keys|joy|sprite|audio]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
for f in "$gsplus" "$rom" "$sys_disk" "$data_disk" "$null_c600"; do
|
||||
|
|
|
|||
80
src/core/audio.c
Normal file
80
src/core/audio.c
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
// Public audio API: thin pass-through to the per-port HAL plus a
|
||||
// gate that makes every call safe when audio init failed (or has not
|
||||
// been called). Game code can fire-and-forget calls regardless of
|
||||
// whether the platform actually has audio working at runtime.
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "joey/audio.h"
|
||||
#include "hal.h"
|
||||
|
||||
static bool gAudioReady = false;
|
||||
|
||||
|
||||
bool joeyAudioInit(void) {
|
||||
if (gAudioReady) {
|
||||
return true;
|
||||
}
|
||||
gAudioReady = halAudioInit();
|
||||
return gAudioReady;
|
||||
}
|
||||
|
||||
|
||||
void joeyAudioShutdown(void) {
|
||||
if (!gAudioReady) {
|
||||
return;
|
||||
}
|
||||
halAudioShutdown();
|
||||
gAudioReady = false;
|
||||
}
|
||||
|
||||
|
||||
void joeyAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
||||
if (!gAudioReady || data == NULL || length == 0) {
|
||||
return;
|
||||
}
|
||||
halAudioPlayMod(data, length, loop);
|
||||
}
|
||||
|
||||
|
||||
void joeyAudioStopMod(void) {
|
||||
if (!gAudioReady) {
|
||||
return;
|
||||
}
|
||||
halAudioStopMod();
|
||||
}
|
||||
|
||||
|
||||
bool joeyAudioIsPlayingMod(void) {
|
||||
if (!gAudioReady) {
|
||||
return false;
|
||||
}
|
||||
return halAudioIsPlayingMod();
|
||||
}
|
||||
|
||||
|
||||
void joeyAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
|
||||
if (!gAudioReady || sample == NULL || length == 0) {
|
||||
return;
|
||||
}
|
||||
if (slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
halAudioPlaySfx(slot, sample, length, rateHz);
|
||||
}
|
||||
|
||||
|
||||
void joeyAudioStopSfx(uint8_t slot) {
|
||||
if (!gAudioReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
halAudioStopSfx(slot);
|
||||
}
|
||||
|
||||
|
||||
void joeyAudioFrameTick(void) {
|
||||
if (!gAudioReady) {
|
||||
return;
|
||||
}
|
||||
halAudioFrameTick();
|
||||
}
|
||||
53
src/core/audioSfxMix.c
Normal file
53
src/core/audioSfxMix.c
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Shared SFX overlay mixer. See audioSfxMixInternal.h for the contract.
|
||||
|
||||
#include "audioSfxMixInternal.h"
|
||||
|
||||
|
||||
void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) {
|
||||
AudioSfxSlotT *slot;
|
||||
uint32_t pos;
|
||||
uint32_t step;
|
||||
uint32_t idx;
|
||||
uint16_t i;
|
||||
uint8_t s;
|
||||
int sample;
|
||||
int mixed;
|
||||
|
||||
for (s = 0; s < numSlots; s++) {
|
||||
slot = &slots[s];
|
||||
if (!slot->active) {
|
||||
continue;
|
||||
}
|
||||
pos = slot->posFp;
|
||||
step = slot->stepFp;
|
||||
for (i = 0; i < len; i++) {
|
||||
idx = pos >> 16;
|
||||
if (idx >= slot->length) {
|
||||
slot->active = false;
|
||||
break;
|
||||
}
|
||||
sample = (int)(int8_t)slot->sample[idx];
|
||||
mixed = (int)dst[i] - 128 + sample;
|
||||
if (mixed > 127) {
|
||||
mixed = 127;
|
||||
} else if (mixed < -128) {
|
||||
mixed = -128;
|
||||
}
|
||||
dst[i] = (uint8_t)(mixed + 128);
|
||||
pos += step;
|
||||
}
|
||||
slot->posFp = pos;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate) {
|
||||
if (rateHz == 0 || outputRate == 0) {
|
||||
return;
|
||||
}
|
||||
slot->sample = sample;
|
||||
slot->length = length;
|
||||
slot->posFp = 0;
|
||||
slot->stepFp = ((uint32_t)rateHz << 16) / outputRate;
|
||||
slot->active = true;
|
||||
}
|
||||
47
src/core/audioSfxMixInternal.h
Normal file
47
src/core/audioSfxMixInternal.h
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Shared SFX overlay mixer used by HALs whose engine outputs MOD-only
|
||||
// (libxmp-lite on DOS + ST). The mixer overlays one-shot signed 8-bit
|
||||
// PCM samples on top of the engine's already-rendered unsigned 8-bit
|
||||
// MOD output, with a 16.16 fixed-point step so the SFX rate can
|
||||
// differ from the output rate without needing an up-front resampling
|
||||
// pass.
|
||||
//
|
||||
// Not used by Amiga (PTPlayer's mt_playfx handles SFX natively) or
|
||||
// IIgs (NTPstreamsound runs its own DOC-RAM streamer).
|
||||
|
||||
#ifndef JOEYLIB_AUDIO_SFX_MIX_H
|
||||
#define JOEYLIB_AUDIO_SFX_MIX_H
|
||||
|
||||
#include "joey/audio.h"
|
||||
#include "joey/types.h"
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *sample; // signed 8-bit PCM, caller-owned
|
||||
uint32_t length; // sample bytes
|
||||
uint32_t posFp; // 16.16 fixed-point read position
|
||||
uint32_t stepFp; // (rateHz << 16) / outputRate
|
||||
bool active;
|
||||
} AudioSfxSlotT;
|
||||
|
||||
// Arm a slot for one-shot playback. `outputRate` is the HAL's mixer
|
||||
// output rate; the helper computes stepFp from rateHz and outputRate.
|
||||
// Sets `active = true`. No-op if rateHz is 0.
|
||||
void audioSfxSlotArm(AudioSfxSlotT *slot,
|
||||
const uint8_t *sample,
|
||||
uint32_t length,
|
||||
uint16_t rateHz,
|
||||
uint32_t outputRate);
|
||||
|
||||
// Overlay all active slots' contributions on top of `dst[0..len)`.
|
||||
// `dst` is unsigned 8-bit centered at $80. Mix is signed-to-signed
|
||||
// against (dst[i] - 128), clamped at +/-127, then biased back. Slots
|
||||
// whose read position passes the sample end are auto-deactivated.
|
||||
//
|
||||
// dst is `volatile` because the ST HAL's ISR consumes the buffer
|
||||
// from interrupt context; the DOS HAL's dst is non-volatile DMA
|
||||
// memory but the function accepts both via the wider qualifier.
|
||||
void audioSfxOverlayMix(volatile uint8_t *dst,
|
||||
uint16_t len,
|
||||
AudioSfxSlotT *slots,
|
||||
uint8_t numSlots);
|
||||
|
||||
#endif
|
||||
|
|
@ -50,4 +50,18 @@ void halInputPoll(void);
|
|||
// graphics.library WaitTOF, XBIOS Vsync, $C019 polling).
|
||||
void halWaitVBL(void);
|
||||
|
||||
// Audio: per-port engine setup, module + SFX playback, teardown.
|
||||
// halAudioInit returns true if the platform has a working engine.
|
||||
// All entry points are safe to call when init failed -- they become
|
||||
// no-ops. See joey/audio.h for the public API contract that wraps
|
||||
// these.
|
||||
bool halAudioInit(void);
|
||||
void halAudioShutdown(void);
|
||||
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop);
|
||||
void halAudioStopMod(void);
|
||||
bool halAudioIsPlayingMod(void);
|
||||
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz);
|
||||
void halAudioStopSfx(uint8_t slot);
|
||||
void halAudioFrameTick(void);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
15
src/port/amiga/SDI_compiler.h
Normal file
15
src/port/amiga/SDI_compiler.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Minimal SDI_compiler.h shim. The full SDI suite is a multi-compiler
|
||||
// macro layer; PTPlayer.h only needs ASM + REG, both of which collapse
|
||||
// to GCC m68k-amigaos register-binding syntax under Bebbo's compiler.
|
||||
|
||||
#ifndef SDI_COMPILER_H
|
||||
#define SDI_COMPILER_H
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define ASM
|
||||
#define REG(reg, decl) decl __asm(#reg)
|
||||
#else
|
||||
#error "SDI_compiler.h shim: add ASM/REG bindings for this compiler"
|
||||
#endif
|
||||
|
||||
#endif
|
||||
256
src/port/amiga/audio.c
Normal file
256
src/port/amiga/audio.c
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// Amiga audio HAL: PTPlayer (Frank Wille, public domain) drives Paula
|
||||
// for both module playback and one-shot SFX overlay.
|
||||
//
|
||||
// We build PTPlayer in OSCOMPAT mode so it cooperates with audio.device
|
||||
// rather than taking Paula away from the OS -- matches our cooperative-
|
||||
// with-Intuition graphics HAL. mt_install allocates all four Paula
|
||||
// channels via audio.device; mt_init loads a .MOD; mt_Enable=1 starts
|
||||
// playback; mt_playfx triggers a SFX on a free channel (or the worst
|
||||
// channel if all are busy) with priority arbitration.
|
||||
//
|
||||
// PTPlayer expects modules and SFX samples to live in Chip RAM. The
|
||||
// asset pipeline does not yet partition into Chip vs. Fast, so we
|
||||
// AllocMem(MEMF_CHIP) a copy at PlayMod / PlaySfx time and free it at
|
||||
// StopMod / StopSfx (or shutdown).
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <exec/types.h>
|
||||
#include <exec/memory.h>
|
||||
#include <hardware/custom.h>
|
||||
|
||||
#include <proto/exec.h>
|
||||
|
||||
#include "ptplayer.h"
|
||||
#include "hal.h"
|
||||
#include "joey/audio.h"
|
||||
|
||||
extern struct Custom custom;
|
||||
extern UBYTE mt_Enable;
|
||||
extern UBYTE mt_E8Trigger;
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
// Paula's NTSC PAL period for middle-C-ish playback. PTPlayer's SFX
|
||||
// API takes a hardware period (cycles per sample) rather than a
|
||||
// frequency. To convert from a sample's authored Hz: period = clock /
|
||||
// rate, where clock is ~3546895 (PAL) or ~3579545 (NTSC). We default
|
||||
// PAL since that's what our screen mode requests, and clamp to legal
|
||||
// Paula period limits.
|
||||
#define PAULA_CLOCK_PAL 3546895UL
|
||||
#define PAULA_PERIOD_MIN 124 // Paula HW limit
|
||||
#define PAULA_PERIOD_MAX 65535
|
||||
|
||||
#define SFX_VOLUME_MAX 64
|
||||
|
||||
// PTPlayer has no built-in "play once" mode -- songs loop forever
|
||||
// unless the author placed an `E8FF` effect at song end. mt_E8Trigger
|
||||
// reflects the most-recently-seen E8 value; mt_init resets it to 0.
|
||||
// When loop=false on PlayMod, we poll for this sentinel in
|
||||
// halAudioFrameTick and clear mt_Enable when seen.
|
||||
#define PTPLAYER_LOOP_END_MARKER 0xFF
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
typedef struct {
|
||||
UBYTE *samples; // Chip-RAM copy of one SFX sample
|
||||
UWORD lengthWords; // sample length in 16-bit words
|
||||
} SfxSlotT;
|
||||
|
||||
static UBYTE *gModuleChip = NULL; // Chip-RAM copy of the playing module
|
||||
static ULONG gModuleLength = 0; // bytes to FreeMem on swap / shutdown
|
||||
static SfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
|
||||
static bool gInstalled = false;
|
||||
static bool gModPlaying = false;
|
||||
static bool gWantLoopOnce = false; // honor loop=false via E8 marker
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
||||
static UWORD periodForRate(uint16_t rateHz) {
|
||||
unsigned long period;
|
||||
|
||||
if (rateHz == 0) {
|
||||
return 428; // ~middle C, sane default
|
||||
}
|
||||
period = PAULA_CLOCK_PAL / (unsigned long)rateHz;
|
||||
if (period < PAULA_PERIOD_MIN) {
|
||||
period = PAULA_PERIOD_MIN;
|
||||
}
|
||||
if (period > PAULA_PERIOD_MAX) {
|
||||
period = PAULA_PERIOD_MAX;
|
||||
}
|
||||
return (UWORD)period;
|
||||
}
|
||||
|
||||
|
||||
// ----- HAL API (alphabetical) -----
|
||||
|
||||
bool halAudioInit(void) {
|
||||
int i;
|
||||
|
||||
if (gInstalled) {
|
||||
return true;
|
||||
}
|
||||
if (mt_install() == 0) {
|
||||
return false;
|
||||
}
|
||||
mt_Enable = 0;
|
||||
gInstalled = true;
|
||||
gModPlaying = false;
|
||||
gWantLoopOnce = false;
|
||||
gModuleChip = NULL;
|
||||
gModuleLength = 0;
|
||||
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
|
||||
gSfxSlots[i].samples = NULL;
|
||||
gSfxSlots[i].lengthWords = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioShutdown(void) {
|
||||
int i;
|
||||
|
||||
if (!gInstalled) {
|
||||
return;
|
||||
}
|
||||
mt_Enable = 0;
|
||||
mt_end((void *)&custom);
|
||||
mt_remove();
|
||||
gInstalled = false;
|
||||
gModPlaying = false;
|
||||
|
||||
if (gModuleChip != NULL) {
|
||||
FreeMem(gModuleChip, gModuleLength);
|
||||
gModuleChip = NULL;
|
||||
gModuleLength = 0;
|
||||
}
|
||||
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
|
||||
if (gSfxSlots[i].samples != NULL) {
|
||||
FreeMem(gSfxSlots[i].samples, (ULONG)gSfxSlots[i].lengthWords * 2UL);
|
||||
gSfxSlots[i].samples = NULL;
|
||||
gSfxSlots[i].lengthWords = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool halAudioIsPlayingMod(void) {
|
||||
return gModPlaying && mt_Enable != 0;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
||||
UBYTE *chip;
|
||||
|
||||
if (!gInstalled) {
|
||||
return;
|
||||
}
|
||||
mt_Enable = 0;
|
||||
mt_end((void *)&custom);
|
||||
gModPlaying = false;
|
||||
gWantLoopOnce = false;
|
||||
if (gModuleChip != NULL) {
|
||||
FreeMem(gModuleChip, gModuleLength);
|
||||
gModuleChip = NULL;
|
||||
gModuleLength = 0;
|
||||
}
|
||||
|
||||
chip = (UBYTE *)AllocMem((ULONG)length, MEMF_CHIP);
|
||||
if (chip == NULL) {
|
||||
return;
|
||||
}
|
||||
memcpy(chip, data, length);
|
||||
gModuleChip = chip;
|
||||
gModuleLength = (ULONG)length;
|
||||
|
||||
// PTPlayer takes the module pointer in a0, sample-data pointer in
|
||||
// a1 (NULL = samples follow the module header), song position in
|
||||
// d0. mt_init resets mt_E8Trigger to 0 so subsequent halAudioFrame
|
||||
// Tick polling sees a clean state.
|
||||
mt_init((void *)&custom, chip, NULL, 0);
|
||||
mt_Enable = 1;
|
||||
gWantLoopOnce = !loop;
|
||||
gModPlaying = true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
|
||||
SfxStructure sfx;
|
||||
SfxSlotT *s;
|
||||
UBYTE *chip;
|
||||
UWORD words;
|
||||
|
||||
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
if (length < 2 || length > 0x1FFFEUL) {
|
||||
return; // Paula sample length is 16 bits of words = 128KB max
|
||||
}
|
||||
|
||||
s = &gSfxSlots[slot];
|
||||
if (s->samples != NULL) {
|
||||
FreeMem(s->samples, (ULONG)s->lengthWords * 2UL);
|
||||
s->samples = NULL;
|
||||
s->lengthWords = 0;
|
||||
}
|
||||
|
||||
chip = (UBYTE *)AllocMem((ULONG)length, MEMF_CHIP);
|
||||
if (chip == NULL) {
|
||||
return;
|
||||
}
|
||||
memcpy(chip, sample, length);
|
||||
words = (UWORD)(length >> 1);
|
||||
|
||||
s->samples = chip;
|
||||
s->lengthWords = words;
|
||||
|
||||
sfx.sfx_ptr = chip;
|
||||
sfx.sfx_len = words;
|
||||
sfx.sfx_per = periodForRate(rateHz);
|
||||
sfx.sfx_vol = SFX_VOLUME_MAX;
|
||||
sfx.sfx_cha = -1; // best free channel
|
||||
sfx.sfx_pri = (BYTE)(slot + 1); // priority 1..N from slot index
|
||||
mt_playfx((void *)&custom, &sfx);
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopMod(void) {
|
||||
if (!gInstalled) {
|
||||
return;
|
||||
}
|
||||
mt_Enable = 0;
|
||||
mt_end((void *)&custom);
|
||||
gModPlaying = false;
|
||||
gWantLoopOnce = false;
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopSfx(uint8_t slot) {
|
||||
UBYTE i;
|
||||
|
||||
if (!gInstalled || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
// PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx
|
||||
// picks a channel dynamically and does not return which one. We
|
||||
// can't reliably map slot -> channel after the fact, so stop all
|
||||
// four channels' SFX state. mt_stopfx is idempotent on idle
|
||||
// channels, so stopping a quiet one is harmless.
|
||||
for (i = 0; i < 4; i++) {
|
||||
mt_stopfx((void *)&custom, i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void halAudioFrameTick(void) {
|
||||
// PTPlayer drives itself off CIA-B; the only host-loop work is the
|
||||
// play-once watchdog. When the song author placed an `E8FF` at
|
||||
// song end, mt_E8Trigger latches to 0xFF -- if the caller passed
|
||||
// loop=false to PlayMod, that's our cue to stop the song.
|
||||
if (gWantLoopOnce && gModPlaying && mt_E8Trigger == PTPLAYER_LOOP_END_MARKER) {
|
||||
mt_Enable = 0;
|
||||
gModPlaying = false;
|
||||
gWantLoopOnce = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ extern struct Custom custom;
|
|||
// ----- Prototypes -----
|
||||
|
||||
static void buildCopperList(const SurfaceT *src);
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1);
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1, uint16_t byteStart, uint16_t byteEnd);
|
||||
static void dumpCopperList(void);
|
||||
static void installCopperList(void);
|
||||
static void uploadFirstBandPalette(const SurfaceT *src);
|
||||
|
|
@ -84,7 +84,11 @@ static bool paletteOrScbChanged(const SurfaceT *src);
|
|||
// Each plane scanline is 40 bytes (1 bit per pixel x 320 pixels).
|
||||
// For each destination byte, 8 pixels' worth of 4bpp chunky source is
|
||||
// read and split into one bit per plane.
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) {
|
||||
// c2p over rows y0..y1 and planar-byte columns byteStart..byteEnd.
|
||||
// Each planar byte corresponds to 8 horizontal pixels = 4 source
|
||||
// bytes; partial-rect callers should round byteStart down and byteEnd
|
||||
// up to keep the 8-pixel alignment.
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1, uint16_t byteStart, uint16_t byteEnd) {
|
||||
const uint8_t *srcLine;
|
||||
UBYTE *p0;
|
||||
UBYTE *p1;
|
||||
|
|
@ -109,7 +113,7 @@ static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) {
|
|||
p2 = &gPlanes[2][y * AMIGA_BYTES_PER_ROW];
|
||||
p3 = &gPlanes[3][y * AMIGA_BYTES_PER_ROW];
|
||||
|
||||
for (planarByte = 0; planarByte < AMIGA_BYTES_PER_ROW; planarByte++) {
|
||||
for (planarByte = byteStart; planarByte < byteEnd; planarByte++) {
|
||||
b0 = 0;
|
||||
b1 = 0;
|
||||
b2 = 0;
|
||||
|
|
@ -147,7 +151,6 @@ static void buildCopperList(const SurfaceT *src) {
|
|||
UWORD prevPalIdx;
|
||||
UWORD vpos;
|
||||
UWORD topBorder;
|
||||
UWORD bandCount;
|
||||
|
||||
ucl = (struct UCopList *)AllocMem(sizeof(struct UCopList),
|
||||
MEMF_PUBLIC | MEMF_CLEAR);
|
||||
|
|
@ -177,7 +180,6 @@ static void buildCopperList(const SurfaceT *src) {
|
|||
// the viewport end.
|
||||
topBorder = 0;
|
||||
prevPalIdx = 0xFFFF;
|
||||
bandCount = 0;
|
||||
|
||||
for (line = 0; line < SURFACE_HEIGHT; line++) {
|
||||
palIdx = src->scb[line];
|
||||
|
|
@ -194,9 +196,7 @@ static void buildCopperList(const SurfaceT *src) {
|
|||
CMOVE(ucl, custom.color[col], src->palette[palIdx][col]);
|
||||
}
|
||||
prevPalIdx = (UWORD)palIdx;
|
||||
bandCount++;
|
||||
}
|
||||
(void)bandCount;
|
||||
CEND(ucl);
|
||||
|
||||
gNewUCL = ucl;
|
||||
|
|
@ -417,19 +417,27 @@ void halPresent(const SurfaceT *src) {
|
|||
return;
|
||||
}
|
||||
updateCopperIfNeeded(src);
|
||||
c2pRange(src, 0, SURFACE_HEIGHT);
|
||||
c2pRange(src, 0, SURFACE_HEIGHT, 0, AMIGA_BYTES_PER_ROW);
|
||||
}
|
||||
|
||||
|
||||
void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) {
|
||||
(void)x;
|
||||
(void)w;
|
||||
uint16_t byteStart;
|
||||
uint16_t byteEnd;
|
||||
|
||||
if (src == NULL || gScreen == NULL) {
|
||||
return;
|
||||
}
|
||||
updateCopperIfNeeded(src);
|
||||
c2pRange(src, y, y + (int16_t)h);
|
||||
// Each planar byte covers 8 horizontal pixels. Round dirty pixel
|
||||
// range to the enclosing planar-byte range so we never miss an
|
||||
// edge pixel while still honoring the rect width.
|
||||
byteStart = (uint16_t)(x >> 3);
|
||||
byteEnd = (uint16_t)(((uint16_t)x + w + 7) >> 3);
|
||||
if (byteEnd > AMIGA_BYTES_PER_ROW) {
|
||||
byteEnd = AMIGA_BYTES_PER_ROW;
|
||||
}
|
||||
c2pRange(src, y, y + (int16_t)h, byteStart, byteEnd);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
251
src/port/atarist/audio.c
Normal file
251
src/port/atarist/audio.c
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
// Atari ST audio HAL: libxmp-lite Protracker decoder + 68k Timer A IRQ
|
||||
// running PWM through YM2149 channel-A volume register.
|
||||
//
|
||||
// Pipeline:
|
||||
// * halAudioInit creates an xmp_context, allocates a 2 KB mix buffer
|
||||
// (in BSS), parks the Timer A vector at our 68k ISR (audio_isr.s),
|
||||
// and starts Timer A at ~12.3 kHz (MFP prescaler 200, data 1).
|
||||
// * halAudioPlayMod loads a .MOD via xmp_load_module_from_memory and
|
||||
// calls xmp_start_player at the same 12288 Hz mono unsigned-8-bit
|
||||
// output that the ISR consumes one byte at a time.
|
||||
// * Each Timer A tick the ISR pops one byte off the buffer, writes
|
||||
// its high 4 bits to YM register 8 (channel A volume), advances
|
||||
// the play pointer, raises a refill flag at half/end. The refill
|
||||
// itself happens in halAudioFrameTick on the main thread because
|
||||
// libxmp-lite is not interrupt-safe.
|
||||
//
|
||||
// Quality budget: 4-bit unsigned PWM at 12.3 kHz on the YM is grim by
|
||||
// modern standards but period-correct for stock 520ST/1040ST without
|
||||
// the STe DMA chip. Library users that want the higher-fidelity STe
|
||||
// path can skip this HAL and build their own.
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include <mint/osbind.h>
|
||||
|
||||
#include <libxmp-lite/xmp.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "audioSfxMixInternal.h"
|
||||
#include "joey/audio.h"
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
// MFP Timer A registers and the autovector slot for its IRQ (vector
|
||||
// $134, Setexc(0x134/4, ...) = Setexc(13, ...)).
|
||||
#define ST_MFP_TACR ((volatile uint8_t *)0xFFFFFA19L)
|
||||
#define ST_MFP_TADR ((volatile uint8_t *)0xFFFFFA1FL)
|
||||
#define ST_MFP_IERA ((volatile uint8_t *)0xFFFFFA07L)
|
||||
#define ST_MFP_IMRA ((volatile uint8_t *)0xFFFFFA13L)
|
||||
#define ST_MFP_ISRA ((volatile uint8_t *)0xFFFFFA0FL)
|
||||
|
||||
#define MFP_TA_BIT 0x20
|
||||
#define MFP_TACR_STOP 0x00
|
||||
#define MFP_TACR_DIV200 0x07
|
||||
|
||||
#define VEC_MFP_TA (0x134 / 4)
|
||||
#define INT_TIMER_A 13 // Jenabint / Jdisint id for Timer A
|
||||
|
||||
#define MIX_RATE 12288 // Hz, matches MFP 2.4576 MHz / 200
|
||||
#define MIX_BUFFER 2048
|
||||
#define MIX_HALF (MIX_BUFFER / 2)
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
// The ISR (audio_isr.s) reads/writes these. Names must match the
|
||||
// .extern declarations there.
|
||||
volatile uint8_t gMixBuf[MIX_BUFFER];
|
||||
volatile uint8_t *gMixPos = NULL;
|
||||
volatile uint8_t *gMixMid = NULL;
|
||||
volatile uint8_t *gMixEnd = NULL;
|
||||
volatile uint8_t gNeedRefill[2] = { 0, 0 };
|
||||
|
||||
extern void mfpTimerAIsr(void);
|
||||
|
||||
static xmp_context gXmpCtx = NULL;
|
||||
static bool gReady = false;
|
||||
static bool gXmpStarted = false;
|
||||
static bool gXmpLoaded = false;
|
||||
static int gXmpLoopCount = 0;
|
||||
|
||||
static void (*gOldTimerAVec)(void) = NULL;
|
||||
|
||||
// SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c).
|
||||
// Mixed in over libxmp's MOD output during halAudioFrameTick.
|
||||
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
||||
static long installTimerA(void) {
|
||||
uint16_t i;
|
||||
|
||||
// Park the buffer in known-silent state (unsigned 8-bit middle
|
||||
// = 128) before opening the IRQ gate.
|
||||
for (i = 0; i < MIX_BUFFER; i++) {
|
||||
gMixBuf[i] = 0x80;
|
||||
}
|
||||
gMixPos = (volatile uint8_t *)gMixBuf;
|
||||
gMixMid = (volatile uint8_t *)(gMixBuf + MIX_HALF);
|
||||
gMixEnd = (volatile uint8_t *)(gMixBuf + MIX_BUFFER);
|
||||
gNeedRefill[0] = 0;
|
||||
gNeedRefill[1] = 0;
|
||||
|
||||
// MFP Timer A: stop, install our vector, set prescaler 200 + data
|
||||
// 1 (= 2.4576 MHz / 200 = 12288 Hz), then start.
|
||||
*ST_MFP_TACR = MFP_TACR_STOP;
|
||||
gOldTimerAVec = (void (*)(void))Setexc(VEC_MFP_TA, (long)mfpTimerAIsr);
|
||||
*ST_MFP_TADR = 1;
|
||||
*ST_MFP_TACR = MFP_TACR_DIV200;
|
||||
Jenabint(INT_TIMER_A);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static long uninstallTimerA(void) {
|
||||
Jdisint(INT_TIMER_A);
|
||||
*ST_MFP_TACR = MFP_TACR_STOP;
|
||||
if (gOldTimerAVec != NULL) {
|
||||
(void)Setexc(VEC_MFP_TA, (long)gOldTimerAVec);
|
||||
gOldTimerAVec = NULL;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static void silenceMixBuffer(void) {
|
||||
uint16_t i;
|
||||
for (i = 0; i < MIX_BUFFER; i++) {
|
||||
gMixBuf[i] = 0x80;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----- HAL API (alphabetical) -----
|
||||
|
||||
bool halAudioInit(void) {
|
||||
if (gReady) {
|
||||
return true;
|
||||
}
|
||||
gXmpCtx = xmp_create_context();
|
||||
if (gXmpCtx == NULL) {
|
||||
return false;
|
||||
}
|
||||
Supexec(installTimerA);
|
||||
gReady = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioShutdown(void) {
|
||||
if (!gReady) {
|
||||
return;
|
||||
}
|
||||
Supexec(uninstallTimerA);
|
||||
silenceMixBuffer();
|
||||
|
||||
if (gXmpCtx != NULL) {
|
||||
if (gXmpStarted) {
|
||||
xmp_end_player(gXmpCtx);
|
||||
gXmpStarted = false;
|
||||
}
|
||||
if (gXmpLoaded) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
}
|
||||
xmp_free_context(gXmpCtx);
|
||||
gXmpCtx = NULL;
|
||||
}
|
||||
gReady = false;
|
||||
}
|
||||
|
||||
|
||||
bool halAudioIsPlayingMod(void) {
|
||||
return gReady && gXmpStarted;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
||||
if (!gReady || gXmpCtx == NULL) {
|
||||
return;
|
||||
}
|
||||
if (gXmpStarted) {
|
||||
xmp_end_player(gXmpCtx);
|
||||
gXmpStarted = false;
|
||||
}
|
||||
if (gXmpLoaded) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
}
|
||||
if (xmp_load_module_from_memory(gXmpCtx, (void *)data, (long)length) != 0) {
|
||||
return;
|
||||
}
|
||||
gXmpLoaded = true;
|
||||
if (xmp_start_player(gXmpCtx, MIX_RATE,
|
||||
XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
return;
|
||||
}
|
||||
gXmpLoopCount = loop ? -1 : 0;
|
||||
gXmpStarted = true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
|
||||
if (!gReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopMod(void) {
|
||||
if (!gReady || gXmpCtx == NULL) {
|
||||
return;
|
||||
}
|
||||
if (gXmpStarted) {
|
||||
xmp_end_player(gXmpCtx);
|
||||
gXmpStarted = false;
|
||||
}
|
||||
if (gXmpLoaded) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
}
|
||||
silenceMixBuffer();
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopSfx(uint8_t slot) {
|
||||
if (slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
gSfxSlots[slot].active = false;
|
||||
}
|
||||
|
||||
|
||||
// Drains ISR-raised refill flags by re-running libxmp's mixer into
|
||||
// the consumed half then overlaying any active SFX. Called from the
|
||||
// game loop, never from IRQ.
|
||||
void halAudioFrameTick(void) {
|
||||
if (!gReady) {
|
||||
return;
|
||||
}
|
||||
if (gNeedRefill[0]) {
|
||||
if (gXmpStarted) {
|
||||
xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount);
|
||||
} else {
|
||||
memset((void *)gMixBuf, 0x80, MIX_HALF);
|
||||
}
|
||||
audioSfxOverlayMix(gMixBuf, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
|
||||
gNeedRefill[0] = 0;
|
||||
}
|
||||
if (gNeedRefill[1]) {
|
||||
if (gXmpStarted) {
|
||||
xmp_play_buffer(gXmpCtx, (void *)(gMixBuf + MIX_HALF), MIX_HALF, gXmpLoopCount);
|
||||
} else {
|
||||
memset((void *)(gMixBuf + MIX_HALF), 0x80, MIX_HALF);
|
||||
}
|
||||
audioSfxOverlayMix(gMixBuf + MIX_HALF, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
|
||||
gNeedRefill[1] = 0;
|
||||
}
|
||||
}
|
||||
56
src/port/atarist/audio_isr.s
Normal file
56
src/port/atarist/audio_isr.s
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
; Atari ST audio ISR -- MFP Timer A drives per-sample PWM output via
|
||||
; the YM2149 channel-A volume register.
|
||||
;
|
||||
; Each interrupt:
|
||||
; 1. Reads one byte from the libxmp-lite mix buffer.
|
||||
; 2. Writes its high 4 bits to YM register 8 (channel A volume) so
|
||||
; the analog filter on the YM output emits a 4-bit-resolution
|
||||
; sample for that tick.
|
||||
; 3. Advances the play pointer; raises a refill flag when crossing
|
||||
; the midpoint or end of the buffer; wraps at the end.
|
||||
; 4. Acks the MFP Timer-A bit in ISRA and RTEs.
|
||||
;
|
||||
; Communication with the C HAL is via four globals defined in audio.c:
|
||||
; _gMixBuf -- start of the mix buffer (uint8_t array)
|
||||
; _gMixPos -- volatile pointer to the next sample to play
|
||||
; _gMixMid -- end of first half (used to detect mid-cross)
|
||||
; _gMixEnd -- end of buffer (used to detect wrap)
|
||||
; _gNeedRefill -- two-byte array; ISR writes 1 to mark a half stale
|
||||
|
||||
xdef _mfpTimerAIsr
|
||||
xref _gMixBuf
|
||||
xref _gMixPos
|
||||
xref _gMixMid
|
||||
xref _gMixEnd
|
||||
xref _gNeedRefill
|
||||
|
||||
YM_SELECT equ $FFFF8800
|
||||
YM_DATA equ $FFFF8802
|
||||
MFP_ISRA equ $FFFFFA0F
|
||||
|
||||
_mfpTimerAIsr:
|
||||
movem.l d0/a0,-(sp)
|
||||
|
||||
move.l _gMixPos,a0
|
||||
move.b (a0)+,d0
|
||||
lsr.b #4,d0
|
||||
and.w #$0F,d0
|
||||
|
||||
move.b #8,YM_SELECT
|
||||
move.b d0,YM_DATA
|
||||
|
||||
cmp.l _gMixMid,a0
|
||||
bne.s .check_end
|
||||
move.b #1,_gNeedRefill+0
|
||||
.check_end:
|
||||
cmp.l _gMixEnd,a0
|
||||
blt.s .save_pos
|
||||
move.b #1,_gNeedRefill+1
|
||||
lea _gMixBuf,a0
|
||||
.save_pos:
|
||||
move.l a0,_gMixPos
|
||||
|
||||
move.b #$DF,MFP_ISRA
|
||||
|
||||
movem.l (sp)+,d0/a0
|
||||
rte
|
||||
|
|
@ -64,8 +64,8 @@
|
|||
// ----- Prototypes -----
|
||||
|
||||
static uint16_t quantizeColorToSt(uint16_t orgb);
|
||||
static void c2pRow(const uint8_t *src, uint16_t *dst);
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1);
|
||||
static void c2pRow(const uint8_t *src, uint16_t *dst, uint16_t groupStart, uint16_t groupEnd);
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1, uint16_t groupStart, uint16_t groupEnd);
|
||||
static void flattenScbPalettes(const SurfaceT *src);
|
||||
static void writeDiagnostics(void);
|
||||
static long writePrevPaletteRegs(void);
|
||||
|
|
@ -131,9 +131,10 @@ static bool gCacheValid = false;
|
|||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
// Convert 16 chunky pixels (8 bytes 4bpp packed) to 4 ST planar words.
|
||||
// The output word order is plane 0, 1, 2, 3 (low bit = plane 0).
|
||||
static void c2pRow(const uint8_t *src, uint16_t *dst) {
|
||||
// Convert 16 chunky pixels (8 bytes 4bpp packed) to 4 ST planar words
|
||||
// per group. groupStart..groupEnd selects a horizontal sub-range so
|
||||
// halPresentRect can avoid touching unchanged groups.
|
||||
static void c2pRow(const uint8_t *src, uint16_t *dst, uint16_t groupStart, uint16_t groupEnd) {
|
||||
uint16_t group;
|
||||
uint16_t px;
|
||||
uint16_t plane0;
|
||||
|
|
@ -144,7 +145,7 @@ static void c2pRow(const uint8_t *src, uint16_t *dst) {
|
|||
uint8_t nibble;
|
||||
uint16_t bit;
|
||||
|
||||
for (group = 0; group < ST_GROUPS_PER_ROW; group++) {
|
||||
for (group = groupStart; group < groupEnd; group++) {
|
||||
plane0 = 0;
|
||||
plane1 = 0;
|
||||
plane2 = 0;
|
||||
|
|
@ -168,7 +169,7 @@ static void c2pRow(const uint8_t *src, uint16_t *dst) {
|
|||
}
|
||||
|
||||
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) {
|
||||
static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1, uint16_t groupStart, uint16_t groupEnd) {
|
||||
int16_t y;
|
||||
const uint8_t *srcLine;
|
||||
uint16_t *dstLine;
|
||||
|
|
@ -176,7 +177,7 @@ static void c2pRange(const SurfaceT *src, int16_t y0, int16_t y1) {
|
|||
for (y = y0; y < y1; y++) {
|
||||
srcLine = &src->pixels[y * SURFACE_BYTES_PER_ROW];
|
||||
dstLine = (uint16_t *)&gScreenBase[y * ST_BYTES_PER_ROW];
|
||||
c2pRow(srcLine, dstLine);
|
||||
c2pRow(srcLine, dstLine, groupStart, groupEnd);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -481,19 +482,27 @@ void halPresent(const SurfaceT *src) {
|
|||
return;
|
||||
}
|
||||
refreshPaletteStateIfNeeded(src);
|
||||
c2pRange(src, 0, SURFACE_HEIGHT);
|
||||
c2pRange(src, 0, SURFACE_HEIGHT, 0, ST_GROUPS_PER_ROW);
|
||||
}
|
||||
|
||||
|
||||
void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) {
|
||||
(void)x;
|
||||
(void)w;
|
||||
uint16_t groupStart;
|
||||
uint16_t groupEnd;
|
||||
|
||||
if (src == NULL || !gModeSet) {
|
||||
return;
|
||||
}
|
||||
refreshPaletteStateIfNeeded(src);
|
||||
c2pRange(src, y, y + (int16_t)h);
|
||||
// Each c2p group covers 16 horizontal pixels. Round dirty pixel
|
||||
// range to the enclosing group range to keep the planar word
|
||||
// alignment without missing edge pixels.
|
||||
groupStart = (uint16_t)(x >> 4);
|
||||
groupEnd = (uint16_t)(((uint16_t)x + w + 15) >> 4);
|
||||
if (groupEnd > ST_GROUPS_PER_ROW) {
|
||||
groupEnd = ST_GROUPS_PER_ROW;
|
||||
}
|
||||
c2pRange(src, y, y + (int16_t)h, groupStart, groupEnd);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
633
src/port/dos/audio.c
Normal file
633
src/port/dos/audio.c
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
// DOS audio HAL: Sound Blaster Pro / SB16 8-bit mono playback driven
|
||||
// by 8237 auto-init DMA, fed by libxmp-lite's mixer.
|
||||
//
|
||||
// Pipeline:
|
||||
// * halAudioInit detects the card via DSP reset + version, allocates
|
||||
// a 4 KB conventional-memory DMA buffer (aligned to not cross a
|
||||
// 64 KB boundary), installs an IRQ handler on the SB's IRQ line,
|
||||
// creates an xmp_context, and arms auto-init DMA at 22050 Hz mono
|
||||
// 8-bit. The DMA buffer is split into two halves; the SB IRQ fires
|
||||
// at the end of each half and our handler refills the *other* half
|
||||
// by calling xmp_play_buffer with libxmp-lite's mixer output.
|
||||
// * halAudioPlayMod loads a .MOD blob through xmp_load_module and
|
||||
// starts the player; subsequent IRQs auto-pump the audio.
|
||||
// * halAudioPlaySfx routes through libxmp-lite's SMIX overlay so SFX
|
||||
// mix on top of the music using xmp_smix_play_sample.
|
||||
//
|
||||
// Defaults match DOSBox-Staging's emulated SB Pro: base $220, IRQ 7,
|
||||
// 8-bit DMA channel 1. BLASTER environment variable overrides each.
|
||||
//
|
||||
// Because DJGPP runs in protected mode with paging, the IRQ handler
|
||||
// code and any data it touches (DMA selector, libxmp context pointer,
|
||||
// the DMA buffer) must be locked or a page fault at interrupt time
|
||||
// will hang the machine.
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dos.h>
|
||||
#include <dpmi.h>
|
||||
#include <go32.h>
|
||||
#include <pc.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/farptr.h>
|
||||
#include <sys/movedata.h>
|
||||
#include <sys/nearptr.h>
|
||||
|
||||
#include <libxmp-lite/xmp.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "audioSfxMixInternal.h"
|
||||
#include "joey/audio.h"
|
||||
|
||||
// ----- SB I/O port offsets from base -----
|
||||
|
||||
#define SB_RESET_OFFSET 0x06
|
||||
#define SB_READ_OFFSET 0x0A
|
||||
#define SB_WRITE_OFFSET 0x0C
|
||||
#define SB_DATA_AVAIL_OFFSET 0x0E
|
||||
|
||||
// ----- DSP commands -----
|
||||
|
||||
#define DSP_GET_VERSION 0xE1
|
||||
#define DSP_SPEAKER_ON 0xD1
|
||||
#define DSP_SPEAKER_OFF 0xD3
|
||||
#define DSP_TIME_CONST 0x40
|
||||
#define DSP_BLOCK_SIZE 0x48
|
||||
#define DSP_OUT_8_AUTO 0x1C
|
||||
#define DSP_HALT_DMA8 0xD0
|
||||
#define DSP_EXIT_AUTO 0xDA
|
||||
|
||||
#define DSP_RESET_RETRIES 1024
|
||||
#define DSP_READY_BYTE 0xAA
|
||||
|
||||
// ----- 8237 DMA controller (channel 1) -----
|
||||
|
||||
#define DMA_MASK 0x0A
|
||||
#define DMA_MODE 0x0B
|
||||
#define DMA_FLIPFLOP 0x0C
|
||||
#define DMA1_ADDR 0x02
|
||||
#define DMA1_COUNT 0x03
|
||||
#define DMA1_PAGE 0x83
|
||||
|
||||
// Channel-1 mode byte: single transfer (01), incrementing (0),
|
||||
// auto-init (1), read transfer (10), channel 1 (01) = 01011001 = 0x59.
|
||||
#define DMA_MODE_OUT_AI_CH1 0x59
|
||||
#define DMA_MASK_DISABLE_CH1 0x05 // bit 2 = mask, channel 1
|
||||
#define DMA_MASK_ENABLE_CH1 0x01
|
||||
|
||||
// ----- PIC -----
|
||||
|
||||
#define PIC1_CMD 0x20
|
||||
#define PIC1_IMR 0x21
|
||||
#define PIC2_CMD 0xA0
|
||||
#define PIC2_IMR 0xA1
|
||||
#define PIC_EOI 0x20
|
||||
#define PIC_CASCADE_IRQ 2
|
||||
|
||||
// ----- Defaults -----
|
||||
|
||||
#define DEFAULT_SB_BASE 0x220
|
||||
#define DEFAULT_SB_IRQ 7
|
||||
#define DEFAULT_SB_DMA8 1
|
||||
|
||||
// ----- Mix configuration -----
|
||||
|
||||
#define MIX_RATE 22050
|
||||
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves
|
||||
#define MIX_HALF (MIX_DMA_BUFFER / 2)
|
||||
|
||||
#define ISR_LOCK_SIZE 4096
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
static uint16_t gSbBase = DEFAULT_SB_BASE;
|
||||
static uint8_t gSbIrq = DEFAULT_SB_IRQ;
|
||||
static uint8_t gSbDma8 = DEFAULT_SB_DMA8;
|
||||
static uint8_t gDspMajor = 0;
|
||||
static uint8_t gDspMinor = 0;
|
||||
static bool gDspPresent = false;
|
||||
|
||||
// Conventional-memory DMA buffer.
|
||||
static int gDmaSelector = 0;
|
||||
static uint32_t gDmaLinear = 0;
|
||||
static uint8_t *gDmaBuf = NULL;
|
||||
|
||||
// PIC vector for the SB IRQ.
|
||||
static _go32_dpmi_seginfo gOldIrqVec;
|
||||
static _go32_dpmi_seginfo gNewIrqVec;
|
||||
static bool gIrqHooked = false;
|
||||
|
||||
// libxmp state.
|
||||
static xmp_context gXmpCtx = NULL;
|
||||
static bool gXmpStarted = false;
|
||||
static bool gXmpLoaded = false;
|
||||
static volatile int gXmpLoopCount = 0; // last arg to xmp_play_buffer
|
||||
|
||||
// SFX overlay: libxmp-lite's SMIX takes a file path (not an in-memory
|
||||
// buffer), so we run a one-shot sample mixer (shared with the ST HAL,
|
||||
// see src/core/audioSfxMix.c) on top of the MOD output. Mix happens in
|
||||
// refillHalf after libxmp has filled the half-buffer.
|
||||
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
|
||||
|
||||
// ISR-side state. volatile so the main loop sees IRQ updates promptly.
|
||||
// gNextHalf flips between 0 and 1 each IRQ; gNeedRefill[i] is raised
|
||||
// in the IRQ handler and lowered in halAudioFrameTick after the
|
||||
// half has been refilled. We deliberately do NOT call libxmp-lite
|
||||
// from interrupt context -- the mixer allocates internally and is
|
||||
// not reentrancy-safe -- so all work that touches the xmp context
|
||||
// happens in the main thread.
|
||||
static volatile uint8_t gNextHalf = 0;
|
||||
static volatile bool gNeedRefill[2] = { false, false };
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void parseBlasterEnv(void);
|
||||
static bool dspReset(void);
|
||||
static void dspWrite(uint8_t value);
|
||||
static int dspRead(void);
|
||||
static int picVectorForIrq(uint8_t irq);
|
||||
static bool allocDmaBuffer(void);
|
||||
static void freeDmaBuffer(void);
|
||||
static void programDmaController(void);
|
||||
static void programDspAutoInit(void);
|
||||
static void haltDspDma(void);
|
||||
static void picUnmaskIrq(uint8_t irq);
|
||||
static void picMaskIrq(uint8_t irq);
|
||||
static void sbIsr(void);
|
||||
static void refillHalf(uint8_t halfIdx);
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
||||
static void parseBlasterEnv(void) {
|
||||
const char *blaster;
|
||||
const char *p;
|
||||
char ch;
|
||||
long val;
|
||||
char *end;
|
||||
|
||||
blaster = getenv("BLASTER");
|
||||
if (blaster == NULL || *blaster == '\0') {
|
||||
return;
|
||||
}
|
||||
for (p = blaster; *p != '\0';) {
|
||||
while (*p != '\0' && isspace((unsigned char)*p)) {
|
||||
p++;
|
||||
}
|
||||
if (*p == '\0') {
|
||||
break;
|
||||
}
|
||||
ch = (char)toupper((unsigned char)*p);
|
||||
p++;
|
||||
val = strtol(p, &end, ch == 'A' ? 16 : 10);
|
||||
if (end == p) {
|
||||
while (*p != '\0' && !isspace((unsigned char)*p)) {
|
||||
p++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
p = end;
|
||||
switch (ch) {
|
||||
case 'A': gSbBase = (uint16_t)val; break;
|
||||
case 'I': gSbIrq = (uint8_t)val; break;
|
||||
case 'D': gSbDma8 = (uint8_t)val; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void dspWrite(uint8_t value) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 65536; i++) {
|
||||
if ((inportb(gSbBase + SB_WRITE_OFFSET) & 0x80) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
outportb(gSbBase + SB_WRITE_OFFSET, value);
|
||||
}
|
||||
|
||||
|
||||
static int dspRead(void) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 65536; i++) {
|
||||
if ((inportb(gSbBase + SB_DATA_AVAIL_OFFSET) & 0x80) != 0) {
|
||||
return inportb(gSbBase + SB_READ_OFFSET);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
static bool dspReset(void) {
|
||||
int retry;
|
||||
int byte;
|
||||
uint16_t port;
|
||||
int i;
|
||||
|
||||
port = (uint16_t)(gSbBase + SB_RESET_OFFSET);
|
||||
outportb(port, 1);
|
||||
for (i = 0; i < 8; i++) {
|
||||
(void)inportb(port);
|
||||
}
|
||||
outportb(port, 0);
|
||||
|
||||
for (retry = 0; retry < DSP_RESET_RETRIES; retry++) {
|
||||
byte = dspRead();
|
||||
if (byte == DSP_READY_BYTE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// PIC1 (master) handles IRQ 0-7 at INT $08-$0F. PIC2 (slave) handles
|
||||
// IRQ 8-15 at INT $70-$77. Map an IRQ number to its PM interrupt
|
||||
// vector. SB's typical IRQs are 5 or 7 (PIC1) or 10 (PIC2).
|
||||
static int picVectorForIrq(uint8_t irq) {
|
||||
if (irq < 8) {
|
||||
return 0x08 + irq;
|
||||
}
|
||||
return 0x70 + (irq - 8);
|
||||
}
|
||||
|
||||
|
||||
// Allocate a DMA buffer in conventional memory that does not cross a
|
||||
// 64 KB physical-page boundary -- the 8237 cannot DMA across that
|
||||
// boundary on 8-bit channels. We over-allocate to 2x size and use the
|
||||
// lower-aligned half so we always have room.
|
||||
static bool allocDmaBuffer(void) {
|
||||
int sel;
|
||||
int seg;
|
||||
uint32_t linear;
|
||||
uint32_t aligned;
|
||||
int paragraphs;
|
||||
|
||||
paragraphs = ((MIX_DMA_BUFFER * 2) + 15) >> 4;
|
||||
seg = __dpmi_allocate_dos_memory(paragraphs, &sel);
|
||||
if (seg < 0) {
|
||||
return false;
|
||||
}
|
||||
linear = (uint32_t)seg << 4;
|
||||
|
||||
// Round linear up to a starting offset that gives us
|
||||
// MIX_DMA_BUFFER without crossing the next 64 KB boundary.
|
||||
if (((linear & 0xFFFF) + MIX_DMA_BUFFER) > 0x10000) {
|
||||
aligned = (linear + 0xFFFFu) & ~0xFFFFu;
|
||||
} else {
|
||||
aligned = linear;
|
||||
}
|
||||
|
||||
gDmaSelector = sel;
|
||||
gDmaLinear = aligned;
|
||||
gDmaBuf = (uint8_t *)(__djgpp_conventional_base + aligned);
|
||||
memset(gDmaBuf, 0x80, MIX_DMA_BUFFER); // 8-bit unsigned silence
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void freeDmaBuffer(void) {
|
||||
if (gDmaSelector != 0) {
|
||||
__dpmi_free_dos_memory(gDmaSelector);
|
||||
gDmaSelector = 0;
|
||||
gDmaLinear = 0;
|
||||
gDmaBuf = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void programDmaController(void) {
|
||||
uint8_t page;
|
||||
uint16_t offset;
|
||||
uint16_t count;
|
||||
|
||||
page = (uint8_t)((gDmaLinear >> 16) & 0xFF);
|
||||
offset = (uint16_t)(gDmaLinear & 0xFFFF);
|
||||
count = (uint16_t)(MIX_DMA_BUFFER - 1);
|
||||
|
||||
outportb(DMA_MASK, DMA_MASK_DISABLE_CH1);
|
||||
outportb(DMA_FLIPFLOP, 0);
|
||||
outportb(DMA_MODE, DMA_MODE_OUT_AI_CH1);
|
||||
outportb(DMA1_ADDR, (uint8_t)(offset & 0xFF));
|
||||
outportb(DMA1_ADDR, (uint8_t)((offset >> 8) & 0xFF));
|
||||
outportb(DMA1_PAGE, page);
|
||||
outportb(DMA1_COUNT, (uint8_t)(count & 0xFF));
|
||||
outportb(DMA1_COUNT, (uint8_t)((count >> 8) & 0xFF));
|
||||
outportb(DMA_MASK, DMA_MASK_ENABLE_CH1);
|
||||
}
|
||||
|
||||
|
||||
// 8-bit DSP time constant: tc = 256 - 1000000 / rate. SB plays at
|
||||
// 1000000 / (256 - tc) Hz, so we round to the nearest representable
|
||||
// rate. For 22050 Hz: tc = 256 - 45 = 211.
|
||||
static uint8_t dspTimeConst(uint16_t rate) {
|
||||
uint32_t tc;
|
||||
|
||||
tc = 256u - (1000000u / rate);
|
||||
if (tc < 4) {
|
||||
tc = 4;
|
||||
}
|
||||
if (tc > 255) {
|
||||
tc = 255;
|
||||
}
|
||||
return (uint8_t)tc;
|
||||
}
|
||||
|
||||
|
||||
static void programDspAutoInit(void) {
|
||||
uint16_t blockMinusOne;
|
||||
|
||||
dspWrite(DSP_SPEAKER_ON);
|
||||
dspWrite(DSP_TIME_CONST);
|
||||
dspWrite(dspTimeConst(MIX_RATE));
|
||||
|
||||
blockMinusOne = (uint16_t)(MIX_HALF - 1);
|
||||
dspWrite(DSP_BLOCK_SIZE);
|
||||
dspWrite((uint8_t)(blockMinusOne & 0xFF));
|
||||
dspWrite((uint8_t)((blockMinusOne >> 8) & 0xFF));
|
||||
|
||||
dspWrite(DSP_OUT_8_AUTO);
|
||||
}
|
||||
|
||||
|
||||
static void haltDspDma(void) {
|
||||
dspWrite(DSP_HALT_DMA8);
|
||||
dspWrite(DSP_EXIT_AUTO);
|
||||
dspWrite(DSP_SPEAKER_OFF);
|
||||
}
|
||||
|
||||
|
||||
// Unmask the IRQ line in the PIC's Interrupt Mask Register so the IRQ
|
||||
// can reach the CPU. Some environments (DOSBox in particular) leave the
|
||||
// SB IRQ masked at boot; without this the hooked vector never fires
|
||||
// and audio drops out after the first half-buffer.
|
||||
static void picUnmaskIrq(uint8_t irq) {
|
||||
uint16_t port;
|
||||
uint8_t bit;
|
||||
uint8_t mask;
|
||||
|
||||
if (irq < 8) {
|
||||
port = PIC1_IMR;
|
||||
bit = (uint8_t)(1u << irq);
|
||||
} else {
|
||||
port = PIC2_IMR;
|
||||
bit = (uint8_t)(1u << (irq - 8));
|
||||
}
|
||||
mask = inportb(port);
|
||||
mask = (uint8_t)(mask & ~bit);
|
||||
outportb(port, mask);
|
||||
if (irq >= 8) {
|
||||
// Cascade line on PIC1 must also be unmasked for PIC2 to reach
|
||||
// the CPU.
|
||||
mask = inportb(PIC1_IMR);
|
||||
mask = (uint8_t)(mask & ~(1u << PIC_CASCADE_IRQ));
|
||||
outportb(PIC1_IMR, mask);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void picMaskIrq(uint8_t irq) {
|
||||
uint16_t port;
|
||||
uint8_t bit;
|
||||
uint8_t mask;
|
||||
|
||||
if (irq < 8) {
|
||||
port = PIC1_IMR;
|
||||
bit = (uint8_t)(1u << irq);
|
||||
} else {
|
||||
port = PIC2_IMR;
|
||||
bit = (uint8_t)(1u << (irq - 8));
|
||||
}
|
||||
mask = inportb(port);
|
||||
mask = (uint8_t)(mask | bit);
|
||||
outportb(port, mask);
|
||||
}
|
||||
|
||||
|
||||
// Refill one half of the DMA buffer from libxmp-lite's mixer.
|
||||
// Called from halAudioFrameTick on the main thread (NOT in IRQ
|
||||
// context); the IRQ only flags which halves need refilling.
|
||||
// xmp_play_buffer fills our bytes regardless of whether a module is
|
||||
// loaded -- it returns silence (centered 0x80) when idle.
|
||||
static void refillHalf(uint8_t halfIdx) {
|
||||
uint8_t *dst;
|
||||
|
||||
dst = gDmaBuf + ((uint32_t)halfIdx * MIX_HALF);
|
||||
if (gXmpCtx != NULL && gXmpStarted) {
|
||||
// libxmp emits unsigned 8-bit when XMP_FORMAT_UNSIGNED is set
|
||||
// in xmp_start_player, matching SB's DMA format -- no extra
|
||||
// signed/unsigned conversion needed here.
|
||||
xmp_play_buffer(gXmpCtx, dst, MIX_HALF, gXmpLoopCount);
|
||||
} else {
|
||||
memset(dst, 0x80, MIX_HALF);
|
||||
}
|
||||
audioSfxOverlayMix(dst, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
|
||||
}
|
||||
|
||||
|
||||
// SB IRQ. Acks the SB (read $0E), marks the half that just played
|
||||
// for refill, EOIs the PIC. The actual mixer work happens in
|
||||
// halAudioFrameTick on the main thread.
|
||||
static void sbIsr(void) {
|
||||
uint8_t fillHalf;
|
||||
|
||||
fillHalf = gNextHalf;
|
||||
gNextHalf = (uint8_t)(gNextHalf ^ 1);
|
||||
gNeedRefill[fillHalf] = true;
|
||||
|
||||
(void)inportb(gSbBase + SB_DATA_AVAIL_OFFSET);
|
||||
|
||||
if (gSbIrq >= 8) {
|
||||
outportb(PIC2_CMD, PIC_EOI);
|
||||
}
|
||||
outportb(PIC1_CMD, PIC_EOI);
|
||||
}
|
||||
|
||||
|
||||
// ----- HAL API (alphabetical) -----
|
||||
|
||||
bool halAudioInit(void) {
|
||||
int major;
|
||||
int minor;
|
||||
int vector;
|
||||
|
||||
if (gDspPresent) {
|
||||
return true;
|
||||
}
|
||||
parseBlasterEnv();
|
||||
if (!dspReset()) {
|
||||
return false;
|
||||
}
|
||||
dspWrite(DSP_GET_VERSION);
|
||||
major = dspRead();
|
||||
minor = dspRead();
|
||||
if (major < 0 || minor < 0) {
|
||||
return false;
|
||||
}
|
||||
gDspMajor = (uint8_t)major;
|
||||
gDspMinor = (uint8_t)minor;
|
||||
|
||||
if (!allocDmaBuffer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
gXmpCtx = xmp_create_context();
|
||||
if (gXmpCtx == NULL) {
|
||||
freeDmaBuffer();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lock ISR code + the data sbIsr touches at IRQ time. The mixer
|
||||
// refill no longer runs in interrupt context, so refillHalf and
|
||||
// libxmp internals do not need locking.
|
||||
_go32_dpmi_lock_code(sbIsr, ISR_LOCK_SIZE);
|
||||
_go32_dpmi_lock_data((void *)&gNextHalf, sizeof(gNextHalf));
|
||||
_go32_dpmi_lock_data((void *)gNeedRefill, sizeof(gNeedRefill));
|
||||
_go32_dpmi_lock_data((void *)&gSbBase, sizeof(gSbBase));
|
||||
_go32_dpmi_lock_data((void *)&gSbIrq, sizeof(gSbIrq));
|
||||
|
||||
vector = picVectorForIrq(gSbIrq);
|
||||
_go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
|
||||
gNewIrqVec.pm_offset = (unsigned long)sbIsr;
|
||||
gNewIrqVec.pm_selector = _go32_my_cs();
|
||||
if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
|
||||
xmp_free_context(gXmpCtx);
|
||||
gXmpCtx = NULL;
|
||||
freeDmaBuffer();
|
||||
return false;
|
||||
}
|
||||
_go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec);
|
||||
gIrqHooked = true;
|
||||
|
||||
gNextHalf = 0;
|
||||
programDmaController();
|
||||
programDspAutoInit();
|
||||
picUnmaskIrq(gSbIrq);
|
||||
|
||||
gDspPresent = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioShutdown(void) {
|
||||
int vector;
|
||||
|
||||
if (!gDspPresent) {
|
||||
return;
|
||||
}
|
||||
haltDspDma();
|
||||
if (gIrqHooked) {
|
||||
picMaskIrq(gSbIrq);
|
||||
vector = picVectorForIrq(gSbIrq);
|
||||
_go32_dpmi_set_protected_mode_interrupt_vector(vector, &gOldIrqVec);
|
||||
_go32_dpmi_free_iret_wrapper(&gNewIrqVec);
|
||||
gIrqHooked = false;
|
||||
}
|
||||
if (gXmpCtx != NULL) {
|
||||
if (gXmpStarted) {
|
||||
xmp_end_player(gXmpCtx);
|
||||
gXmpStarted = false;
|
||||
}
|
||||
if (gXmpLoaded) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
}
|
||||
xmp_free_context(gXmpCtx);
|
||||
gXmpCtx = NULL;
|
||||
}
|
||||
freeDmaBuffer();
|
||||
(void)dspReset();
|
||||
gDspPresent = false;
|
||||
}
|
||||
|
||||
|
||||
bool halAudioIsPlayingMod(void) {
|
||||
return gXmpStarted;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
||||
if (!gDspPresent || gXmpCtx == NULL) {
|
||||
return;
|
||||
}
|
||||
if (gXmpStarted) {
|
||||
xmp_end_player(gXmpCtx);
|
||||
gXmpStarted = false;
|
||||
}
|
||||
if (gXmpLoaded) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
}
|
||||
if (xmp_load_module_from_memory(gXmpCtx, (void *)data, (long)length) != 0) {
|
||||
return;
|
||||
}
|
||||
gXmpLoaded = true;
|
||||
if (xmp_start_player(gXmpCtx, MIX_RATE,
|
||||
XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
return;
|
||||
}
|
||||
// libxmp-lite controls looping per-buffer-call via the last arg of
|
||||
// xmp_play_buffer (0 = stop on song end, nonzero = max loop count
|
||||
// before stop, -1 = forever). We honor `loop` by feeding that
|
||||
// value through gXmpLoopCount; the IRQ hands it to play_buffer.
|
||||
gXmpLoopCount = loop ? -1 : 0;
|
||||
gXmpStarted = true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
|
||||
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopMod(void) {
|
||||
if (!gDspPresent || gXmpCtx == NULL) {
|
||||
return;
|
||||
}
|
||||
if (gXmpStarted) {
|
||||
xmp_end_player(gXmpCtx);
|
||||
gXmpStarted = false;
|
||||
}
|
||||
if (gXmpLoaded) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopSfx(uint8_t slot) {
|
||||
if (slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
gSfxSlots[slot].active = false;
|
||||
}
|
||||
|
||||
|
||||
// Called once per game-loop iteration from the application. Drains
|
||||
// the IRQ-raised refill flags and refills out-of-IRQ (so libxmp-lite
|
||||
// is never re-entered from interrupt context). At 70 Hz polling a
|
||||
// 22050 Hz half-buffer of 2048 samples drains ~143000 samples/sec
|
||||
// of headroom, well above the playback rate, so a missed frame or
|
||||
// two is OK without underflow.
|
||||
void halAudioFrameTick(void) {
|
||||
int i;
|
||||
|
||||
if (!gDspPresent) {
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < 2; i++) {
|
||||
if (gNeedRefill[i]) {
|
||||
refillHalf((uint8_t)i);
|
||||
gNeedRefill[i] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/port/iigs/audio.c
Normal file
70
src/port/iigs/audio.c
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// Apple IIgs audio HAL stub. Real implementation pending.
|
||||
//
|
||||
// Pipeline already in place:
|
||||
// * toolchains/install.sh fetches Ninjaforce NTP source into
|
||||
// toolchains/iigs/ntp/ (CRLF stripped on extraction).
|
||||
// * make/iigs.mk assembles ninjatrackerplus.s with Merlin32 into
|
||||
// build/iigs/audio/ntpplayer.bin (34 KB raw 65816, originally
|
||||
// org $0F0000 but bank-internal so it relocates at any bank-start
|
||||
// load address).
|
||||
// * package-disk.sh bundles ntpplayer.bin onto the IIgs disk image
|
||||
// alongside the demo binaries.
|
||||
//
|
||||
// Why this file is still a stub:
|
||||
// The runtime load path (NewHandle + fopen + fread on NTPPLAYER.BIN)
|
||||
// pulls Memory Manager + ORCA stdio into the link, and ORCA Linker
|
||||
// fails with "Expression too complex in 13/SysLib" when those are
|
||||
// added on top of the existing graphics + input HAL plumbing for
|
||||
// *every* demo (the IIgs build links each binary as one monolithic
|
||||
// image, no static-library culling). Bringing in those tool sets
|
||||
// needs to land alongside the JSL trampoline that actually uses the
|
||||
// loaded NTP -- one combined effort, with the demos that don't need
|
||||
// audio either kept on a thinner audio shim or split out so the
|
||||
// linker isn't asked to resolve everything for everyone.
|
||||
//
|
||||
// Until that combined load + trampoline iteration ships, every entry
|
||||
// here is a safe no-op so the audio API stays callable.
|
||||
|
||||
#include "hal.h"
|
||||
|
||||
|
||||
bool halAudioInit(void) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void halAudioShutdown(void) {
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
||||
(void)data;
|
||||
(void)length;
|
||||
(void)loop;
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopMod(void) {
|
||||
}
|
||||
|
||||
|
||||
bool halAudioIsPlayingMod(void) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
|
||||
(void)slot;
|
||||
(void)sample;
|
||||
(void)length;
|
||||
(void)rateHz;
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopSfx(uint8_t slot) {
|
||||
(void)slot;
|
||||
}
|
||||
|
||||
|
||||
void halAudioFrameTick(void) {
|
||||
}
|
||||
394
src/port/iigs/audio_full.c
Normal file
394
src/port/iigs/audio_full.c
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
// Apple IIgs audio HAL -- full version (linked only into the AUDIO
|
||||
// demo via make/iigs.mk's split source set). audio.c keeps the no-op
|
||||
// stub for every other demo so the monolithic IIgs link budget stays
|
||||
// safe.
|
||||
//
|
||||
// Stage 1 (this file's first cut): load ntpplayer.bin (the Merlin32-
|
||||
// assembled NinjaTrackerPlus replayer staged by the iigs.mk Merlin
|
||||
// rule and bundled on the disk image) into a Memory Manager handle.
|
||||
// halAudioInit reports true if the load succeeds. PlayMod / PlaySfx /
|
||||
// StopMod still no-op until the JSL trampoline lands -- that's stage
|
||||
// 2 and gets its own file once the inline-asm syntax is nailed down.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <types.h>
|
||||
#include <memory.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "joey/audio.h"
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
#define NTP_FILENAME "NTPPLAYER.BIN"
|
||||
#define NTP_BUFFER_BYTES (64L * 1024L)
|
||||
// Sanity check: NTP source assembles to ~34 KB; reject reads that
|
||||
// come back too short to plausibly be the replayer.
|
||||
#define NTP_MIN_BYTES 32000L
|
||||
|
||||
// Ensoniq 5503 (DOC) I/O at $E1C03C..$E1C03E. NTP saves its preferred
|
||||
// sound_control byte at $E100CA so callers that want DOC-register
|
||||
// access don't have to know NTP's mode bits.
|
||||
#define DOC_SOUND_CONTROL ((volatile uint8_t *)0xE1C03CL)
|
||||
#define DOC_SOUND_DATA ((volatile uint8_t *)0xE1C03DL)
|
||||
#define DOC_SOUND_ADDRESS ((volatile uint8_t *)0xE1C03EL)
|
||||
#define DOC_IRQ_VOLUME ((volatile uint8_t *)0xE100CAL)
|
||||
|
||||
// DOC oscillator control register layout: bit 0 = halt. NTPstreamsound
|
||||
// itself uses 0x03 (halt + IE) when it wants an osc stopped, so we
|
||||
// mirror that.
|
||||
#define DOC_OSC_CTRL_BASE 0xA0
|
||||
#define DOC_OSC_HALT_BITS 0x03
|
||||
|
||||
// SFX: one fixed-bank handle holding all slots' stream structures +
|
||||
// sample buffers. Per slot: 16-byte struct followed by 4080 bytes of
|
||||
// sample, total 4 KB. JOEY_AUDIO_SFX_SLOTS slots fit in 16 KB.
|
||||
#define SFX_SLOT_BYTES (4L * 1024L)
|
||||
#define SFX_BUFFER_BYTES (SFX_SLOT_BYTES * (long)JOEY_AUDIO_SFX_SLOTS)
|
||||
|
||||
// NTP uses oscillators 0..N for music. Each SFX slot reserves 2 oscs
|
||||
// (1 streamer + 1 player) starting at 14: slot 0 = 14..15, slot 1 =
|
||||
// 16..17, etc. With 7 max music tracks * 2 oscs = 14 (oscs 0..13)
|
||||
// and NTP's interrupt osc at 31, this avoids both ranges. Each slot
|
||||
// also gets its own 512-byte-aligned DOC RAM page so they don't share
|
||||
// the streaming buffer.
|
||||
#define SFX_BASE_OSC 14
|
||||
#define SFX_OSCS_PER_SLOT 2
|
||||
#define SFX_BASE_DOC_PAGE 0xC0 // 512-aligned (bit 0 must be 0)
|
||||
#define SFX_DOC_PAGE_STEP 2 // 512 bytes per slot
|
||||
#define SFX_VOLUME 255
|
||||
#define SFX_CHANNEL_LEFT 0x00 // Upper 4 bits = output channel
|
||||
|
||||
// DOC freq formula: rate_Hz = freq * input_clk / (8 * num_oscs * 65536).
|
||||
// NTP's max_tracks_available = 31 + 1 interrupt osc => 32 active oscs.
|
||||
// IIgs DOC input clock = 7158000 Hz; (input_clk / 8 / 32) = 27961.
|
||||
// Inverted: freq_word = rate * 65536 / 27961. Caps usable rate at
|
||||
// ~27 kHz (above that freq_word overflows uint16).
|
||||
#define IIGS_DOC_DIVISOR 27961UL
|
||||
#define IIGS_MAX_SFX_RATE 27000U
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
static Handle gNTPHandle = NULL;
|
||||
static uint32_t gNTPBase = 0;
|
||||
static Handle gModuleHandle = NULL;
|
||||
static uint32_t gModuleBase = 0;
|
||||
static Handle gSfxHandle = NULL;
|
||||
static uint32_t gSfxBase = 0;
|
||||
static bool gNTPReady = false;
|
||||
static bool gNTPPlaying = false;
|
||||
|
||||
// SFX handle layout: stream structure first, sample bytes after.
|
||||
// Both end up at known 24-bit addresses, side-stepping the small
|
||||
// memory model's 16-bit pointer issue.
|
||||
//
|
||||
// Stream structure consumed by NTPstreamsound (per NTP docs):
|
||||
// 0..3 : sample pointer (4 bytes, little-endian, 24-bit in low 3)
|
||||
// 4..7 : sample length (4 bytes)
|
||||
// 8..9 : freq word (DOC frequency, 2 bytes)
|
||||
// 10 : DOC RAM page (512-aligned)
|
||||
// 11 : first oscillator (0..29)
|
||||
// 12 : playing-osc count (1..30; total oscs used = 1 + count)
|
||||
// 13 : volume of playing osc 0 (0..255)
|
||||
// 14 : channel of playing osc 0 (upper 4 bits)
|
||||
#define SFX_STRUCT_BYTES 16
|
||||
#define SFX_SAMPLE_OFFSET SFX_STRUCT_BYTES
|
||||
|
||||
// Self-modifying call stub. Bakes the X/Y/A register loads AND the
|
||||
// JSL target into the buffer, so the C-side inline asm only needs
|
||||
// `jsl gCallStub` -- no global-operand references in the asm block.
|
||||
//
|
||||
// (We tried the obvious `lda gAsmGlobal / jsl gJslStub` shape first;
|
||||
// ORCA's inline assembler accepts the first absolute-global operand
|
||||
// in a block but rejects the second and third with "invalid operand".
|
||||
// Folding the loads into the stub side-steps the issue entirely.)
|
||||
//
|
||||
// Layout (15 bytes):
|
||||
// 00: C2 30 REP #$30 ; 16-bit M/X
|
||||
// 02: A2 lo hi LDX #X
|
||||
// 05: A0 lo hi LDY #Y
|
||||
// 08: A9 lo hi LDA #A
|
||||
// 0B: 22 lo hi bk JSL target
|
||||
// 0F: 6B RTL
|
||||
static unsigned char gCallStub[16];
|
||||
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
||||
static void buildCallStub(uint32_t target, uint16_t x, uint16_t y, uint16_t a) {
|
||||
gCallStub[0] = 0xC2;
|
||||
gCallStub[1] = 0x30;
|
||||
gCallStub[2] = 0xA2;
|
||||
gCallStub[3] = (unsigned char)(x & 0xFFu);
|
||||
gCallStub[4] = (unsigned char)((x >> 8) & 0xFFu);
|
||||
gCallStub[5] = 0xA0;
|
||||
gCallStub[6] = (unsigned char)(y & 0xFFu);
|
||||
gCallStub[7] = (unsigned char)((y >> 8) & 0xFFu);
|
||||
gCallStub[8] = 0xA9;
|
||||
gCallStub[9] = (unsigned char)(a & 0xFFu);
|
||||
gCallStub[10] = (unsigned char)((a >> 8) & 0xFFu);
|
||||
gCallStub[11] = 0x22;
|
||||
gCallStub[12] = (unsigned char)(target & 0xFFu);
|
||||
gCallStub[13] = (unsigned char)((target >> 8) & 0xFFu);
|
||||
gCallStub[14] = (unsigned char)((target >> 16) & 0xFFu);
|
||||
gCallStub[15] = 0x6B;
|
||||
}
|
||||
|
||||
static bool loadNTP(void) {
|
||||
Handle h;
|
||||
Pointer p;
|
||||
FILE *fp;
|
||||
size_t bytesRead;
|
||||
|
||||
h = NewHandle(NTP_BUFFER_BYTES, _ownerid,
|
||||
attrFixed | attrLocked | attrPage | attrNoCross,
|
||||
NULL);
|
||||
if (h == NULL || _toolErr != 0) {
|
||||
return false;
|
||||
}
|
||||
HLock(h);
|
||||
p = *h;
|
||||
if (p == NULL) {
|
||||
DisposeHandle(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
fp = fopen(NTP_FILENAME, "rb");
|
||||
if (fp == NULL) {
|
||||
DisposeHandle(h);
|
||||
return false;
|
||||
}
|
||||
bytesRead = fread(p, 1, NTP_BUFFER_BYTES, fp);
|
||||
fclose(fp);
|
||||
if (bytesRead < NTP_MIN_BYTES) {
|
||||
DisposeHandle(h);
|
||||
return false;
|
||||
}
|
||||
|
||||
gNTPHandle = h;
|
||||
gNTPBase = (uint32_t)p;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ----- HAL API (alphabetical) -----
|
||||
|
||||
bool halAudioInit(void) {
|
||||
Handle modHandle;
|
||||
Handle sfxHandle;
|
||||
|
||||
if (gNTPReady) {
|
||||
return true;
|
||||
}
|
||||
if (!loadNTP()) {
|
||||
return false;
|
||||
}
|
||||
// A second fixed-bank handle holds the per-call .NTP module bytes.
|
||||
// BlockMove copies the caller's data here so we have a known 24-bit
|
||||
// address to pass to NTPprepare in X/Y.
|
||||
modHandle = NewHandle(NTP_BUFFER_BYTES, _ownerid,
|
||||
attrFixed | attrLocked | attrPage | attrNoCross,
|
||||
NULL);
|
||||
if (modHandle == NULL || _toolErr != 0) {
|
||||
DisposeHandle(gNTPHandle);
|
||||
gNTPHandle = NULL;
|
||||
return false;
|
||||
}
|
||||
HLock(modHandle);
|
||||
gModuleHandle = modHandle;
|
||||
gModuleBase = (uint32_t)*modHandle;
|
||||
|
||||
// Third fixed-bank handle for SFX sample storage. Same idea as
|
||||
// the module handle: NTPstreamsound wants a 24-bit pointer, and
|
||||
// the small-memory-model caller can only hand us a 16-bit one.
|
||||
sfxHandle = NewHandle(SFX_BUFFER_BYTES, _ownerid,
|
||||
attrFixed | attrLocked | attrPage | attrNoCross,
|
||||
NULL);
|
||||
if (sfxHandle == NULL || _toolErr != 0) {
|
||||
DisposeHandle(gModuleHandle);
|
||||
DisposeHandle(gNTPHandle);
|
||||
gModuleHandle = NULL;
|
||||
gNTPHandle = NULL;
|
||||
return false;
|
||||
}
|
||||
HLock(sfxHandle);
|
||||
gSfxHandle = sfxHandle;
|
||||
gSfxBase = (uint32_t)*sfxHandle;
|
||||
|
||||
gNTPReady = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioShutdown(void) {
|
||||
if (!gNTPReady) {
|
||||
return;
|
||||
}
|
||||
if (gNTPPlaying) {
|
||||
halAudioStopMod();
|
||||
}
|
||||
if (gSfxHandle != NULL) {
|
||||
DisposeHandle(gSfxHandle);
|
||||
gSfxHandle = NULL;
|
||||
gSfxBase = 0;
|
||||
}
|
||||
if (gModuleHandle != NULL) {
|
||||
DisposeHandle(gModuleHandle);
|
||||
gModuleHandle = NULL;
|
||||
gModuleBase = 0;
|
||||
}
|
||||
if (gNTPHandle != NULL) {
|
||||
DisposeHandle(gNTPHandle);
|
||||
gNTPHandle = NULL;
|
||||
gNTPBase = 0;
|
||||
}
|
||||
gNTPReady = false;
|
||||
}
|
||||
|
||||
|
||||
bool halAudioIsPlayingMod(void) {
|
||||
return gNTPReady && gNTPPlaying;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
||||
if (!gNTPReady || gModuleHandle == NULL) {
|
||||
return;
|
||||
}
|
||||
if (length == 0 || length > NTP_BUFFER_BYTES) {
|
||||
return;
|
||||
}
|
||||
// Drop any previous song before loading a new one.
|
||||
if (gNTPPlaying) {
|
||||
halAudioStopMod();
|
||||
}
|
||||
BlockMove((Pointer)data, (Pointer)gModuleBase, length);
|
||||
|
||||
// NTPprepare(modPtr in X/Y, doubling in A). modPtr is a 24-bit
|
||||
// address: low 16 bits in X, bank byte in Y (high byte is don't
|
||||
// care, low byte is the bank).
|
||||
buildCallStub(gNTPBase + 0,
|
||||
(uint16_t)(gModuleBase & 0xFFFFu),
|
||||
(uint16_t)((gModuleBase >> 16) & 0xFFu),
|
||||
0);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
}
|
||||
|
||||
// NTPplay(loopFlag in A). 0 = loop forever, 1 = play once.
|
||||
buildCallStub(gNTPBase + 3, 0, 0, loop ? 0 : 1);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
}
|
||||
gNTPPlaying = true;
|
||||
}
|
||||
|
||||
|
||||
void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint16_t rateHz) {
|
||||
unsigned char *sfx;
|
||||
uint32_t slotBase;
|
||||
uint32_t sampleAddr;
|
||||
uint32_t structAddr;
|
||||
uint16_t freqWord;
|
||||
uint16_t clampedRate;
|
||||
|
||||
if (!gNTPReady || gSfxHandle == NULL || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
if (length == 0 || length > (SFX_SLOT_BYTES - SFX_SAMPLE_OFFSET)) {
|
||||
return;
|
||||
}
|
||||
|
||||
clampedRate = (rateHz > IIGS_MAX_SFX_RATE) ? IIGS_MAX_SFX_RATE : rateHz;
|
||||
freqWord = (uint16_t)(((uint32_t)clampedRate * 65536UL) / IIGS_DOC_DIVISOR);
|
||||
|
||||
slotBase = gSfxBase + (uint32_t)slot * (uint32_t)SFX_SLOT_BYTES;
|
||||
structAddr = slotBase;
|
||||
sampleAddr = slotBase + SFX_SAMPLE_OFFSET;
|
||||
|
||||
// Copy the sample into this slot's fixed-bank region, converting
|
||||
// signed 8-bit (public API contract) to unsigned 8-bit (DOC RAM
|
||||
// format) by flipping the sign bit.
|
||||
{
|
||||
unsigned char *dst;
|
||||
uint32_t i;
|
||||
|
||||
dst = (unsigned char *)sampleAddr;
|
||||
for (i = 0; i < length; i++) {
|
||||
dst[i] = (unsigned char)(sample[i] ^ 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the stream structure in this slot's first 16 bytes.
|
||||
sfx = (unsigned char *)slotBase;
|
||||
sfx[0] = (unsigned char)(sampleAddr & 0xFFu);
|
||||
sfx[1] = (unsigned char)((sampleAddr >> 8) & 0xFFu);
|
||||
sfx[2] = (unsigned char)((sampleAddr >> 16) & 0xFFu);
|
||||
sfx[3] = 0;
|
||||
sfx[4] = (unsigned char)(length & 0xFFu);
|
||||
sfx[5] = (unsigned char)((length >> 8) & 0xFFu);
|
||||
sfx[6] = (unsigned char)((length >> 16) & 0xFFu);
|
||||
sfx[7] = (unsigned char)((length >> 24) & 0xFFu);
|
||||
sfx[8] = (unsigned char)(freqWord & 0xFFu);
|
||||
sfx[9] = (unsigned char)((freqWord >> 8) & 0xFFu);
|
||||
sfx[10] = (unsigned char)(SFX_BASE_DOC_PAGE + slot * SFX_DOC_PAGE_STEP);
|
||||
sfx[11] = (unsigned char)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT);
|
||||
sfx[12] = 1; // one playing osc
|
||||
sfx[13] = SFX_VOLUME;
|
||||
sfx[14] = SFX_CHANNEL_LEFT;
|
||||
|
||||
// NTPstreamsound(structPtr in X/Y). Same 24-bit address packing
|
||||
// pattern as NTPprepare: low 16 in X, bank in Y.
|
||||
buildCallStub(gNTPBase + 24,
|
||||
(uint16_t)(structAddr & 0xFFFFu),
|
||||
(uint16_t)((structAddr >> 16) & 0xFFu),
|
||||
0);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopMod(void) {
|
||||
if (!gNTPReady || !gNTPPlaying) {
|
||||
return;
|
||||
}
|
||||
buildCallStub(gNTPBase + 6, 0, 0, 0);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
}
|
||||
gNTPPlaying = false;
|
||||
}
|
||||
|
||||
|
||||
void halAudioStopSfx(uint8_t slot) {
|
||||
uint8_t firstOsc;
|
||||
uint8_t i;
|
||||
|
||||
if (!gNTPReady || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstOsc = (uint8_t)(SFX_BASE_OSC + slot * SFX_OSCS_PER_SLOT);
|
||||
|
||||
// NTP has no SFX-stop entry point. We poke DOC oscillator control
|
||||
// registers directly to halt the slot's osc pair. SEI/CLI keeps
|
||||
// NTP's DOC IRQ from racing our sound_address writes.
|
||||
asm {
|
||||
sei
|
||||
}
|
||||
*DOC_SOUND_CONTROL = *DOC_IRQ_VOLUME;
|
||||
for (i = 0; i < SFX_OSCS_PER_SLOT; i++) {
|
||||
*DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_CTRL_BASE + firstOsc + i);
|
||||
*DOC_SOUND_DATA = DOC_OSC_HALT_BITS;
|
||||
}
|
||||
asm {
|
||||
cli
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void halAudioFrameTick(void) {
|
||||
// NTP is DOC-IRQ driven; nothing for the host to pump.
|
||||
}
|
||||
|
|
@ -45,6 +45,33 @@
|
|||
static uint8_t gPreviousNewVideo = 0;
|
||||
static bool gModeSet = false;
|
||||
|
||||
// Last-uploaded SCB and palette. Both registers live in bank $E1; on a
|
||||
// 2.8 MHz 65816 the 200+512-byte memcpy across the bank boundary is a
|
||||
// real cost when it runs every present. Caching here lets the typical
|
||||
// game loop (which mutates pixels but rarely SCB/palette) skip the
|
||||
// upload entirely on clean frames.
|
||||
static uint8_t gCachedScb [SURFACE_HEIGHT];
|
||||
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
|
||||
static bool gCacheValid = false;
|
||||
|
||||
// Upload SCB and palette into bank-$E1 SHR memory only when they have
|
||||
// changed since the last call. paletteOrScbChanged returns false when
|
||||
// the cache is already in sync, in which case both memcpys to $E1 are
|
||||
// skipped.
|
||||
static void uploadScbAndPaletteIfNeeded(const SurfaceT *src) {
|
||||
if (gCacheValid
|
||||
&& memcmp(gCachedScb, src->scb, sizeof(gCachedScb)) == 0
|
||||
&& memcmp(gCachedPalette, src->palette, sizeof(gCachedPalette)) == 0) {
|
||||
return;
|
||||
}
|
||||
memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT);
|
||||
memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette));
|
||||
memcpy(gCachedScb, src->scb, sizeof(gCachedScb));
|
||||
memcpy(gCachedPalette, src->palette, sizeof(gCachedPalette));
|
||||
gCacheValid = true;
|
||||
}
|
||||
|
||||
|
||||
// ----- HAL API (alphabetical) -----
|
||||
|
||||
bool halInit(const JoeyConfigT *config) {
|
||||
|
|
@ -65,9 +92,8 @@ void halPresent(const SurfaceT *src) {
|
|||
if (src == NULL) {
|
||||
return;
|
||||
}
|
||||
uploadScbAndPaletteIfNeeded(src);
|
||||
memcpy(IIGS_SHR_PIXELS, src->pixels, SURFACE_PIXELS_SIZE);
|
||||
memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT);
|
||||
memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -81,12 +107,7 @@ void halPresentRect(const SurfaceT *src, int16_t x, int16_t y, uint16_t w, uint1
|
|||
return;
|
||||
}
|
||||
|
||||
// SCBs and palettes track the whole surface, not just the rect --
|
||||
// cheap enough on IIgs (200 bytes + 512 bytes) and avoids tracking
|
||||
// which palette/SCB regions changed. A future optimization can
|
||||
// limit these to the affected scanlines.
|
||||
memcpy(IIGS_SHR_SCB, src->scb, SURFACE_HEIGHT);
|
||||
memcpy(IIGS_SHR_PALETTE, src->palette, sizeof(src->palette));
|
||||
uploadScbAndPaletteIfNeeded(src);
|
||||
|
||||
// Pixel copy: byte-aligned runs per scanline. x is always even
|
||||
// after API-level clipping for 4bpp packed if caller aligned it;
|
||||
|
|
|
|||
|
|
@ -197,29 +197,42 @@ static void pollJoystick(void) {
|
|||
uint8_t px;
|
||||
uint8_t py;
|
||||
uint8_t byte;
|
||||
bool xResolved;
|
||||
bool yResolved;
|
||||
|
||||
// One PTRIG read starts BOTH paddle timers simultaneously per the
|
||||
// IIgs Hardware Reference. Polling them in parallel halves the
|
||||
// wall-clock time vs. polling each serially after its own trigger.
|
||||
byte = *IIGS_PTRIG;
|
||||
px = 255;
|
||||
px = 0;
|
||||
py = 0;
|
||||
xResolved = false;
|
||||
yResolved = false;
|
||||
for (count = 0; count < PADDLE_TIMEOUT; count++) {
|
||||
if (!xResolved) {
|
||||
byte = *IIGS_PADDLE0;
|
||||
if ((byte & IIGS_PADDLE_BUSY) == 0) {
|
||||
px = (uint8_t)count;
|
||||
break;
|
||||
xResolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
byte = *IIGS_PTRIG;
|
||||
py = 255;
|
||||
for (count = 0; count < PADDLE_TIMEOUT; count++) {
|
||||
if (!yResolved) {
|
||||
byte = *IIGS_PADDLE1;
|
||||
if ((byte & IIGS_PADDLE_BUSY) == 0) {
|
||||
py = (uint8_t)count;
|
||||
yResolved = true;
|
||||
}
|
||||
}
|
||||
if (xResolved && yResolved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
gJoyAxisX[JOYSTICK_0] = thresholdPaddle(px);
|
||||
gJoyAxisY[JOYSTICK_0] = thresholdPaddle(py);
|
||||
// Timed-out paddles default to centered axis. Without an explicit
|
||||
// resolved flag we couldn't distinguish "no joystick" from "stick
|
||||
// hard right" -- both would yield px=255 and report AXIS_MAX.
|
||||
gJoyAxisX[JOYSTICK_0] = xResolved ? thresholdPaddle(px) : 0;
|
||||
gJoyAxisY[JOYSTICK_0] = yResolved ? thresholdPaddle(py) : 0;
|
||||
gJoyButtonState[JOYSTICK_0][JOY_BUTTON_0] = (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0;
|
||||
gJoyButtonState[JOYSTICK_0][JOY_BUTTON_1] = (*IIGS_BTN1 & IIGS_BUTTON_BIT) != 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -180,11 +180,79 @@ should_install() {
|
|||
# IIgs: Merlin32 (free), ORCA/C (manual), GoldenGate (manual)
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
install_audio_libxmp() {
|
||||
header "libxmp-lite (DOS / ST audio decoder)"
|
||||
local base="${SCRIPT_DIR}/audio"
|
||||
local target="${base}/libxmp-lite"
|
||||
local key="audio_libxmp"
|
||||
local version="4.7.0"
|
||||
|
||||
if [ "${FORCE_INSTALL}" -eq 1 ]; then
|
||||
rm -rf "${target}"
|
||||
clear_done "${key}"
|
||||
fi
|
||||
if is_done "${key}" && [ -f "${target}/include/libxmp-lite/xmp.h" ]; then
|
||||
ok "libxmp-lite ${version} already installed"
|
||||
STATUS[${key}]="ok"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Downloading libxmp-lite ${version} from libxmp.org"
|
||||
local tarball="${CACHE_DIR}/libxmp-lite-${version}.tar.gz"
|
||||
local url="https://github.com/libxmp/libxmp/releases/download/libxmp-${version}/libxmp-lite-${version}.tar.gz"
|
||||
if ! download "${url}" "${tarball}"; then
|
||||
STATUS[${key}]="failed"
|
||||
INSTRUCTIONS[${key}]="Could not download ${url}"
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "${base}"
|
||||
rm -rf "${target}"
|
||||
if (cd "${base}" && tar xzf "${tarball}") && [ -d "${base}/libxmp-lite-${version}" ]; then
|
||||
mv "${base}/libxmp-lite-${version}" "${target}"
|
||||
mark_done "${key}"
|
||||
ok "libxmp-lite installed at ${target}"
|
||||
STATUS[${key}]="ok"
|
||||
else
|
||||
STATUS[${key}]="failed"
|
||||
INSTRUCTIONS[${key}]="Extract of ${tarball} did not produce libxmp-lite-${version}/"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
install_iigs() {
|
||||
header "IIgs toolchain"
|
||||
local base="${SCRIPT_DIR}/iigs"
|
||||
mkdir -p "${base}"
|
||||
|
||||
# PHP CLI is required by tools/joeymod when converting .MOD files
|
||||
# to NinjaTrackerPlus's .NTP runtime format. Installed once here so
|
||||
# the IIgs audio asset pipeline works out of the box.
|
||||
if command -v php >/dev/null 2>&1; then
|
||||
ok "php CLI already on PATH"
|
||||
STATUS[iigs_php]="ok"
|
||||
else
|
||||
info "Installing php CLI for joeymod's MOD->NTP converter"
|
||||
local php_pkgs=""
|
||||
case "${HOST_OS}" in
|
||||
linux) php_pkgs="php-cli" ;;
|
||||
macos) php_pkgs="php" ;;
|
||||
esac
|
||||
if [ -n "${php_pkgs}" ] && pkg_install ${php_pkgs}; then
|
||||
ok "php installed"
|
||||
STATUS[iigs_php]="ok"
|
||||
else
|
||||
STATUS[iigs_php]="missing"
|
||||
INSTRUCTIONS[iigs_php]=$(cat <<EOF
|
||||
php CLI not installed. Required by tools/joeymod for the IIgs
|
||||
.MOD -> .NTP conversion path.
|
||||
Debian/Ubuntu: sudo apt install php-cli
|
||||
macOS Homebrew: brew install php
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
fi
|
||||
|
||||
# Merlin32 -- 65816 cross-assembler from Brutal Deluxe.
|
||||
# The canonical distribution is a zip on brutaldeluxe.fr that bundles
|
||||
# prebuilt binaries for Linux / macOS / Windows plus the C source.
|
||||
|
|
@ -465,6 +533,51 @@ EOF
|
|||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# NinjaTrackerPlus -- Ninjaforce's IIgs MOD player. Distributed as
|
||||
# source (ZIP) plus binary tool disk images on ninjaforce.com.
|
||||
# Redistribution is granted by the copyright holder per the user's
|
||||
# standing permission. We download the source archive (extracted
|
||||
# into ${base}/ntp), the demo disk (kept in cache), and the tool
|
||||
# disk (kept in cache) so a fresh checkout can build the IIgs
|
||||
# audio HAL without manual intervention.
|
||||
if [ "${FORCE_INSTALL}" -eq 1 ]; then
|
||||
rm -rf "${base}/ntp"
|
||||
clear_done "iigs_ntp"
|
||||
fi
|
||||
if is_done "iigs_ntp" && [ -f "${base}/ntp/ninjatrackerplus.s" ]; then
|
||||
ok "NinjaTrackerPlus already installed"
|
||||
STATUS[iigs_ntp]="ok"
|
||||
else
|
||||
info "Downloading NinjaTrackerPlus from ninjaforce.com"
|
||||
local ntp_src_zip="${CACHE_DIR}/ntpsources.zip"
|
||||
local ntp_demo="${CACHE_DIR}/ntp.2mg"
|
||||
local ntp_tool="${CACHE_DIR}/ninjatrackerplus_tool222_v1.4.2mg"
|
||||
local ntp_failed=0
|
||||
download "https://www.ninjaforce.com/downloads/ntpsources.zip" "${ntp_src_zip}" || ntp_failed=1
|
||||
download "https://www.ninjaforce.com/downloads/ntp.2mg" "${ntp_demo}" || true
|
||||
download "https://www.ninjaforce.com/downloads/ninjatrackerplus_tool222_v1.4.2mg" "${ntp_tool}" || true
|
||||
if [ "${ntp_failed}" -eq 1 ]; then
|
||||
STATUS[iigs_ntp]="failed"
|
||||
INSTRUCTIONS[iigs_ntp]="Could not download ntpsources.zip from www.ninjaforce.com"
|
||||
else
|
||||
mkdir -p "${base}/ntp"
|
||||
if (cd "${base}/ntp" && unzip -oq "${ntp_src_zip}") && [ -f "${base}/ntp/ninjatrackerplus.s" ]; then
|
||||
# ntpsources.zip ships .s files with CRLF line
|
||||
# endings; Merlin32 rejects those as "Unknown line".
|
||||
# Normalize once here so the build doesn't have to.
|
||||
for f in "${base}/ntp"/*.s; do
|
||||
sed -i 's/\r$//' "$f"
|
||||
done
|
||||
mark_done "iigs_ntp"
|
||||
ok "NinjaTrackerPlus installed at ${base}/ntp"
|
||||
STATUS[iigs_ntp]="ok"
|
||||
else
|
||||
STATUS[iigs_ntp]="failed"
|
||||
INSTRUCTIONS[iigs_ntp]="Extract of ${ntp_src_zip} did not produce ninjatrackerplus.s"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
|
|
@ -1317,6 +1430,14 @@ main() {
|
|||
if should_install atarist; then install_atarist; fi
|
||||
if should_install dos; then install_dos; fi
|
||||
|
||||
# Audio third-party fetches that span multiple targets. libxmp-lite
|
||||
# is used by the DOS HAL today and will back the ST mixer's
|
||||
# Protracker decoder later, so it lives in toolchains/audio/ rather
|
||||
# than under any one platform's tree.
|
||||
if should_install dos || should_install atarist; then
|
||||
install_audio_libxmp
|
||||
fi
|
||||
|
||||
if [ "${SKIP_EMULATORS}" -eq 0 ] && [ -z "${ONLY_PLATFORM}" ]; then
|
||||
install_emulators
|
||||
fi
|
||||
|
|
|
|||
100
tools/joeymod/joeymod
Executable file
100
tools/joeymod/joeymod
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/usr/bin/env bash
|
||||
# joeymod: convert a Protracker .MOD into the runtime form a target
|
||||
# platform expects.
|
||||
#
|
||||
# Usage:
|
||||
# joeymod input.mod output.mod -- passthrough copy + magic validation
|
||||
# (DOS / ST runtimes detect song end
|
||||
# inside libxmp-lite, so the file is
|
||||
# used as-is)
|
||||
# joeymod input.mod output.amod -- passthrough + E8FF stop-marker
|
||||
# pattern injected at song end. The
|
||||
# Amiga audio HAL relies on E8FF to
|
||||
# honor PlayMod(loop=false) since
|
||||
# PTPlayer has no native song-end
|
||||
# signal. Other replayers ignore
|
||||
# E8FF (or treat it as a filter
|
||||
# toggle), so .amod is harmless if
|
||||
# fed to the wrong port.
|
||||
# joeymod input.mod output.ntp -- runs the PHP ntpconverter staged
|
||||
# at toolchains/iigs/ntp/ntpconverter.php
|
||||
# (NinjaTrackerPlus' .NTP runtime
|
||||
# format on the IIgs)
|
||||
#
|
||||
# The output extension picks the conversion. Build rules per platform
|
||||
# call this with whichever pair their HAL needs.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "usage: joeymod input.mod output.{mod,amod,ntp}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
in=$1
|
||||
out=$2
|
||||
|
||||
if [[ ! -f $in ]]; then
|
||||
echo "joeymod: input not found: $in" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# All Protracker-derived .MOD variants store a 4-byte signature at
|
||||
# offset 1080. Reject anything that does not match a known one --
|
||||
# better to fail at build time than ship a corrupted audio asset.
|
||||
magic=$(dd if="$in" bs=1 count=4 skip=1080 status=none)
|
||||
case "$magic" in
|
||||
M.K.|M!K!|FLT4|FLT8|6CHN|8CHN|CD81|OKTA|OCTA) ;;
|
||||
*)
|
||||
printf 'joeymod: %s does not look like a .MOD (magic %q at offset 1080)\n' "$in" "$magic" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
ext="${out##*.}"
|
||||
ext="${ext,,}"
|
||||
|
||||
case "$ext" in
|
||||
mod)
|
||||
cp "$in" "$out"
|
||||
;;
|
||||
amod)
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
modloopend="$repo/build/tools/modloopend"
|
||||
if [[ ! -x $modloopend ]]; then
|
||||
echo "joeymod: $modloopend missing; run 'make tools' first" >&2
|
||||
exit 2
|
||||
fi
|
||||
"$modloopend" "$in" "$out"
|
||||
;;
|
||||
ntp)
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
ntp_dir="$repo/toolchains/iigs/ntp"
|
||||
if [[ ! -f $ntp_dir/ntpconverter.php ]]; then
|
||||
echo "joeymod: $ntp_dir/ntpconverter.php missing; run toolchains/install.sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! command -v php >/dev/null 2>&1; then
|
||||
echo "joeymod: php CLI not found; install php-cli" >&2
|
||||
exit 2
|
||||
fi
|
||||
# ntpconverter.php writes <basename>.ntp next to its input. Stage
|
||||
# in a scratch dir so we own the output path and don't pollute
|
||||
# the caller's source tree.
|
||||
tmp=$(mktemp -d -t joeymod.XXXXXX)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
cp "$in" "$tmp/song.MOD"
|
||||
( cd "$ntp_dir" && php ntpconverter.php "$tmp/song.MOD" >/dev/null )
|
||||
if [[ ! -f $tmp/song.ntp ]]; then
|
||||
echo "joeymod: ntpconverter.php did not produce an .ntp from $in" >&2
|
||||
exit 2
|
||||
fi
|
||||
cp "$tmp/song.ntp" "$out"
|
||||
;;
|
||||
*)
|
||||
echo "joeymod: unknown output extension '.$ext' (expected .mod, .amod, or .ntp)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
printf 'joeymod: %s -> %s (%d bytes)\n' "$in" "$out" "$(stat -c %s "$out")"
|
||||
221
tools/modloopend/modloopend.c
Normal file
221
tools/modloopend/modloopend.c
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// Append a one-shot stop marker to a Protracker .MOD file so the Amiga
|
||||
// audio HAL (PTPlayer-based) can honor `joeyAudioPlayMod(loop=false)`.
|
||||
//
|
||||
// PTPlayer has no built-in song-end signal -- songs loop forever
|
||||
// unless an `E8FF` effect command is encountered, which latches into
|
||||
// the public `mt_E8Trigger` byte. Our halAudioFrameTick polls that
|
||||
// byte and clears mt_Enable when the marker fires, but the marker has
|
||||
// to actually be IN the .MOD for the polling to ever match.
|
||||
//
|
||||
// This tool injects a new pattern at the end of the song that
|
||||
// contains exactly one E8FF effect on row 0 and is otherwise empty.
|
||||
// The order table is extended to play that pattern once after the
|
||||
// existing song. Result: the song plays normally, then the marker
|
||||
// pattern fires, mt_E8Trigger goes 0xFF, and PlayMod(loop=false)
|
||||
// stops cleanly. Other replayers (libxmp on DOS/ST, NTP on IIgs)
|
||||
// either ignore E8FF or treat it as a no-op filter command -- on
|
||||
// those ports the engine's own song-end detection is what stops the
|
||||
// song, so the injected marker is harmless there.
|
||||
//
|
||||
// Usage: modloopend input.mod output.mod
|
||||
//
|
||||
// The existing pattern data is not modified -- only the order table,
|
||||
// song length, and a new pattern at the end are written.
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MOD_HEADER_SIZE 1084
|
||||
#define MOD_SONG_LENGTH_OFFSET 950
|
||||
#define MOD_RESTART_OFFSET 951
|
||||
#define MOD_ORDER_TABLE_OFFSET 952
|
||||
#define MOD_ORDER_TABLE_LEN 128
|
||||
#define MOD_MAGIC_OFFSET 1080
|
||||
#define MOD_MAGIC_LEN 4
|
||||
#define MOD_PATTERN_BYTES 1024
|
||||
#define MOD_MAX_PATTERNS 128
|
||||
#define MOD_MIN_BYTES (MOD_HEADER_SIZE + MOD_PATTERN_BYTES)
|
||||
|
||||
// E8FF on row 0, channel 0. Per Protracker pattern row layout:
|
||||
// byte 0 (SSSSPPPP): sample hi nibble + period hi nibble = 0x00
|
||||
// byte 1 (PPPPPPPP): period lo byte = 0x00
|
||||
// byte 2 (SSSSEEEE): sample lo nibble + effect cmd = 0x0E
|
||||
// byte 3 (PPPPPPPP): effect param = 0xFF
|
||||
#define E8FF_BYTE_0 0x00
|
||||
#define E8FF_BYTE_1 0x00
|
||||
#define E8FF_BYTE_2 0x0E
|
||||
#define E8FF_BYTE_3 0xFF
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static bool magicMatches(const uint8_t *header);
|
||||
static int process(const char *inPath, const char *outPath);
|
||||
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static bool magicMatches(const uint8_t *header) {
|
||||
static const char *known[] = {
|
||||
"M.K.", "M!K!", "FLT4", "FLT8", "6CHN", "8CHN", "CD81", "OKTA", "OCTA"
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(known) / sizeof(known[0]); i++) {
|
||||
if (memcmp(header + MOD_MAGIC_OFFSET, known[i], MOD_MAGIC_LEN) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static int process(const char *inPath, const char *outPath) {
|
||||
FILE *fp;
|
||||
uint8_t *buf;
|
||||
long fileSize;
|
||||
size_t readBytes;
|
||||
size_t newSize;
|
||||
int i;
|
||||
int songLength;
|
||||
int maxPattern;
|
||||
int patternCount;
|
||||
long samplesOffset;
|
||||
long samplesLen;
|
||||
uint8_t *newBuf;
|
||||
uint8_t *newPatternStart;
|
||||
|
||||
fp = fopen(inPath, "rb");
|
||||
if (fp == NULL) {
|
||||
fprintf(stderr, "modloopend: cannot open %s\n", inPath);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
fprintf(stderr, "modloopend: seek failed on %s\n", inPath);
|
||||
fclose(fp);
|
||||
return 2;
|
||||
}
|
||||
fileSize = ftell(fp);
|
||||
if (fileSize < MOD_MIN_BYTES) {
|
||||
fprintf(stderr, "modloopend: %s is %ld bytes, too small for a MOD\n", inPath, fileSize);
|
||||
fclose(fp);
|
||||
return 2;
|
||||
}
|
||||
rewind(fp);
|
||||
|
||||
buf = (uint8_t *)malloc((size_t)fileSize);
|
||||
if (buf == NULL) {
|
||||
fprintf(stderr, "modloopend: out of memory\n");
|
||||
fclose(fp);
|
||||
return 2;
|
||||
}
|
||||
readBytes = fread(buf, 1, (size_t)fileSize, fp);
|
||||
fclose(fp);
|
||||
if (readBytes != (size_t)fileSize) {
|
||||
fprintf(stderr, "modloopend: short read on %s\n", inPath);
|
||||
free(buf);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!magicMatches(buf)) {
|
||||
fprintf(stderr, "modloopend: %s does not have a Protracker magic at offset 1080\n", inPath);
|
||||
free(buf);
|
||||
return 2;
|
||||
}
|
||||
|
||||
songLength = buf[MOD_SONG_LENGTH_OFFSET];
|
||||
if (songLength <= 0 || songLength >= MOD_ORDER_TABLE_LEN) {
|
||||
fprintf(stderr, "modloopend: %s has unusable song length %d\n", inPath, songLength);
|
||||
free(buf);
|
||||
return 2;
|
||||
}
|
||||
|
||||
maxPattern = 0;
|
||||
for (i = 0; i < MOD_ORDER_TABLE_LEN; i++) {
|
||||
int p = buf[MOD_ORDER_TABLE_OFFSET + i];
|
||||
if (p > maxPattern) {
|
||||
maxPattern = p;
|
||||
}
|
||||
}
|
||||
patternCount = maxPattern + 1;
|
||||
if (patternCount >= MOD_MAX_PATTERNS) {
|
||||
fprintf(stderr, "modloopend: %s already uses pattern %d, no room for stop marker\n",
|
||||
inPath, maxPattern);
|
||||
free(buf);
|
||||
return 2;
|
||||
}
|
||||
|
||||
samplesOffset = MOD_HEADER_SIZE + (long)patternCount * MOD_PATTERN_BYTES;
|
||||
if (samplesOffset > fileSize) {
|
||||
fprintf(stderr, "modloopend: %s claims %d patterns but file is only %ld bytes\n",
|
||||
inPath, patternCount, fileSize);
|
||||
free(buf);
|
||||
return 2;
|
||||
}
|
||||
samplesLen = fileSize - samplesOffset;
|
||||
|
||||
newSize = (size_t)fileSize + MOD_PATTERN_BYTES;
|
||||
newBuf = (uint8_t *)malloc(newSize);
|
||||
if (newBuf == NULL) {
|
||||
fprintf(stderr, "modloopend: out of memory\n");
|
||||
free(buf);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Copy header + existing patterns.
|
||||
memcpy(newBuf, buf, (size_t)samplesOffset);
|
||||
|
||||
// New pattern: 1024 bytes of zeros except row 0 channel 0 = E8FF.
|
||||
newPatternStart = newBuf + samplesOffset;
|
||||
memset(newPatternStart, 0, MOD_PATTERN_BYTES);
|
||||
newPatternStart[0] = E8FF_BYTE_0;
|
||||
newPatternStart[1] = E8FF_BYTE_1;
|
||||
newPatternStart[2] = E8FF_BYTE_2;
|
||||
newPatternStart[3] = E8FF_BYTE_3;
|
||||
|
||||
// Append samples after the new pattern.
|
||||
if (samplesLen > 0) {
|
||||
memcpy(newBuf + samplesOffset + MOD_PATTERN_BYTES, buf + samplesOffset, (size_t)samplesLen);
|
||||
}
|
||||
|
||||
// Patch song length: now plays one extra position pointing at the
|
||||
// new pattern index.
|
||||
newBuf[MOD_SONG_LENGTH_OFFSET] = (uint8_t)(songLength + 1);
|
||||
newBuf[MOD_ORDER_TABLE_OFFSET + songLength] = (uint8_t)patternCount;
|
||||
|
||||
free(buf);
|
||||
|
||||
fp = fopen(outPath, "wb");
|
||||
if (fp == NULL) {
|
||||
fprintf(stderr, "modloopend: cannot open %s for writing\n", outPath);
|
||||
free(newBuf);
|
||||
return 2;
|
||||
}
|
||||
if (fwrite(newBuf, 1, newSize, fp) != newSize) {
|
||||
fprintf(stderr, "modloopend: short write on %s\n", outPath);
|
||||
fclose(fp);
|
||||
free(newBuf);
|
||||
return 2;
|
||||
}
|
||||
fclose(fp);
|
||||
free(newBuf);
|
||||
|
||||
printf("modloopend: %s -> %s (added pattern %d as stop marker, %zu bytes)\n",
|
||||
inPath, outPath, patternCount, newSize);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "usage: modloopend input.mod output.mod\n");
|
||||
return 2;
|
||||
}
|
||||
return process(argv[1], argv[2]);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue