524 lines
11 KiB
C
524 lines
11 KiB
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 interface.
|
|
//
|
|
|
|
#include "config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "SDL.h"
|
|
|
|
#include "opl.h"
|
|
#include "opl_internal.h"
|
|
|
|
//#define OPL_DEBUG_TRACE
|
|
|
|
#ifdef HAVE_IOPERM
|
|
extern opl_driver_t opl_linux_driver;
|
|
#endif
|
|
#if defined(HAVE_LIBI386) || defined(HAVE_LIBAMD64)
|
|
extern opl_driver_t opl_openbsd_driver;
|
|
#endif
|
|
#ifdef _WIN32
|
|
extern opl_driver_t opl_win32_driver;
|
|
#endif
|
|
extern opl_driver_t opl_sdl_driver;
|
|
|
|
static opl_driver_t *drivers[] =
|
|
{
|
|
#ifdef HAVE_IOPERM
|
|
&opl_linux_driver,
|
|
#endif
|
|
#if defined(HAVE_LIBI386) || defined(HAVE_LIBAMD64)
|
|
&opl_openbsd_driver,
|
|
#endif
|
|
#ifdef _WIN32
|
|
&opl_win32_driver,
|
|
#endif
|
|
&opl_sdl_driver,
|
|
NULL
|
|
};
|
|
|
|
static opl_driver_t *driver = NULL;
|
|
static int init_stage_reg_writes = 1;
|
|
|
|
unsigned int opl_sample_rate = 22050;
|
|
|
|
//
|
|
// Init/shutdown code.
|
|
//
|
|
|
|
// Initialize the specified driver and detect an OPL chip. Returns
|
|
// true if an OPL is detected.
|
|
|
|
static opl_init_result_t InitDriver(opl_driver_t *_driver,
|
|
unsigned int port_base)
|
|
{
|
|
opl_init_result_t result1, result2;
|
|
|
|
// Initialize the driver.
|
|
|
|
if (!_driver->init_func(port_base))
|
|
{
|
|
return OPL_INIT_NONE;
|
|
}
|
|
|
|
// The driver was initialized okay, so we now have somewhere
|
|
// to write to. It doesn't mean there's an OPL chip there,
|
|
// though. Perform the detection sequence to make sure.
|
|
// (it's done twice, like how Doom does it).
|
|
|
|
driver = _driver;
|
|
init_stage_reg_writes = 1;
|
|
|
|
result1 = OPL_Detect();
|
|
result2 = OPL_Detect();
|
|
if (result1 == OPL_INIT_NONE || result2 == OPL_INIT_NONE)
|
|
{
|
|
printf("OPL_Init: No OPL detected using '%s' driver.\n", _driver->name);
|
|
_driver->shutdown_func();
|
|
driver = NULL;
|
|
return OPL_INIT_NONE;
|
|
}
|
|
|
|
init_stage_reg_writes = 0;
|
|
|
|
printf("OPL_Init: Using driver '%s'.\n", driver->name);
|
|
|
|
return result2;
|
|
}
|
|
|
|
// Find a driver automatically by trying each in the list.
|
|
|
|
static opl_init_result_t AutoSelectDriver(unsigned int port_base)
|
|
{
|
|
int i;
|
|
opl_init_result_t result;
|
|
|
|
for (i=0; drivers[i] != NULL; ++i)
|
|
{
|
|
result = InitDriver(drivers[i], port_base);
|
|
if (result != OPL_INIT_NONE)
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
printf("OPL_Init: Failed to find a working driver.\n");
|
|
|
|
return OPL_INIT_NONE;
|
|
}
|
|
|
|
// Initialize the OPL library. Return value indicates type of OPL chip
|
|
// detected, if any.
|
|
|
|
opl_init_result_t OPL_Init(unsigned int port_base)
|
|
{
|
|
char *driver_name;
|
|
int i;
|
|
int result;
|
|
|
|
driver_name = getenv("OPL_DRIVER");
|
|
|
|
if (driver_name != NULL)
|
|
{
|
|
// Search the list until we find the driver with this name.
|
|
|
|
for (i=0; drivers[i] != NULL; ++i)
|
|
{
|
|
if (!strcmp(driver_name, drivers[i]->name))
|
|
{
|
|
result = InitDriver(drivers[i], port_base);
|
|
if (result)
|
|
{
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
printf("OPL_Init: Failed to initialize "
|
|
"driver: '%s'.\n", driver_name);
|
|
return OPL_INIT_NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
printf("OPL_Init: unknown driver: '%s'.\n", driver_name);
|
|
|
|
return OPL_INIT_NONE;
|
|
}
|
|
else
|
|
{
|
|
return AutoSelectDriver(port_base);
|
|
}
|
|
}
|
|
|
|
// Shut down the OPL library.
|
|
|
|
void OPL_Shutdown(void)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
driver->shutdown_func();
|
|
driver = NULL;
|
|
}
|
|
}
|
|
|
|
// Set the sample rate used for software OPL emulation.
|
|
|
|
void OPL_SetSampleRate(unsigned int rate)
|
|
{
|
|
opl_sample_rate = rate;
|
|
}
|
|
|
|
void OPL_WritePort(opl_port_t port, unsigned int value)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
#ifdef OPL_DEBUG_TRACE
|
|
printf("OPL_write: %i, %x\n", port, value);
|
|
fflush(stdout);
|
|
#endif
|
|
driver->write_port_func(port, value);
|
|
}
|
|
}
|
|
|
|
unsigned int OPL_ReadPort(opl_port_t port)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
unsigned int result;
|
|
|
|
#ifdef OPL_DEBUG_TRACE
|
|
printf("OPL_read: %i...\n", port);
|
|
fflush(stdout);
|
|
#endif
|
|
|
|
result = driver->read_port_func(port);
|
|
|
|
#ifdef OPL_DEBUG_TRACE
|
|
printf("OPL_read: %i -> %x\n", port, result);
|
|
fflush(stdout);
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Higher-level functions, based on the lower-level functions above
|
|
// (register write, etc).
|
|
//
|
|
|
|
unsigned int OPL_ReadStatus(void)
|
|
{
|
|
return OPL_ReadPort(OPL_REGISTER_PORT);
|
|
}
|
|
|
|
// Write an OPL register value
|
|
|
|
void OPL_WriteRegister(int reg, int value)
|
|
{
|
|
int i;
|
|
|
|
if (reg & 0x100)
|
|
{
|
|
OPL_WritePort(OPL_REGISTER_PORT_OPL3, reg);
|
|
}
|
|
else
|
|
{
|
|
OPL_WritePort(OPL_REGISTER_PORT, reg);
|
|
}
|
|
|
|
// For timing, read the register port six times after writing the
|
|
// register number to cause the appropriate delay
|
|
|
|
for (i=0; i<6; ++i)
|
|
{
|
|
// An oddity of the Doom OPL code: at startup initialization,
|
|
// the spacing here is performed by reading from the register
|
|
// port; after initialization, the data port is read, instead.
|
|
|
|
if (init_stage_reg_writes)
|
|
{
|
|
OPL_ReadPort(OPL_REGISTER_PORT);
|
|
}
|
|
else
|
|
{
|
|
OPL_ReadPort(OPL_DATA_PORT);
|
|
}
|
|
}
|
|
|
|
OPL_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_ReadStatus();
|
|
}
|
|
}
|
|
|
|
// Detect the presence of an OPL chip
|
|
|
|
opl_init_result_t OPL_Detect(void)
|
|
{
|
|
int result1, result2;
|
|
int i;
|
|
|
|
// Reset both timers:
|
|
OPL_WriteRegister(OPL_REG_TIMER_CTRL, 0x60);
|
|
|
|
// Enable interrupts:
|
|
OPL_WriteRegister(OPL_REG_TIMER_CTRL, 0x80);
|
|
|
|
// Read status
|
|
result1 = OPL_ReadStatus();
|
|
|
|
// Set timer:
|
|
OPL_WriteRegister(OPL_REG_TIMER1, 0xff);
|
|
|
|
// Start timer 1:
|
|
OPL_WriteRegister(OPL_REG_TIMER_CTRL, 0x21);
|
|
|
|
// Wait for 80 microseconds
|
|
// This is how Doom does it:
|
|
|
|
for (i=0; i<200; ++i)
|
|
{
|
|
OPL_ReadStatus();
|
|
}
|
|
|
|
OPL_Delay(1 * OPL_MS);
|
|
|
|
// Read status
|
|
result2 = OPL_ReadStatus();
|
|
|
|
// Reset both timers:
|
|
OPL_WriteRegister(OPL_REG_TIMER_CTRL, 0x60);
|
|
|
|
// Enable interrupts:
|
|
OPL_WriteRegister(OPL_REG_TIMER_CTRL, 0x80);
|
|
|
|
if ((result1 & 0xe0) == 0x00 && (result2 & 0xe0) == 0xc0)
|
|
{
|
|
result1 = OPL_ReadPort(OPL_REGISTER_PORT);
|
|
result2 = OPL_ReadPort(OPL_REGISTER_PORT_OPL3);
|
|
if (result1 == 0x00)
|
|
{
|
|
return OPL_INIT_OPL3;
|
|
}
|
|
else
|
|
{
|
|
return OPL_INIT_OPL2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return OPL_INIT_NONE;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Timer functions.
|
|
//
|
|
|
|
void OPL_SetCallback(uint64_t us, opl_callback_t callback, void *data)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
driver->set_callback_func(us, callback, data);
|
|
}
|
|
}
|
|
|
|
void OPL_ClearCallbacks(void)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
driver->clear_callbacks_func();
|
|
}
|
|
}
|
|
|
|
void OPL_Lock(void)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
driver->lock_func();
|
|
}
|
|
}
|
|
|
|
void OPL_Unlock(void)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
driver->unlock_func();
|
|
}
|
|
}
|
|
|
|
typedef struct
|
|
{
|
|
int finished;
|
|
|
|
SDL_mutex *mutex;
|
|
SDL_cond *cond;
|
|
} delay_data_t;
|
|
|
|
static void DelayCallback(void *_delay_data)
|
|
{
|
|
delay_data_t *delay_data = _delay_data;
|
|
|
|
SDL_LockMutex(delay_data->mutex);
|
|
delay_data->finished = 1;
|
|
|
|
SDL_CondSignal(delay_data->cond);
|
|
|
|
SDL_UnlockMutex(delay_data->mutex);
|
|
}
|
|
|
|
void OPL_Delay(uint64_t us)
|
|
{
|
|
delay_data_t delay_data;
|
|
|
|
if (driver == NULL)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Create a callback that will signal this thread after the
|
|
// specified time.
|
|
|
|
delay_data.finished = 0;
|
|
delay_data.mutex = SDL_CreateMutex();
|
|
delay_data.cond = SDL_CreateCond();
|
|
|
|
OPL_SetCallback(us, DelayCallback, &delay_data);
|
|
|
|
// Wait until the callback is invoked.
|
|
|
|
SDL_LockMutex(delay_data.mutex);
|
|
|
|
while (!delay_data.finished)
|
|
{
|
|
SDL_CondWait(delay_data.cond, delay_data.mutex);
|
|
}
|
|
|
|
SDL_UnlockMutex(delay_data.mutex);
|
|
|
|
// Clean up.
|
|
|
|
SDL_DestroyMutex(delay_data.mutex);
|
|
SDL_DestroyCond(delay_data.cond);
|
|
}
|
|
|
|
void OPL_SetPaused(int paused)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
driver->set_paused_func(paused);
|
|
}
|
|
}
|
|
|
|
void OPL_AdjustCallbacks(float value)
|
|
{
|
|
if (driver != NULL)
|
|
{
|
|
driver->adjust_callbacks_func(value);
|
|
}
|
|
}
|
|
|