mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
gb: vgm3 player preset
This commit is contained in:
1 parent
6f805e3c7e
commit
b3cf4e159d
8 files changed
+795
-3
No files matched your search
@@ -184,8 +184,6 @@ void start_music(const byte* music) {
|
||||
|
||||
void main(void)
|
||||
{
|
||||
unsigned char s;
|
||||
|
||||
clrscr();
|
||||
gotoxy(8, 4);
|
||||
cputs("MOCKINGBOARD DEMO");
|
||||
|
||||
@@ -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.
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in new issue
Block a user