diff --git a/presets/apple2/mockingboard.c b/presets/apple2/mockingboard.c index 92d9b56c..aae99a00 100644 --- a/presets/apple2/mockingboard.c +++ b/presets/apple2/mockingboard.c @@ -184,8 +184,6 @@ void start_music(const byte* music) { void main(void) { - unsigned char s; - clrscr(); gotoxy(8, 4); cputs("MOCKINGBOARD DEMO"); diff --git a/presets/gb/demo_vgr3.c b/presets/gb/demo_vgr3.c new file mode 100644 index 00000000..bec81e3d --- /dev/null +++ b/presets/gb/demo_vgr3.c @@ -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 +#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); + } +} diff --git a/presets/gb/tune.vgr3 b/presets/gb/tune.vgr3 new file mode 100644 index 00000000..9f51e9a6 Binary files /dev/null and b/presets/gb/tune.vgr3 differ diff --git a/presets/gb/vgr3_format.h b/presets/gb/vgr3_format.h new file mode 100644 index 00000000..4850202a --- /dev/null +++ b/presets/gb/vgr3_format.h @@ -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<= 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 */ diff --git a/presets/gb/vgr3_play.c b/presets/gb/vgr3_play.c new file mode 100644 index 00000000..575a9b14 --- /dev/null +++ b/presets/gb/vgr3_play.c @@ -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 */ diff --git a/presets/gb/vgr3_play.h b/presets/gb/vgr3_play.h new file mode 100644 index 00000000..98b709c3 --- /dev/null +++ b/presets/gb/vgr3_play.h @@ -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 +#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 */ diff --git a/src/common/util.ts b/src/common/util.ts index 51721c1e..79516cef 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -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 diff --git a/src/platform/gb.ts b/src/platform/gb.ts index 379dfeed..ac62c6b4 100644 --- a/src/platform/gb.ts +++ b/src/platform/gb.ts @@ -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' },