gb: vgm3 player preset

This commit is contained in:
Steven Hugg committed 2026-10-07 15:53:30 +02:00
1 parent 6f805e3c7e
commit b3cf4e159d
8 files changed
+795 -3

No files matched your search

-2
View File
@@ -184,8 +184,6 @@ void start_music(const byte* music) {
void main(void)
{
unsigned char s;
clrscr();
gotoxy(8, 4);
cputs("MOCKINGBOARD DEMO");
+105
View File
@@ -0,0 +1,105 @@
/* vgr3_gb.c -- Game Boy DMG playback of a .vgr3 file, the GB sibling of
* vgr3_coleco.c / vgr3_nes.c / vgr3_pokey.c.
*
* The DMG sound registers are a memory-mapped block. The VGR3 shadow
* layout flattens two disjoint hardware ranges into one index space:
*
* 0x00-0x16 NR10-NR52 ($FF10-$FF26)
* 0x20-0x2F wave RAM ($FF30-$FF3F)
*
* Both map to the same address formula, $FF10 + r, so the flush is just
* "write every dirty byte" in ascending order. The hole at 0x17-0x1F
* ($FF27-$FF2F, unused IO) and 0x30+ are skipped; the encoder never
* dirties them, but we test the range rather than trust that.
*
* Ascending order puts the per-channel NRx0-NRx4 setup and NR52 (0x16,
* sound enable) ahead of the wave RAM reload, which is what the source
* VGM stream wants: a channel must be configured before it can be
* triggered, and wave RAM is typically rewritten while CH3 is off.
*
* The decoder only marks a byte dirty when an op writes it, and the
* encoder replays "side-effect" bytes whenever the source writes them
* even if the value is unchanged. Those extra bytes for the DMG are the
* NRx4 trigger bits (a value-equal write still retriggers a voice) and
* the NRx1 length loads (a value-equal write still reloads the length
* counter while length is enabled). So a value-equal write that still
* has a hardware effect is preserved by the dirty bitmap.
*
* Like the other ports this reads its .vgr3 file in place, so the file
* must live in ROM.
*/
#include <stdint.h>
#include "gb/types.h"
#include "gb/hardware.h"
#include "gb/gb.h"
#include "gbtext.h"
#include "vgr3_play.h"
//#link "vgr3_play.c"
const unsigned char MUSIC[] = {
#embed "tune.vgr3"
};
#pragma opt_code_speed
/* 0x00-0x16 -> $FF10-$FF26, 0x20-0x2F -> $FF30-$FF3F. */
static void gbSoundOut(uint8_t reg, uint8_t val) {
((volatile uint8_t *)0xFF10)[reg] = val;
}
static Vgr3Player g_player;
static void gbFlush8(uint8_t r0) {
uint8_t dirty = g_player.dirty[r0];
uint8_t r;
if (!dirty) return;
g_player.dirty[r0] = 0;
r = r0 << 3;
while (dirty) {
if (dirty & 1) {
gbSoundOut(r, g_player.regs[r]);
}
r++;
dirty >>= 1;
}
}
void gbFlushVgr(void) {
gbFlush8(0); // 0x0
gbFlush8(1); // 0x8
gbFlush8(2); // 0x10 - 0x16
gbFlush8(4); // 0x20
gbFlush8(5); // 0x28
}
const char HEX_CHAR[] = "0123456789ABCDEF";
void main(void) {
uint16_t frame = 0;
char hexstr[5] = "0000\0";
DISPLAY_OFF;
BGP_REG = 0xE4;
font_init();
put_str(2, 2, "VGR3 PLAYER");
put_str(2, 4, "FRAME");
SHOW_BKG;
DISPLAY_ON;
if (!vgr3Init(&g_player, MUSIC)) {
put_str(2, 8, "BAD FILE");
while (1) wait_vbl_done();
}
while (1) {
wait_vbl_done(); // once per frame, in vblank
gbFlushVgr(); // send them to the APU
vgr3Frame(&g_player); // decode this frame's register writes
frame++;
hexstr[3] = HEX_CHAR[frame & 0xf];
hexstr[2] = HEX_CHAR[(frame >> 4) & 0xf];
hexstr[1] = HEX_CHAR[(frame >> 8) & 0xf];
hexstr[0] = HEX_CHAR[(frame >> 12) & 0xf];
put_str(8, 4, hexstr);
}
}
Binary file not shown.
+123
View File
@@ -0,0 +1,123 @@
/* vgr3_format.h -- VGR3 ("per-voice channel") file format. See new.md.
*
* A song is a small set of channels. Each channel owns a contiguous
* window [base, base+width) of a global shadow register file and runs
* its own opcode stream. Ops write bytes into the register file and
* mark them dirty; platform glue flushes dirty bytes once per frame.
*
* All multi-byte fields are little-endian.
*
* Header (16 bytes):
* u8 magic[4] "VGR3"
* u8 version VGR3_VERSION
* u8 chipType VGR3_CHIP_*
* u8 tickRate frames/second, e.g. 60
* u8 numChans
* u8 dictCount 0..VGR3_DICT_MAX
* u8 reserved 0
* u16 dataSize bytes in the data blob
* u16 totalFrames song length (frame of the loop JUMP), <= 0xFFFE
* u16 loopFrame 0xFFFF if the song doesn't loop (info only:
* the decoder just follows each channel's JUMP).
* vgm2vgr3 --loop turns a source with no loop
* point into a loop-at-frame-0 file (loopFrame 0,
* each channel ends in a JUMP back to its start)
* Channel table [numChans] (4 bytes each):
* u8 base first register-file byte this channel writes
* u8 width W: number of bytes in its window
* u16 start offset of its first op in the data blob
* Dict table [dictCount] (2 bytes each):
* u16 offset of a SET, CALLM or CALLL item in the data blob
* Data blob (all u16 addresses below are offsets into it)
*
* Opcodes (one byte, operands follow):
*
* 1mmm mwww SET mask = (op & 0x7F) >> K, wait = op & ((1<<K)-1),
* K = 7-W (W < 8) or 7 (W >= 8, no mask bits).
* One value byte follows per set mask bit (bit 0 =
* base+0). If wait == 0 a u8 wait (1-255) follows
* the values. mask == 0 is a pure WAIT.
* 0x00-0x5B DICT execute the item at dict[op] (pc not moved): a
* SET, or a CALLM/CALLL that returns to after the
* DICT op (one item of the caller, like a CALL)
* 0x5C EXT prefix for a bulk op: the next byte selects it.
* Current bulk ops:
* 0x00 LOAD: W value bytes (every byte of the
* window); no wait, so a WAIT item follows (the
* same wave then repeats as the same bytes)
* (more bulk ops can be added as sub-opcodes here)
* 0x5D JUMP u16 addr (loop; top level only)
* 0x5E END hold forever
* 0x5F CALLL u16 addr, u8 count (1-255)
* 0x6n CALLS u8 dist: addr = (address of this op) - dist,
* count = n+1
* 0x7n CALLM u16 addr, count = n+1
*
* An item is one op at the current level (a CALL is one item of its
* caller, however much it runs). CALL runs `count` items starting at
* addr, which may themselves be CALLs, up to VGR3_MAX_DEPTH levels,
* then returns. Every SET item ends with a wait >= 1; each frame a
* channel executes items until one sets a wait.
*/
#ifndef VGR3_FORMAT_H
#define VGR3_FORMAT_H
#define VGR3_MAGIC "VGR3"
#define VGR3_VERSION 2
#define VGR3_HEADER_SIZE 16
#define VGR3_LOOP_NONE 0xFFFFu
#define VGR3_CHAN_SIZE 4
enum {
VGR3_CHIP_SN76489 = 1,
VGR3_CHIP_AY8910 = 2,
VGR3_CHIP_NES_APU = 3,
VGR3_CHIP_GB_DMG = 4,
VGR3_CHIP_POKEY = 5,
VGR3_CHIP_SID = 6
};
/* Register-file layouts, per chip:
* SN76489: 0-8 = tone0 lo4, tone0 hi6, vol0, tone1 lo4, ..., vol2;
* 9 = noise control, 10 = noise vol
* AY8910: 0-13 = R0-R13
* NES APU: 0x00-0x17 = $4000-$4017
* GB DMG: 0x00-0x16 = NR10-NR52 ($FF10-$FF26),
* 0x20-0x2F = wave RAM ($FF30-$FF3F)
* POKEY: 0x00-0x0A = AUDF1/AUDC1/AUDF2/AUDC2/AUDF3/AUDC3/AUDF4/AUDC4,
* AUDCTL, STIMER, SKRES ($D200-$D20A); POT/POTGO are ignored
* SID: 0x00-0x06 = V1 freq(2), PW(2), control, AD, SR; 0x07-0x0D = V2;
* 0x0E-0x14 = V3; 0x15-0x18 = filter cutoff(2), res/routing,
* mode/volume ($D400-$D418). The read-only POT/OSC/ENV registers
* ($D419-$D41C) are never written, so they are not modelled. */
#define VGR3_NREGS_SN76489 11
#define VGR3_NREGS_AY8910 14
#define VGR3_NREGS_NES_APU 0x18
#define VGR3_NREGS_GB_DMG 0x30
#define VGR3_NREGS_POKEY 11
#define VGR3_NREGS_SID 0x19
#ifndef VGR3_MAX_REGS /* a platform build may shrink this to its chip's NREGS */
#define VGR3_MAX_REGS 0x30
#endif
#define VGR3_OP_DICT_END 0x5C /* DICT is 0x00..0x5B */
#define VGR3_OP_EXT 0x5C /* prefix: next byte is a bulk op */
#define VGR3_OP_JUMP 0x5D
#define VGR3_OP_END 0x5E
#define VGR3_OP_CALLL 0x5F
#define VGR3_OP_CALLS 0x60
#define VGR3_OP_CALLM 0x70
#define VGR3_OP_SET 0x80
/* Bulk ops: the byte following VGR3_OP_EXT. */
#define VGR3_EXT_LOAD 0x00 /* write all W window bytes (no wait) */
#define VGR3_DICT_MAX VGR3_OP_DICT_END
#define VGR3_MAX_DEPTH 4
#ifndef VGR3_MAX_CHANS /* likewise: music + sfx channels playing at once */
#define VGR3_MAX_CHANS 16
#endif
#define VGR3_MASK_MAX_W 7 /* widest channel that can use SET masks */
#endif /* VGR3_FORMAT_H */
+397
View File
@@ -0,0 +1,397 @@
/* vgr3_play.c -- see vgr3_play.h. No libc calls. */
#include "vgr3_play.h"
static uint16_t rd16(const uint8_t *p) {
return (uint16_t)(p[0] | (p[1] << 8));
}
static const uint8_t bitTab[8] = {1, 2, 4, 8, 16, 32, 64, 128};
/* Table lookup, not 1 << (r & 7): cc65/sdcc compile a variable shift as a
* loop. A macro so the SET loop pays no call. */
#define PUT(p, r, v) do { \
(p)->regs[r] = (v); \
(p)->dirty[(r) >> 3] |= bitTab[(r) & 7]; \
} while (0)
/* Executes a SET whose opcode is `op` and whose operands start at q.
* Returns the address after the operands. */
static const uint8_t *doSet(Vgr3Ctx *p, Vgr3Chan *c, uint8_t op, const uint8_t *q) {
uint8_t mask = (uint8_t)((op & 0x7F) >> c->k);
uint8_t wait = (uint8_t)(op & c->wmask);
uint8_t r = c->base;
while (mask) {
if (mask & 1) { PUT(p, r, *q); q++; }
mask >>= 1;
r++;
}
if (!wait) wait = *q++;
c->wait = wait;
return q;
}
static void call(Vgr3Chan *c, const uint8_t *target, uint8_t count) {
Vgr3Frame *f = c->sp++;
f->ret = c->pc;
f->left = count;
c->pc = target;
}
/* Runs one item. */
static void step(Vgr3Ctx *p, Vgr3Chan *c) {
const uint8_t *at;
uint8_t op;
Vgr3Frame *f = c->sp; /* one past the top frame; == c->stack when empty */
while (f != c->stack && f[-1].left == 0) c->pc = (--f)->ret;
c->sp = f;
at = c->pc;
op = *c->pc++;
if (f != c->stack) f[-1].left--;
if (op & 0x80) {
c->pc = doSet(p, c, op, c->pc);
} else if (op < VGR3_OP_DICT_END) {
/* the entry is a SET, or a CALLM/CALLL (returns to after this op) */
const uint8_t *q = p->data + rd16(p->dict + op * 2);
if (q[0] & 0x80) doSet(p, c, q[0], q + 1);
else call(c, p->data + rd16(q + 1), (uint8_t)(q[0] == VGR3_OP_CALLL ? q[3] : (q[0] & 15) + 1));
} else if (op >= VGR3_OP_CALLM) {
const uint8_t *t = p->data + rd16(c->pc);
c->pc += 2;
call(c, t, (uint8_t)((op & 15) + 1));
} else if (op >= VGR3_OP_CALLS) {
const uint8_t *t = at - *c->pc++;
call(c, t, (uint8_t)((op & 15) + 1));
} else if (op == VGR3_OP_CALLL) {
const uint8_t *t = p->data + rd16(c->pc);
uint8_t n = c->pc[2];
c->pc += 3;
call(c, t, n);
} else if (op == VGR3_OP_JUMP) {
c->pc = p->data + rd16(c->pc);
} else {
#if VGR3_FEAT_EXT
if (op == VGR3_OP_EXT) {
uint8_t sub = *c->pc++;
if (sub == VGR3_EXT_LOAD) {
uint8_t i;
for (i = 0; i < c->width; i++) {
uint8_t r = (uint8_t)(c->base + i);
PUT(p, r, *c->pc);
c->pc++;
}
return;
}
}
#endif
/* END or unknown: hold */
c->pc = at;
c->wait = 255;
#ifdef VGR3_MIXER
if (!c->ended) c->ended = 1;
#endif
}
}
static void initChan(Vgr3Chan *c, const uint8_t *ct, const uint8_t *data) {
c->base = ct[0];
c->width = ct[1];
c->k = (uint8_t)(c->width <= VGR3_MASK_MAX_W ? 7 - c->width : 7);
c->wmask = (uint8_t)((1 << c->k) - 1);
c->pc = data + rd16(ct + 2);
c->wait = 0;
c->sp = c->stack;
#ifdef VGR3_MIXER
c->ended = 0;
#endif
}
static void clearCtx(Vgr3Ctx *p) {
uint8_t i;
for (i = 0; i < VGR3_MAX_REGS; i++) p->regs[i] = 0;
for (i = 0; i < VGR3_MAX_REGS / 8; i++) p->dirty[i] = 0;
}
static int badFile(const uint8_t *file) {
return file[0] != 'V' || file[1] != 'G' || file[2] != 'R' || file[3] != '3' ||
file[4] != VGR3_VERSION || file[7] > VGR3_MAX_CHANS;
}
int vgr3Init(Vgr3Player *p, const uint8_t *file) {
const uint8_t *ct;
uint8_t i, nd;
if (badFile(file)) return 0;
p->numChans = file[7];
nd = file[8];
ct = file + VGR3_HEADER_SIZE;
p->dict = ct + p->numChans * VGR3_CHAN_SIZE;
p->data = p->dict + nd * 2;
clearCtx((Vgr3Ctx *)p);
for (i = 0; i < p->numChans; i++, ct += VGR3_CHAN_SIZE)
initChan(&p->chans[i], ct, p->data);
return 1;
}
void vgr3Frame(Vgr3Player *p) {
uint8_t i;
Vgr3Chan *c = p->chans; /* walk, don't index: &chans[i] is a multiply on cc65/sdcc */
for (i = p->numChans; i; i--, c++) {
while (c->wait == 0) step((Vgr3Ctx *)p, c);
c->wait--;
}
}
#ifdef VGR3_MIXER
/* ---- mixer ----
* Written for cc65/sdcc: no per-iteration struct-size multiplies (walk
* pointers, keep a direct ctx pointer in each channel), bounded loops
* (chanTop, fadeMask) and a music-only fast path. */
#ifdef VGR3_SINGLE_MIXER
Vgr3Mixer g_mixer;
#define M (&g_mixer)
#else
#define M m
#endif
/* Highest active layer below `above` whose mask covers the byte at
* index i, bit b; VGR3_MAX_LAYERS if none. */
static uint8_t ownerBelow(const Vgr3Mixer *mx, uint8_t above, uint8_t i, uint8_t b) {
uint8_t l = above;
while (l--) {
if ((mx->active & bitTab[l]) && (mx->layer[l].mask[i] & b)) return l;
}
return VGR3_MAX_LAYERS;
}
/* Value layer l presents for register r (shadow, attenuated if fading and r
* is a volume byte; i = r >> 3, bm = 1 << (r & 7)). */
static uint8_t layerVal(const Vgr3Mixer *mx, uint8_t l, const Vgr3Layer *La,
uint8_t r, uint8_t i, uint8_t bm) {
uint8_t v = La->regs[r];
#ifdef VGR3_FADE
if ((mx->fadeMask & bitTab[l]) && (mx->fadeOps->volMask[i] & bm))
v = mx->fadeOps->atten(mx->fadeLevel[l], r, v);
#else
(void)mx; (void)l; (void)i; (void)bm;
#endif
return v;
}
/* Emit the bits d of byte i from layer l. */
static void emit(VGR3_MIXER_PARAM_C uint8_t l, const Vgr3Layer *La, uint8_t i, uint8_t d) {
uint8_t r = (uint8_t)(i << 3), bm = 1;
M->outDirty[i] |= d;
for (; d; d >>= 1, r++, bm <<= 1)
if (d & 1) M->outRegs[r] = layerVal(M, l, La, r, i, bm);
}
static void trimTop(VGR3_MIXER_PARAM) {
while (M->chanTop && !M->chans[M->chanTop - 1].ctx) M->chanTop--;
}
/* `layer` no longer owns register r: re-emit it from the layer below that
* covers it, unless a layer above still owns it. */
static void giveBack(VGR3_MIXER_PARAM_C uint8_t layer, uint8_t r) {
uint8_t i = (uint8_t)(r >> 3), b = bitTab[r & 7], l;
for (l = (uint8_t)(layer + 1); l < VGR3_MAX_LAYERS; l++)
if ((M->active & bitTab[l]) && (M->layer[l].mask[i] & b)) return;
if (M->seMask && (M->seMask[i] & b)) return;
l = ownerBelow(M, layer, i, b);
if (l == VGR3_MAX_LAYERS) return;
M->outRegs[r] = layerVal(M, l, &M->layer[l], r, i, b);
M->outDirty[i] |= b;
}
void vgr3MixerStop(VGR3_MIXER_PARAM_C uint8_t layer) {
Vgr3Layer *La = &M->layer[layer];
Vgr3Chan *c;
uint8_t i, r;
if (!(M->active & bitTab[layer])) return;
for (c = M->chans, i = M->chanTop; i; i--, c++)
if (c->ctx == (Vgr3Ctx *)La) c->ctx = 0;
trimTop(VGR3_MIXER_ARG_ONLY);
M->active &= (uint8_t)~bitTab[layer];
#ifdef VGR3_FADE
M->fadeMask &= (uint8_t)~bitTab[layer];
#endif
for (r = 0; r < VGR3_MAX_REGS; r++)
if (La->mask[r >> 3] & bitTab[r & 7]) giveBack(VGR3_MIXER_ARG layer, r);
for (i = 0; i < VGR3_MAX_REGS / 8; i++) {
La->dirty[i] = 0;
La->mask[i] = 0;
}
}
/* Channel c of an overlay layer parked on END: give its bytes back to the
* layers below now, and stop the layer once all its channels have ended. */
static void endChan(VGR3_MIXER_PARAM_C Vgr3Chan *c) {
Vgr3Layer *La = (Vgr3Layer *)c->ctx;
uint8_t layer = (uint8_t)(La - M->layer), r, i, b;
Vgr3Chan *o;
c->ended = 2;
if (!layer) return; /* nothing below the music */
for (r = c->base; r < c->base + c->width; r++) {
i = (uint8_t)(r >> 3);
b = bitTab[r & 7];
if (!(La->mask[i] & b)) continue;
La->mask[i] &= (uint8_t)~b;
La->dirty[i] &= (uint8_t)~b;
giveBack(VGR3_MIXER_ARG layer, r);
}
for (o = M->chans, i = M->chanTop; i; i--, o++)
if (o->ctx == c->ctx && o->ended != 2) return;
vgr3MixerStop(VGR3_MIXER_ARG layer);
}
int vgr3MixerPlay(VGR3_MIXER_PARAM_C uint8_t layer, const uint8_t *file) {
Vgr3Layer *La;
const uint8_t *ct;
uint8_t i, n, nd, slot;
if (layer >= VGR3_MAX_LAYERS) return 0;
vgr3MixerStop(VGR3_MIXER_ARG layer);
if (badFile(file)) return 0;
La = &M->layer[layer];
n = file[7];
nd = file[8];
/* need n free slots */
for (i = 0, slot = 0; i < VGR3_MAX_CHANS; i++)
if (!M->chans[i].ctx) slot++;
if (slot < n) return 0;
ct = file + VGR3_HEADER_SIZE;
La->dict = ct + n * VGR3_CHAN_SIZE;
La->data = La->dict + nd * 2;
clearCtx((Vgr3Ctx *)La);
for (i = 0; i < VGR3_MAX_REGS / 8; i++) La->mask[i] = 0;
slot = 0;
for (i = 0; i < n; i++, ct += VGR3_CHAN_SIZE) {
Vgr3Chan *c;
while (M->chans[slot].ctx) slot++;
c = &M->chans[slot];
c->ctx = (Vgr3Ctx *)La;
initChan(c, ct, La->data);
slot++;
if (slot > M->chanTop) M->chanTop = slot;
}
M->active |= bitTab[layer];
return 1;
}
int vgr3MixerInit(VGR3_MIXER_PARAM_C const uint8_t *file) {
uint8_t i;
for (i = 0; i < VGR3_MAX_CHANS; i++) M->chans[i].ctx = 0;
for (i = 0; i < VGR3_MAX_REGS; i++) M->outRegs[i] = 0;
for (i = 0; i < VGR3_MAX_REGS / 8; i++) M->outDirty[i] = 0;
M->active = 0;
M->chanTop = 0;
#ifdef VGR3_FADE
M->fadeKick = 0;
M->fadeMask = 0;
M->fadeOps = 0;
for (i = 0; i < VGR3_MAX_LAYERS; i++) M->fadeStep[i] = 0;
#endif
M->seMask = 0;
return vgr3MixerPlay(VGR3_MIXER_ARG 0, file);
}
void vgr3MixerFrame(VGR3_MIXER_PARAM) {
uint8_t i, l, taken, d;
Vgr3Chan *c;
Vgr3Layer *La;
for (c = M->chans, i = M->chanTop; i; i--, c++) {
if (!c->ctx) continue;
while (c->wait == 0) step(c->ctx, c);
c->wait--;
if (c->ended == 1) endChan(VGR3_MIXER_ARG c);
}
#ifdef VGR3_FADE
if (M->fadeMask) {
for (l = 0; l < VGR3_MAX_LAYERS; l++) {
if (!(M->fadeMask & bitTab[l])) continue;
if (--M->fadeLeft[l]) continue;
M->fadeLeft[l] = M->fadeStep[l];
if (++M->fadeLevel[l] == 16) vgr3MixerStop(VGR3_MIXER_ARG l);
else M->fadeKick |= bitTab[l];
}
}
#endif
if (M->active == 1) {
/* music only: nothing can be covered */
La = &M->layer[0];
for (i = 0; i < VGR3_MAX_REGS / 8; i++) {
d = La->dirty[i];
La->mask[i] |= d;
#ifdef VGR3_FADE
if (M->fadeKick & 1) d |= (uint8_t)(M->fadeOps->volMask[i] & La->mask[i]);
#endif
if (!d) continue;
La->dirty[i] = 0;
emit(VGR3_MIXER_ARG 0, La, i, d);
}
} else {
/* A layer's write goes out only if no higher active layer covers the
* byte; writes to bytes an overlay holds are dropped (the lower
* layer's shadow keeps them for release). */
for (i = 0; i < VGR3_MAX_REGS / 8; i++) {
taken = 0;
La = &M->layer[VGR3_MAX_LAYERS];
for (l = VGR3_MAX_LAYERS; l--;) {
La--;
if (!(M->active & bitTab[l])) continue;
d = La->dirty[i];
La->mask[i] |= d; /* a layer owns a byte once it has written it */
#ifdef VGR3_FADE
if (M->fadeKick & bitTab[l]) d |= (uint8_t)(M->fadeOps->volMask[i] & La->mask[i]);
#endif
d &= (uint8_t)~taken;
La->dirty[i] = 0;
taken |= La->mask[i];
if (d) emit(VGR3_MIXER_ARG l, La, i, d);
}
}
}
#ifdef VGR3_FADE
M->fadeKick = 0;
#endif
}
#ifdef VGR3_FADE
void vgr3MixerFade(VGR3_MIXER_PARAM_C uint8_t layer, uint8_t stepLen) {
if (layer >= VGR3_MAX_LAYERS || !stepLen || !M->fadeOps) return;
M->fadeStep[layer] = stepLen;
M->fadeLeft[layer] = stepLen;
M->fadeLevel[layer] = 0;
M->fadeMask |= bitTab[layer];
}
void vgr3MixerFadeCancel(VGR3_MIXER_PARAM_C uint8_t layer) {
if (layer >= VGR3_MAX_LAYERS || !(M->fadeMask & bitTab[layer])) return;
M->fadeMask &= (uint8_t)~bitTab[layer];
M->fadeKick |= bitTab[layer];
}
void vgr3MixerSetFadeOps(VGR3_MIXER_PARAM_C const Vgr3FadeOps *ops) {
M->fadeOps = ops;
}
uint8_t vgr3AttenLow4(uint8_t level, uint8_t reg, uint8_t v) {
uint8_t cap = (uint8_t)(15 - level);
(void)reg;
return (uint8_t)((v & 15) > cap ? (v & 0xF0) | cap : v);
}
uint8_t vgr3AttenSN(uint8_t level, uint8_t reg, uint8_t v) {
(void)reg;
return (uint8_t)((v & 15) < level ? (v & 0xF0) | level : v);
}
uint8_t vgr3AttenGB(uint8_t level, uint8_t reg, uint8_t v) {
uint8_t cap = (uint8_t)(7 - (level >> 1));
uint8_t r = (uint8_t)(v & 7), l = (uint8_t)((v >> 4) & 7);
(void)reg;
if (r > cap) r = cap;
if (l > cap) l = cap;
return (uint8_t)((v & 0x88) | (l << 4) | r);
}
#endif /* VGR3_FADE */
#endif /* VGR3_MIXER */
+168
View File
@@ -0,0 +1,168 @@
/* vgr3_play.h -- VGR3 decoder (see vgr3_format.h).
*
* Call vgr3Frame() once per tick. It updates regs[] and sets a bit in
* dirty[] for every byte written this frame; platform glue then writes
* the dirty bytes to the hardware in whatever order the chip wants
* (ascending works for NES/GB; SN76489 needs latch/data pairs) and
* clears dirty[].
*
* RAM: VGR3_MAX_REGS + VGR3_MAX_REGS/8 bytes, plus per channel
* 9 + 3*VGR3_MAX_DEPTH bytes on a target with 2-byte pointers.
*/
#ifndef VGR3_PLAY_H
#define VGR3_PLAY_H
#include <stdint.h>
#include "vgr3_format.h"
typedef struct {
const uint8_t *ret;
uint8_t left; /* items still to run at this level */
} Vgr3Frame;
/* What step() needs from whatever it decodes into: the song's blob and
* dict, and the shadow register file. Vgr3Player and Vgr3Layer both start
* with these fields, so step() takes a pointer to either. */
#define VGR3_CTX_FIELDS \
const uint8_t *data; /* data blob */ \
const uint8_t *dict; /* dict offset table */ \
uint8_t regs[VGR3_MAX_REGS]; \
uint8_t dirty[VGR3_MAX_REGS / 8];
typedef struct { VGR3_CTX_FIELDS } Vgr3Ctx;
typedef struct {
const uint8_t *pc;
uint8_t base;
uint8_t width;
uint8_t k; /* wait bits in a SET opcode */
uint8_t wait; /* frames until the next item */
uint8_t wmask; /* (1 << k) - 1: the wait bits of a SET opcode */
Vgr3Frame *sp; /* one past the top stack frame */
#ifdef VGR3_MIXER
Vgr3Ctx *ctx; /* owning layer's context; NULL if the slot is unused */
uint8_t ended; /* 0 running, 1 just parked on END, 2 handled */
#endif
Vgr3Frame stack[VGR3_MAX_DEPTH];
} Vgr3Chan;
typedef struct {
VGR3_CTX_FIELDS
uint8_t numChans;
Vgr3Chan chans[VGR3_MAX_CHANS];
} Vgr3Player;
/* file must stay valid while playing (it is read in place). Returns 0
* if it isn't a VGR3 file this decoder can play. */
int vgr3Init(Vgr3Player *p, const uint8_t *file);
void vgr3Frame(Vgr3Player *p);
#if defined(VGR3_FADE) && !defined(VGR3_MIXER)
#error VGR3_FADE needs VGR3_MIXER
#endif
#ifdef VGR3_MIXER
/* ---- Optional mixer: several songs at once (music + SFX overlays) ----
*
* Define VGR3_MIXER to build it; without it none of this exists and the
* decoder is unchanged apart from one tag byte per channel being absent.
* See notes/sfxfade.md. Layer 0 is the music, higher layers are overlays
* and win the bytes their channels cover (`mask`). All layers' channels
* run every frame (the music never pauses); only the merge decides which
* layer's write reaches outRegs/outDirty. Platform glue flushes those
* instead of a player's regs/dirty, then clears outDirty.
*
* Size the arrays per platform by defining these before including this
* header. */
#ifndef VGR3_MAX_LAYERS
#define VGR3_MAX_LAYERS 3 /* 0 = music, 1..N-1 = sfx */
#endif
#ifdef VGR3_FADE
/* Per-chip fade hooks, supplied by the platform glue: which register
* bytes are volumes (VGR3_MAX_REGS/8 bytes) and how to attenuate one by
* `level` (0..15, 0 = no change, 15 = silent). See vgr3AttenLow4 etc. */
typedef struct {
const uint8_t *volMask;
uint8_t (*atten)(uint8_t level, uint8_t reg, uint8_t v);
} Vgr3FadeOps;
#endif
typedef struct {
VGR3_CTX_FIELDS
uint8_t mask[VGR3_MAX_REGS / 8]; /* bytes this layer's channels may write */
} Vgr3Layer;
typedef struct {
Vgr3Layer layer[VGR3_MAX_LAYERS];
Vgr3Chan chans[VGR3_MAX_CHANS]; /* shared pool, each tagged with its layer */
uint8_t active; /* bitmask of live layers */
uint8_t chanTop; /* 1 + highest used slot in chans[] */
uint8_t outRegs[VGR3_MAX_REGS]; /* merged output */
uint8_t outDirty[VGR3_MAX_REGS / 8];
/* Optional: bytes NOT to re-emit on release. NULL (the default) restores
* every held byte, including note triggers such as NES $4003, so a voice
* comes back audibly even if the music only rewrites the period low byte
* for a long stretch; the cost is one retrigger of the held note. */
const uint8_t *seMask;
#ifdef VGR3_FADE
const Vgr3FadeOps *fadeOps; /* set by the platform glue */
uint8_t fadeStep[VGR3_MAX_LAYERS]; /* frames per level step; 0 = not fading */
uint8_t fadeLeft[VGR3_MAX_LAYERS]; /* frames to the next step */
uint8_t fadeLevel[VGR3_MAX_LAYERS]; /* 0..15 attenuation; 16 stops the layer */
uint8_t fadeMask; /* layers currently fading */
uint8_t fadeKick; /* layers whose volume bytes must be re-emitted */
#endif
} Vgr3Mixer;
/* On cc65/sdcc a pointer parameter costs a lot; with VGR3_SINGLE_MIXER the
* API drops it and works on one static g_mixer. Host tests and multi-chip
* boards build without it and pass a pointer. */
#ifdef VGR3_SINGLE_MIXER
extern Vgr3Mixer g_mixer;
#define VGR3_MIXER_PARAM void
#define VGR3_MIXER_PARAM_C
#define VGR3_MIXER_ARG
#define VGR3_MIXER_ARG_ONLY
#else
#define VGR3_MIXER_PARAM Vgr3Mixer *m
#define VGR3_MIXER_PARAM_C VGR3_MIXER_PARAM,
#define VGR3_MIXER_ARG m,
#define VGR3_MIXER_ARG_ONLY m
#endif
/* Clears everything and starts `file` as layer 0. */
int vgr3MixerInit(VGR3_MIXER_PARAM_C const uint8_t *file);
/* Starts `file` on `layer` (1..VGR3_MAX_LAYERS-1), replacing whatever the
* layer was playing. Returns 0 on a bad file or if the channel pool is
* full (the layer is left stopped). */
int vgr3MixerPlay(VGR3_MIXER_PARAM_C uint8_t layer, const uint8_t *file);
/* Stops a layer and hands its bytes back to the layers below: every held
* byte is re-emitted from the lower layer's shadow (except seMask bytes). */
void vgr3MixerStop(VGR3_MIXER_PARAM_C uint8_t layer);
/* Steps every live channel and merges into outRegs/outDirty. */
void vgr3MixerFrame(VGR3_MIXER_PARAM);
#ifdef VGR3_FADE
/* Fades `layer` out: attenuation rises one level every `stepLen` frames
* (1..255), so the fade lasts 16 * stepLen frames, then the layer is
* stopped. Only volume bytes it owns are affected (an overlay's are not).
* Needs fadeOps set (vgr3MixerSetFadeOps). */
void vgr3MixerFade(VGR3_MIXER_PARAM_C uint8_t layer, uint8_t stepLen);
/* Abandons a fade in progress and restores the layer's volumes. */
void vgr3MixerFadeCancel(VGR3_MIXER_PARAM_C uint8_t layer);
void vgr3MixerSetFadeOps(VGR3_MIXER_PARAM_C const Vgr3FadeOps *ops);
/* Ready-made atten functions. Low4: 4-bit volume in the low nibble,
* 15 = loudest, clamped to 15 - level (NES $4000/$4004/$400C, AY R8-R10,
* POKEY AUDCn, SID R24). SN: 4-bit attenuation, 0 = loudest, raised to at
* least `level` (SN76489 volume regs). GB: NR50 master volume, two 3-bit
* fields (NRx2 volume only applies on trigger, so fade NR50 instead). */
uint8_t vgr3AttenLow4(uint8_t level, uint8_t reg, uint8_t v);
uint8_t vgr3AttenSN(uint8_t level, uint8_t reg, uint8_t v);
uint8_t vgr3AttenGB(uint8_t level, uint8_t reg, uint8_t v);
#endif /* VGR3_FADE */
#endif /* VGR3_MIXER */
#endif /* VGR3_PLAY_H */
+1 -1
View File
@@ -328,7 +328,7 @@ export function removeBOM(s:string) {
}
// File extensions that are always treated as binary, regardless of content.
const BINARY_EXTS = ['.CHR', '.BIN', '.DAT', '.PAL', '.NAM', '.RLE', '.LZ4', '.LZH', '.LZSA', '.NSF'];
const BINARY_EXTS = ['.CHR', '.BIN', '.DAT', '.PAL', '.NAM', '.RLE', '.LZ4', '.LZH', '.LZSA', '.NSF', '.VGR3'];
/**
* Strict RFC 3629 UTF-8 validation. Unlike decoding with replacement, this
+1
View File
@@ -23,6 +23,7 @@ const GB_PRESETS: Preset[] = [
{ id: 'gbdecompress.c', name: 'Decompression' },
{ id: 'music.c', name: 'Music Player' },
{ id: 'aputest.c', name: 'Sound Tester' },
{ id: 'demo_vgr3.c', name: 'Compressed VGM Music' },
{ id: 'clock.c', name: 'MBC3 Real-Time Clock' },
{ id: 'savegame.c', name: 'Battery Save RAM' },
{ id: 'banking.c', name: 'ROM Bank Switching' },