fruitjam-doom/opl/opl_pico.c
2022-03-07 12:44:30 -06:00

605 lines
16 KiB
C

//
// Copyright(C) 2005-2014 Simon Howard
// Copyright(C) 2021-2022 Graham Sanderson
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// DESCRIPTION:
// OPL SDL interface.
//
// todo replace opl_queue with pheap
#include "config.h"
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include "pico/mutex.h"
#include "pico/audio_i2s.h"
#include "pico/util/pheap.h"
#include "hardware/gpio.h"
#include "i_picosound.h"
#if USE_WOODY_OPL
#include "woody_opl.h"
#elif USE_EMU8950_OPL
#include "emu8950.h"
#else
#include "opl3.h"
#endif
#include "opl.h"
#include "opl_internal.h"
#include "opl_queue.h"
#define MAX_SOUND_SLICE_TIME 100 /* ms */
typedef struct
{
unsigned int rate; // Number of times the timer is advanced per sec.
unsigned int enabled; // Non-zero if timer is enabled.
unsigned int value; // Last value that was set.
uint64_t expire_time; // Calculated time that timer will expire.
} opl_timer_t;
// When the callback mutex is locked using OPL_Lock, callback functions
// are not invoked.
static mutex_t callback_mutex;
// Queue of callbacks waiting to be invoked.
static opl_callback_queue_t *callback_queue;
// Mutex used to control access to the callback queue.
static mutex_t callback_queue_mutex;
// Current time, in us since startup:
static uint64_t current_time;
// If non-zero, playback is currently paused.
static int opl_pico_paused;
// Time offset (in us) due to the fact that callbacks
// were previously paused.
static uint64_t pause_offset;
// OPL software emulator structure.
#if USE_WOODY_OPL
// todo configure this based on woody build flag
#define opl_op3mode 0
#elif USE_EMU8950_OPL
#define opl_op3mode 0
static OPL *emu8950_opl;
#else
static opl3_chip opl_chip;
static int opl_opl3mode;
#endif
// Register number that was written.
static int register_num = 0;
#if !EMU8950_NO_TIMER
// Timers; DBOPL does not do timer stuff itself.
static opl_timer_t timer1 = { 12500, 0, 0, 0 };
static opl_timer_t timer2 = { 3125, 0, 0, 0 };
#endif
// SDL parameters.
static bool audio_was_initialized = 0;
static inline void Pico_LockMutex(mutex_t *mutex) {
}
static inline void Pico_UnlockMutex(mutex_t *mutex) {
}
// Advance time by the specified number of samples, invoking any
// callback functions as appropriate.
static void AdvanceTime(unsigned int nsamples)
{
opl_callback_t callback;
void *callback_data;
uint64_t us;
Pico_LockMutex(&callback_queue_mutex);
// Advance time.
us = ((uint64_t) nsamples * OPL_SECOND) / PICO_SOUND_SAMPLE_FREQ;
current_time += us;
if (opl_pico_paused)
{
pause_offset += us;
}
// Are there callbacks to invoke now? Keep invoking them
// until there are no more left.
while (!OPL_Queue_IsEmpty(callback_queue)
&& current_time >= OPL_Queue_Peek(callback_queue) + pause_offset)
{
// Pop the callback from the queue to invoke it.
if (!OPL_Queue_Pop(callback_queue, &callback, &callback_data))
{
break;
}
// The mutex stuff here is a bit complicated. We must
// hold callback_mutex when we invoke the callback (so that
// the control thread can use OPL_Lock() to prevent callbacks
// from being invoked), but we must not be holding
// callback_queue_mutex, as the callback must be able to
// call OPL_SetCallback to schedule new callbacks.
Pico_UnlockMutex(&callback_queue_mutex);
Pico_LockMutex(&callback_mutex);
callback(callback_data);
Pico_UnlockMutex(&callback_mutex);
Pico_LockMutex(&callback_queue_mutex);
}
Pico_UnlockMutex(&callback_queue_mutex);
}
// Call the OPL emulator code to fill the specified buffer.
// Callback function to fill a new sound buffer:
#define LIMITED_CALLBACK_TYPES 1
#if LIMITED_CALLBACK_TYPES
extern void RestartSong(void *unused);
extern void TrackTimerCallback(void *track);
#endif
#if DOOM_TINY
extern uint8_t restart_song_state;
#endif
void OPL_Pico_Mix_callback(audio_buffer_t *audio_buffer)
{
unsigned int filled, buffer_samples;
#if DOOM_TINY
if (restart_song_state == 2) {
RestartSong(0);
}
#endif
// Repeatedly call the OPL emulator update function until the buffer is
// full.
filled = 0;
buffer_samples = audio_buffer->max_sample_count;
//#if PICO_ON_DEVICE
// absolute_time_t t0 = get_absolute_time();
// gpio_set_mask(1);
//#endif
while (filled < buffer_samples) {
//#if PICO_ON_DEVICE
// gpio_set_mask(32);
//#endif
uint64_t next_callback_time;
uint64_t nsamples;
Pico_LockMutex(&callback_queue_mutex);
// Work out the time until the next callback waiting in
// the callback queue must be invoked. We can then fill the
// buffer with this many samples.
if (opl_pico_paused || OPL_Queue_IsEmpty(callback_queue)) {
nsamples = buffer_samples - filled;
} else {
next_callback_time = OPL_Queue_Peek(callback_queue) + pause_offset;
nsamples = (next_callback_time - current_time) * PICO_SOUND_SAMPLE_FREQ;
nsamples = (nsamples + OPL_SECOND - 1) / OPL_SECOND;
if (nsamples > buffer_samples - filled) {
nsamples = buffer_samples - filled;
}
}
Pico_UnlockMutex(&callback_queue_mutex);
// Add emulator output to buffer.
//OPL3_GenerateStream(&opl_chip, (Bit16s *) (audio_buffer->buffer->bytes + filled * 4), nsamples);
#if USE_WOODY_OPL
int16_t *sndptr = (int16_t *) (audio_buffer->buffer->bytes + filled * 4);
// todo store in stereo?
adlib_getsample(sndptr, nsamples);
for(int i=nsamples-1; i>=0; i--) {
sndptr[i*2] = sndptr[i*2 + 1] = sndptr[i];
}
#elif USE_EMU8950_OPL
if (nsamples) {
int32_t *sndptr32 = (int32_t *) (audio_buffer->buffer->bytes + filled * 4);
OPL_calc_buffer_stereo(emu8950_opl, sndptr32, nsamples);
}
#else
int16_t *sndptr = (int16_t *) (audio_buffer->buffer->bytes + filled * 4);
for(int i = 0; i < nsamples; i++)
{
OPL3_GenerateResampled(&opl_chip, sndptr);
sndptr += 2;
}
#endif
filled += nsamples;
// Invoke callbacks for this point in time.
//#if PICO_ON_DEVICE
// gpio_clr_mask(32);
//#endif
AdvanceTime(nsamples);
}
audio_buffer->sample_count = audio_buffer->max_sample_count;
#if !USE_WOODY_OPL
int16_t *samples = (int16_t *)audio_buffer->buffer->bytes;
for(uint i=0;i<audio_buffer->sample_count * 2; i++) {
samples[i] <<= 3;
}
#endif
//#if PICO_ON_DEVICE
// gpio_clr_mask(1);
// int32_t t = (int32_t)absolute_time_diff_us(t0, get_absolute_time());
// static int max_t;
// static int ii;
// static int total;
// total += t;
// if (t > max_t) {
// max_t = t;
// }
// ii++;
// if (!(ii &127)) {
// printf("AVG %d MAX %d\n", total / 128, max_t);
// max_t = 0;
// total = 0;
// }
//#endif
}
static void OPL_Pico_Shutdown(void)
{
if (audio_was_initialized)
{
I_PicoSoundSetMusicGenerator(NULL);
OPL_Queue_Destroy(callback_queue);
audio_was_initialized = 0;
}
}
static int OPL_Pico_Init(unsigned int port_base)
{
if (I_PicoSoundIsInitialized()) {
opl_pico_paused = 0;
pause_offset = 0;
// Queue structure of callbacks to invoke.
callback_queue = OPL_Queue_Create();
current_time = 0;
#if USE_WOODY_OPL
adlib_init(mixing_freq);
#elif USE_EMU8950_OPL
emu8950_opl = OPL_new(3579552, PICO_SOUND_SAMPLE_FREQ); // todo check rate
#else
OPL3_Reset(&opl_chip, PICO_SOUND_SAMPLE_FREQ);
opl_opl3mode = 0;
#endif
// // Set postmix that adds the OPL music. This is deliberately done
// // as a postmix and not using Mix_HookMusic() as the latter disables
// // normal Pico_mixer music mixing.
// Mix_SetPostMix(OPL_Mix_Callback, NULL);
I_PicoSoundSetMusicGenerator(OPL_Pico_Mix_callback);
audio_was_initialized = 1;
} else {
audio_was_initialized = 0;
}
return 1;
}
static unsigned int OPL_Pico_PortRead(opl_port_t port)
{
unsigned int result = 0;
if (port == OPL_REGISTER_PORT_OPL3)
{
return 0xff;
}
#if !EMU8950_NO_TIMER
if (timer1.enabled && current_time > timer1.expire_time)
{
result |= 0x80; // Either have expired
result |= 0x40; // Timer 1 has expired
}
if (timer2.enabled && current_time > timer2.expire_time)
{
result |= 0x80; // Either have expired
result |= 0x20; // Timer 2 has expired
}
#endif
return result;
}
static void OPLTimer_CalculateEndTime(opl_timer_t *timer)
{
int tics;
// If the timer is enabled, calculate the time when the timer
// will expire.
if (timer->enabled)
{
tics = 0x100 - timer->value;
timer->expire_time = current_time
+ ((uint64_t) tics * OPL_SECOND) / timer->rate;
}
}
static void WriteRegister(unsigned int reg_num, unsigned int value)
{
switch (reg_num)
{
#if !EMU8950_NO_TIMER
case OPL_REG_TIMER1:
timer1.value = value;
OPLTimer_CalculateEndTime(&timer1);
break;
case OPL_REG_TIMER2:
timer2.value = value;
OPLTimer_CalculateEndTime(&timer2);
break;
case OPL_REG_TIMER_CTRL:
if (value & 0x80)
{
timer1.enabled = 0;
timer2.enabled = 0;
}
else
{
if ((value & 0x40) == 0)
{
timer1.enabled = (value & 0x01) != 0;
OPLTimer_CalculateEndTime(&timer1);
}
if ((value & 0x20) == 0)
{
timer1.enabled = (value & 0x02) != 0;
OPLTimer_CalculateEndTime(&timer2);
}
}
break;
#endif
case OPL_REG_NEW:
#if !USE_WOODY_OPL && !USE_EMU8950_OPL
opl_opl3mode = value & 0x01;
#endif
default:
#if USE_WOODY_OPL
adlib_write(reg_num, value);
#elif USE_EMU8950_OPL
OPL_writeReg(emu8950_opl, reg_num, value);
#else
OPL3_WriteRegBuffered(&opl_chip, reg_num, value);
#endif
break;
}
}
static void OPL_Pico_PortWrite(opl_port_t port, unsigned int value)
{
if (port == OPL_REGISTER_PORT)
{
register_num = value;
}
else if (port == OPL_REGISTER_PORT_OPL3)
{
register_num = value | 0x100;
}
else if (port == OPL_DATA_PORT)
{
WriteRegister(register_num, value);
}
}
static void OPL_Pico_SetCallback(uint64_t us, opl_callback_t callback,
void *data)
{
Pico_LockMutex(&callback_queue_mutex);
OPL_Queue_Push(callback_queue, callback, data,
current_time - pause_offset + us);
Pico_UnlockMutex(&callback_queue_mutex);
}
static void OPL_Pico_ClearCallbacks(void)
{
Pico_LockMutex(&callback_queue_mutex);
OPL_Queue_Clear(callback_queue);
Pico_UnlockMutex(&callback_queue_mutex);
}
static void OPL_Pico_Lock(void)
{
Pico_LockMutex(&callback_mutex);
}
static void OPL_Pico_Unlock(void)
{
Pico_UnlockMutex(&callback_mutex);
}
static void OPL_Pico_SetPaused(int paused)
{
opl_pico_paused = paused;
}
static void OPL_Pico_AdjustCallbacks(unsigned int old_tempo, unsigned int new_tempo)
{
Pico_LockMutex(&callback_queue_mutex);
OPL_Queue_AdjustCallbacks(callback_queue, current_time, old_tempo, new_tempo);
Pico_UnlockMutex(&callback_queue_mutex);
}
const opl_driver_t opl_pico_driver =
{
"Pico",
OPL_Pico_Init,
OPL_Pico_Shutdown,
OPL_Pico_PortRead,
OPL_Pico_PortWrite,
OPL_Pico_SetCallback,
OPL_Pico_ClearCallbacks,
OPL_Pico_Lock,
OPL_Pico_Unlock,
OPL_Pico_SetPaused,
OPL_Pico_AdjustCallbacks,
};
void OPL_Delay(uint64_t us) {
sleep_us(us); // todo not sure we want to block
}
// todo really limited to 1 event per track i think, so could go smaller
#define MAX_OPL_QUEUE 10
PHEAP_DEFINE_STATIC(opl_heap, MAX_OPL_QUEUE + 1);
// todo note also the callback is likely only one of a couple of functions
typedef struct queue_entry {
uint64_t time; // todo graham can likely be 32 bit, too much work atm
#if !LIMITED_CALLBACK_TYPES
opl_callback_t callback;
#endif
void *data;
} queue_entry_t;
struct opl_callback_queue_s {
queue_entry_t entries[MAX_OPL_QUEUE];
};
static struct opl_callback_queue_s queue;
static inline queue_entry_t *get_entry(opl_callback_queue_t *queue, pheap_node_id_t id) {
assert(id && id <= opl_heap.max_nodes);
return queue->entries + id - 1;
}
bool opl_queue_comparator(void *user_data, pheap_node_id_t a, pheap_node_id_t b) {
opl_callback_queue_t *q = (opl_callback_queue_t *)user_data;
return get_entry(q, a)->time < get_entry(q, b)->time;
}
opl_callback_queue_t *OPL_Queue_Create(void) {
ph_post_alloc_init(&opl_heap, MAX_OPL_QUEUE, opl_queue_comparator, &queue);
return &queue;
}
int OPL_Queue_IsEmpty(opl_callback_queue_t *queue) {
return ph_peek_head(&opl_heap) == 0;
}
void OPL_Queue_Clear(opl_callback_queue_t *queue) {
ph_clear(&opl_heap);
}
void OPL_Queue_Destroy(opl_callback_queue_t *queue) {
}
void OPL_Queue_Push(opl_callback_queue_t *queue,
opl_callback_t callback, void *data,
uint64_t time) {
pheap_node_id_t id = ph_new_node(&opl_heap);
assert(id); // check not full
queue_entry_t *qe = get_entry(queue, id);
qe->time = time;
#if !LIMITED_CALLBACK_TYPES
qe->callback = callback
#else
assert(data || callback == RestartSong);
assert(!data || callback == TrackTimerCallback);
#endif
qe->data = data;
ph_insert_node(&opl_heap, id);
}
int OPL_Queue_Pop(opl_callback_queue_t *queue,
opl_callback_t *callback, void **data) {
if (!ph_peek_head(&opl_heap)) return 0;
pheap_node_id_t id = ph_remove_head(&opl_heap, true);
queue_entry_t *qe = get_entry(queue, id);
#if !LIMITED_CALLBACK_TYPES
*callback = qe->callback;
#else
*callback = qe->data ? TrackTimerCallback : RestartSong;
#endif
*data = qe->data;
return 1;
}
uint64_t OPL_Queue_Peek(opl_callback_queue_t *queue) {
pheap_node_id_t head = ph_peek_head(&opl_heap);
if (head) {
return get_entry(queue, head)->time;
} else {
return 0;
}
}
static uint AdjustCallbacks(pheap_node_id_t id, uint64_t time, unsigned int old_tempo, unsigned int new_tempo) {
uint count = 0;
if (id) {
pheap_node_t *node = ph_get_node(&opl_heap, id);
queue_entry_t *entry = get_entry(&queue, id);
uint64_t offset = entry->time - time;
entry->time = time + (offset * new_tempo) / old_tempo;
AdjustCallbacks(node->child, time, old_tempo, new_tempo);
AdjustCallbacks(node->sibling, time, old_tempo, new_tempo);
}
return count;
}
void OPL_Queue_AdjustCallbacks(opl_callback_queue_t *_queue,
uint64_t time, unsigned int old_tempo, unsigned int new_tempo)
{
assert(_queue == &queue);
AdjustCallbacks(ph_peek_head(&opl_heap), time, old_tempo, new_tempo);
}