ref: 0fff1596b491233d435379a9d50e74afdc6d2dfd
dir: /opl_sdl.c/
//
// Copyright(C) 2005-2014 Simon Howard
//
// 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.
//
#include "config.h"
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include "SDL.h"
#include "SDL_mixer.h"
#include "opl3.h"
#include "opl_sdl.h"
#include "opl_queue.h"
unsigned int opl_sample_rate = 22050;
#define MAX_SOUND_SLICE_TIME 100 /* ms */
// When the callback mutex is locked using OPL_Lock, callback functions
// are not invoked.
static SDL_mutex *callback_mutex = NULL;
// Queue of callbacks waiting to be invoked.
static opl_callback_queue_t *callback_queue;
// Mutex used to control access to the callback queue.
static SDL_mutex *callback_queue_mutex = NULL;
// Current time, in us since startup:
static uint64_t current_time;
// If non-zero, playback is currently paused.
static int opl_sdl_paused;
// Time offset (in us) due to the fact that callbacks
// were previously paused.
static uint64_t pause_offset;
// OPL software emulator structure.
static opl3_chip opl_chip;
static int opl_opl3mode;
// Temporary mixing buffer used by the mixing callback.
static int32_t *mix_buffer = NULL;
// Register number that was written.
static int register_num = 0;
// SDL parameters.
static int sdl_was_initialized = 0;
static int mixing_freq, mixing_channels;
static Uint16 mixing_format;
static int SDLIsInitialized(void)
{
int freq, channels;
Uint16 format;
return Mix_QuerySpec(&freq, &format, &channels);
}
// 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;
SDL_LockMutex(callback_queue_mutex);
// Advance time.
us = ((uint64_t) nsamples * OPL_SECOND) / mixing_freq;
current_time += us;
if (opl_sdl_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.
SDL_UnlockMutex(callback_queue_mutex);
SDL_LockMutex(callback_mutex);
callback(callback_data);
SDL_UnlockMutex(callback_mutex);
SDL_LockMutex(callback_queue_mutex);
}
SDL_UnlockMutex(callback_queue_mutex);
}
// Call the OPL emulator code to fill the specified buffer.
static void FillBuffer(int16_t *buffer, unsigned int nsamples)
{
// This seems like a reasonable assumption. mix_buffer is
// 1 second long, which should always be much longer than the
// SDL mix buffer.
assert(nsamples < mixing_freq);
OPL3_GenerateStream(&opl_chip, buffer, nsamples);
}
// Callback function to fill a new sound buffer:
static void OPL_Mix_Callback(void *udata,
Uint8 *byte_buffer,
int buffer_bytes)
{
int16_t *buffer;
unsigned int buffer_len;
unsigned int filled = 0;
// Buffer length in samples (quadrupled, because of 16-bit and stereo)
buffer = (int16_t *) byte_buffer;
buffer_len = buffer_bytes / 4;
// Repeatedly call the OPL emulator update function until the buffer is
// full.
while (filled < buffer_len)
{
uint64_t next_callback_time;
uint64_t nsamples;
SDL_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_sdl_paused || OPL_Queue_IsEmpty(callback_queue))
{
nsamples = buffer_len - filled;
}
else
{
next_callback_time = OPL_Queue_Peek(callback_queue) + pause_offset;
nsamples = (next_callback_time - current_time) * mixing_freq;
nsamples = (nsamples + OPL_SECOND - 1) / OPL_SECOND;
if (nsamples > buffer_len - filled)
{
nsamples = buffer_len - filled;
}
}
SDL_UnlockMutex(callback_queue_mutex);
// Add emulator output to buffer.
FillBuffer(buffer + filled * 2, nsamples);
filled += nsamples;
// Invoke callbacks for this point in time.
AdvanceTime(nsamples);
}
}
void OPL_SDL_Shutdown(void)
{
Mix_HookMusic(NULL, NULL);
if (sdl_was_initialized)
{
Mix_CloseAudio();
SDL_QuitSubSystem(SDL_INIT_AUDIO);
OPL_Queue_Destroy(callback_queue);
free(mix_buffer);
sdl_was_initialized = 0;
}
/*
if (opl_chip != NULL)
{
OPLDestroy(opl_chip);
opl_chip = NULL;
}
*/
if (callback_mutex != NULL)
{
SDL_DestroyMutex(callback_mutex);
callback_mutex = NULL;
}
if (callback_queue_mutex != NULL)
{
SDL_DestroyMutex(callback_queue_mutex);
callback_queue_mutex = NULL;
}
}
static unsigned int GetSliceSize(void)
{
int limit;
int n;
limit = (opl_sample_rate * MAX_SOUND_SLICE_TIME) / 1000;
// Try all powers of two, not exceeding the limit.
for (n=0;; ++n)
{
// 2^n <= limit < 2^n+1 ?
if ((1 << (n + 1)) > limit)
{
return (1 << n);
}
}
// Should never happen?
return 1024;
}
int OPL_SDL_Init(unsigned int port_base)
{
// Check if SDL_mixer has been opened already
// If not, we must initialize it now
if (!SDLIsInitialized())
{
if (SDL_Init(SDL_INIT_AUDIO) < 0)
{
fprintf(stderr, "Unable to set up sound.\n");
return 0;
}
if (Mix_OpenAudio(opl_sample_rate, AUDIO_S16SYS, 2, GetSliceSize()) < 0)
{
fprintf(stderr, "Error initialising SDL_mixer: %s\n", Mix_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return 0;
}
SDL_PauseAudio(0);
// When this module shuts down, it has the responsibility to
// shut down SDL.
sdl_was_initialized = 1;
}
else
{
sdl_was_initialized = 0;
}
opl_sdl_paused = 0;
pause_offset = 0;
// Queue structure of callbacks to invoke.
callback_queue = OPL_Queue_Create();
current_time = 0;
// Get the mixer frequency, format and number of channels.
Mix_QuerySpec(&mixing_freq, &mixing_format, &mixing_channels);
// Only supports AUDIO_S16SYS
if (mixing_format != AUDIO_S16SYS || mixing_channels != 2)
{
fprintf(stderr,
"OPL_SDL only supports native signed 16-bit LSB, "
"stereo format!\n");
OPL_SDL_Shutdown();
return 0;
}
// Mix buffer:
mix_buffer = malloc(mixing_freq * sizeof(uint32_t) * 2);
// Create the emulator structure:
OPL3_Reset(&opl_chip, mixing_freq);
opl_opl3mode = 0;
callback_mutex = SDL_CreateMutex();
callback_queue_mutex = SDL_CreateMutex();
// TODO: This should be music callback? or-?
Mix_HookMusic(OPL_Mix_Callback, NULL);
return 1;
}
unsigned int OPL_SDL_ReadPort(opl_port_t port)
{
if (port == OPL_REGISTER_PORT_OPL3)
{
return 0xff;
}
return 0;
}
static void WriteRegister(unsigned int reg_num, unsigned int value)
{
switch (reg_num)
{
case OPL_REG_TIMER1:
case OPL_REG_TIMER2:
case OPL_REG_TIMER_CTRL:
// andrewj: removed support for these
break;
case OPL_REG_NEW:
opl_opl3mode = value & 0x01;
default:
OPL3_WriteRegBuffered(&opl_chip, reg_num, value);
break;
}
}
void OPL_SDL_WritePort(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);
}
}
void OPL_SDL_SetCallback(uint64_t us, opl_callback_t callback,
void *data)
{
SDL_LockMutex(callback_queue_mutex);
OPL_Queue_Push(callback_queue, callback, data,
current_time - pause_offset + us);
SDL_UnlockMutex(callback_queue_mutex);
}
void OPL_SDL_ClearCallbacks(void)
{
SDL_LockMutex(callback_queue_mutex);
OPL_Queue_Clear(callback_queue);
SDL_UnlockMutex(callback_queue_mutex);
}
void OPL_SDL_Lock(void)
{
SDL_LockMutex(callback_mutex);
}
void OPL_SDL_Unlock(void)
{
SDL_UnlockMutex(callback_mutex);
}
void OPL_SDL_SetPaused(int paused)
{
opl_sdl_paused = paused;
}
void OPL_SDL_AdjustCallbacks(float factor)
{
SDL_LockMutex(callback_queue_mutex);
OPL_Queue_AdjustCallbacks(callback_queue, current_time, factor);
SDL_UnlockMutex(callback_queue_mutex);
}
//
// Higher-level functions, based on the lower-level functions above
// (register write, etc).
//
// Initialize the OPL library. Return value indicates type of OPL chip
// detected, if any.
opl_init_result_t OPL_Init(unsigned int port_base)
{
if (! OPL_SDL_Init(port_base))
{
printf("OPL_Init: Failed to find a working driver.\n");
return OPL_INIT_NONE;
}
printf("OPL_Init: emulating OPL2 via the 'SDL' driver.\n");
return OPL_INIT_OPL2;
}
// Write an OPL register value
void OPL_WriteRegister(int reg, int value)
{
if (reg & 0x100)
{
OPL_SDL_WritePort(OPL_REGISTER_PORT_OPL3, reg);
}
else
{
OPL_SDL_WritePort(OPL_REGISTER_PORT, reg);
}
// For timing, read the register port six times after writing the
// register number to cause the appropriate delay
int i;
for (i=0; i<6; ++i)
{
OPL_SDL_ReadPort(OPL_DATA_PORT);
}
OPL_SDL_WritePort(OPL_DATA_PORT, value);
// Read the register port 24 times after writing the value to
// cause the appropriate delay
for (i=0; i<24; ++i)
{
OPL_SDL_ReadPort(OPL_REGISTER_PORT);
}
}
// Initialize registers on startup
void OPL_InitRegisters(int opl3)
{
int r;
// Initialize level registers
for (r=OPL_REGS_LEVEL; r <= OPL_REGS_LEVEL + OPL_NUM_OPERATORS; ++r)
{
OPL_WriteRegister(r, 0x3f);
}
// Initialize other registers
// These two loops write to registers that actually don't exist,
// but this is what Doom does ...
// Similarly, the <= is also intenational.
for (r=OPL_REGS_ATTACK; r <= OPL_REGS_WAVEFORM + OPL_NUM_OPERATORS; ++r)
{
OPL_WriteRegister(r, 0x00);
}
// More registers ...
for (r=1; r < OPL_REGS_LEVEL; ++r)
{
OPL_WriteRegister(r, 0x00);
}
// Re-initialize the low registers:
// Reset both timers and enable interrupts:
OPL_WriteRegister(OPL_REG_TIMER_CTRL, 0x60);
OPL_WriteRegister(OPL_REG_TIMER_CTRL, 0x80);
// "Allow FM chips to control the waveform of each operator":
OPL_WriteRegister(OPL_REG_WAVEFORM_ENABLE, 0x20);
if (opl3)
{
OPL_WriteRegister(OPL_REG_NEW, 0x01);
// Initialize level registers
for (r=OPL_REGS_LEVEL; r <= OPL_REGS_LEVEL + OPL_NUM_OPERATORS; ++r)
{
OPL_WriteRegister(r | 0x100, 0x3f);
}
// Initialize other registers
// These two loops write to registers that actually don't exist,
// but this is what Doom does ...
// Similarly, the <= is also intenational.
for (r=OPL_REGS_ATTACK; r <= OPL_REGS_WAVEFORM + OPL_NUM_OPERATORS; ++r)
{
OPL_WriteRegister(r | 0x100, 0x00);
}
// More registers ...
for (r=1; r < OPL_REGS_LEVEL; ++r)
{
OPL_WriteRegister(r | 0x100, 0x00);
}
}
// Keyboard split point on (?)
OPL_WriteRegister(OPL_REG_FM_MODE, 0x40);
if (opl3)
{
OPL_WriteRegister(OPL_REG_NEW, 0x01);
}
}