Files

211 lines
5.8 KiB
C++

/*
* pokeymusic.cpp - POKEY music player for the Atari 8-bit.
*
* oscar64 C++ port of pokeymusic.ca65. It exports the same entry points
* that common.h declares, so an oscar64 program can link this file instead
* of the ca65 original:
*
* //#link "pokeymusic.cpp"
* music_start(song);
* // call music_tick() and music_duty() from an interrupt
*
* The ca65 version parks its song pointer in zero page ($fe) because cc65's
* set_irq() saves and restores the whole zero page. Here it is an ordinary
* static pointer, so it survives an oscar64 __interrupt routine untouched.
*/
typedef unsigned char byte;
/* POKEY registers: the 800 decodes POKEY at $d2xx, the 5200 at $e8xx. */
#if defined(__ATARI5200__)
#define A8_AUDF ((volatile byte*)0xe800)
#define A8_AUDC ((volatile byte*)0xe801)
#define A8_AUDCTL (*(volatile byte*)0xe808)
#else
#define A8_AUDF ((volatile byte*)0xd200)
#define A8_AUDC ((volatile byte*)0xd201)
#define A8_AUDCTL (*(volatile byte*)0xd208)
#endif
/* Player state. volatile because the music interrupt writes it while the
main program reads it (e.g. through music_get_ptr / music_is_done). */
static volatile byte chan_dur[4]; /* current note duration per channel */
static volatile byte chan_note[4]; /* current note pitch per channel */
static volatile byte chan_duty[4]; /* current duty-cycle bits per channel */
static volatile byte duration_timer; /* ticks until the next command */
static volatile byte cur_channel; /* next channel to add a note */
static volatile byte volume; /* volume of a new note */
static const byte* volatile song_ptr; /* next byte in the song data */
static const byte* volatile song_rts; /* return address after a back-reference */
static volatile byte song_runlen; /* bytes left in a back-reference */
/* Decode the next song byte, expanding 0xfe back-references.
0xfe <offset> <length>: repeat <length> bytes starting
<offset> bytes before the 0xfe marker. */
static byte next_music_byte(void)
{
byte ch = *song_ptr++;
if (song_runlen) {
/* we are in a back-reference, count down */
if (--song_runlen == 0) {
song_ptr = song_rts;
song_rts = nullptr;
}
} else if (ch == 0xfe) {
byte offset = *song_ptr++;
song_runlen = *song_ptr++;
song_rts = song_ptr;
song_ptr -= offset + 3;
return next_music_byte();
}
return ch;
}
/* AUDF base value for each note (0-63). */
static const byte freqz[64] = {
254, 254, 254, 255, 240, 227, 214, 202,
190, 180, 169, 160, 151, 142, 134, 127,
119, 113, 106, 100, 94, 89, 84, 79,
75, 70, 66, 63, 59, 56, 52, 49,
47, 44, 41, 39, 37, 34, 32, 31,
29, 27, 25, 24, 23, 21, 20, 19,
18, 17, 16, 15, 14, 13, 12, 11,
11, 10, 9, 9, 8, 8, 7, 7
};
/* Duty-cycle rotation bits for each note. */
static const byte dutyz[64] = {
0, 0, 0, 0, 181, 1, 17, 1,
219, 0, 239, 17, 17, 181, 181, 0,
239, 0, 181, 181, 239, 85, 73, 181,
1, 239, 219, 0, 73, 0, 239, 219,
0, 17, 219, 73, 0, 239, 239, 0,
17, 85, 239, 73, 0, 181, 73, 1,
0, 0, 0, 0, 1, 17, 85, 219,
0, 73, 181, 1, 85, 0, 85, 0
};
/* Load a new note onto a channel. */
static void music_do_note(byte chan, byte note)
{
chan_note[chan] = freqz[note];
chan_duty[chan] = dutyz[note];
chan_dur[chan] = volume;
}
/* Hook called at end of song; present in the ca65 version as a stub. */
static void music_done(void)
{
}
/* Rotate each channel's duty bits and fold the bit that falls off the top
into AUDF, which gives the notes their pulse-width flavour. */
/*
void music_duty(void)
{
byte y = 0;
for (byte x = 4; x-- != 0; y += 2) {
if (chan_dur[x] != 0) {
byte d = chan_duty[x];
byte c = (byte)(d >> 7);
chan_duty[x] = (byte)((d << 1) | c);
if (chan_note[x] != 0)
A8_AUDF[y] = (byte)(chan_note[x] + c);
}
}
}
*/
void music_duty(void) {
__asm {
ldx #3
ldy #0
loop:
lda chan_dur,x
beq notplaying
lda chan_duty,x
asl
bcc nobit
ora #1
nobit:
sta chan_duty,x
lda chan_note,x
beq notplaying
// If next bit is set, add 1 to AUDF0
adc #0
#if defined(__ATARI5200__)
sta $e800,y
#else
sta $d200,y
#endif
notplaying:
iny
iny
dex
bpl loop
rts
}
}
/* Decrement the per-channel volumes and the next-note timer, and pull the
next command out of the song when the timer runs out. */
void music_tick(void)
{
byte y = 0;
for (byte x = 4; x-- != 0; y += 2) {
if (chan_dur[x] != 0) {
A8_AUDC[y] = (byte)((chan_dur[x] >> 1) | 0xa0);
chan_dur[x]--;
}
}
if (duration_timer & 0x80)
return; /* end of song, stop fetching */
if (duration_timer != 0) {
duration_timer--;
return;
}
/* Timer expired: consume note bytes until a duration byte shows up. */
for (;;) {
byte b = next_music_byte();
if (b & 0x80) {
if (b == 0xff) {
duration_timer = b; /* stays $ff so music_is_done() is true */
music_done();
return;
}
duration_timer = (byte)(b & 0x7f);
return;
}
music_do_note(cur_channel, b);
cur_channel = (byte)((cur_channel + 1) & 3);
}
}
/* Start playing the song at song. */
void music_start(const char* song)
{
song_ptr = (const byte*)song;
song_rts = nullptr;
song_runlen = 0;
volume = 24;
duration_timer = 0;
cur_channel = 0;
for (byte x = 4; x-- != 0; )
chan_dur[x] = 0;
A8_AUDCTL = 0x01;
}
/* Current position in the song data. */
char* music_get_ptr(void)
{
return (char*)song_ptr;
}
/* Nonzero once the song reaches its $ff terminator. */
char music_is_done(void)
{
return (duration_timer & 0x80) ? 1 : 0;
}