nestopia/source/linux/oss.cpp
2012-09-02 13:13:55 -04:00

567 lines
11 KiB
C++

/***************************************************************************
oss.c - M1 Linux audio output driver - supports SDL, OSS, and ALSA
***************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/soundcard.h>
#include <SDL.h>
#define ALSA_PCM_NEW_HW_PARAMS_API
#define ALSA_PCM_NEW_SW_PARAMS_API
#include <alsa/asoundlib.h>
#include "oss.h"
#define VALGRIND (0) // disables sound output for easier Valgrind usage
#define NUM_FRAGS_BROKEN (8)
#define NUM_FRAGS_NORMAL (4)
static INT32 num_frags;
#define OSS_FRAGMENT (0x000D | (num_frags<<16)); // 16k fragments (2 * 2^14).
// local variables
void (*m1sdr_Callback)(unsigned long dwNumSamples, signed short *data);
unsigned long cbUserData;
static int hw_present, oss_pause;
static INT32 is_broken_driver;
int nDSoundSegLen = 0;
static int oss_nw = 0, oss_playing = 0;
int audiofd;
static snd_pcm_t *pHandle = NULL;
int lnxdrv_apimode = 0; // 0 = SDL, 1 = ALSA, 2 = OSS
static INT16 samples[(48000*2)/10];
#define kMaxBuffers (4) // this adjusts the latency for SDL audio
static volatile INT16 *buffer[kMaxBuffers];
static volatile int bufstat[kMaxBuffers];
static int playbuf, writebuf;
static Uint8 *curpos;
static int bytes_left;
static void fill_buffer(int bufnum)
{
int bytes_to_fill, bufpos;
Uint8 *bufptr;
// printf("FB%d\n", bufnum);
// figure out how much we need out of this buffer
// vs how much we can get out of it
if (bytes_left >= bufstat[bufnum])
{
bytes_to_fill = bufstat[bufnum];
}
else
{
bytes_to_fill = bytes_left;
}
// copy from the buffer's current position
bufptr = (Uint8 *)buffer[bufnum];
bufpos = (nDSoundSegLen*2*sizeof(INT16)) - bufstat[bufnum];
bufptr += bufpos;
memcpy(curpos, bufptr, bytes_to_fill);
// reduce the counters
curpos += bytes_to_fill;
bufstat[bufnum] -= bytes_to_fill;
bytes_left -= bytes_to_fill;
}
static void sdl_callback(void *userdata, Uint8 *stream, int len)
{
int temp;
curpos = stream;
bytes_left = len;
// need more data?
while (bytes_left > 0)
{
// does our current buffer have any samples?
if (bufstat[playbuf] > 0)
{
fill_buffer(playbuf);
}
else
{
// check if the next buffer would collide
temp = playbuf + 1;
if (temp >= kMaxBuffers)
{
temp = 0;
}
// no collision, set it and continue looping
if (temp != writebuf)
{
playbuf = temp;
}
else
{
// printf("UF\n");
// underflow!
memset(curpos, 0, bytes_left);
bytes_left = 0;
}
}
}
}
// set # of samples per update
void m1sdr_SetSamplesPerTick(UINT32 spf)
{
int i;
nDSoundSegLen = spf;
if (lnxdrv_apimode == 0)
{
for (i = 0; i < kMaxBuffers; i++)
{
if (buffer[i])
{
free((void *)buffer[i]);
buffer[i] = (volatile INT16 *)NULL;
}
buffer[i] = (volatile INT16 *)malloc(nDSoundSegLen * 2 * sizeof(UINT16));
if (!buffer[i])
{
printf("Couldn't alloc buffer for SDL audio!\n");
exit(-1);
}
memset((void *)buffer[i], 0, nDSoundSegLen * 2 * sizeof(UINT16));
bufstat[i] = 0;
}
playbuf = 0;
writebuf = 1;
}
}
// m1sdr_Update - timer callback routine: runs sequencer and mixes sound
void m1sdr_Update(void)
{
if ((m1sdr_Callback) && (!oss_pause))
{
if (lnxdrv_apimode == 0)
{
m1sdr_Callback(nDSoundSegLen, (INT16 *)buffer[writebuf]);
bufstat[writebuf] = nDSoundSegLen * 2 * sizeof(UINT16);
if (++writebuf >= kMaxBuffers)
{
writebuf = 0;
}
}
else
{
m1sdr_Callback(nDSoundSegLen, (INT16 *)samples);
}
}
if (oss_pause)
{
memset(samples, 0, nDSoundSegLen*4);
}
}
// checks the play position to see if we should trigger another update
void m1sdr_TimeCheck(void)
{
int timeout;
snd_pcm_sframes_t delay = 0;
#if VALGRIND
m1sdr_Update();
#else
switch (lnxdrv_apimode)
{
case 0: // SDL
SDL_LockAudio();
while ((bufstat[writebuf] == 0) && (writebuf != playbuf))
{
m1sdr_Update();
}
SDL_UnlockAudio();
break;
case 1: // ALSA
if ((!pHandle) || (!oss_playing))
{
m1sdr_Update();
return;
}
// get how many samples are buffered
snd_pcm_delay(pHandle, &delay);
// if we don't have at least 4 audio frames,
// feed it more
// HACK: ALSA likes to go completely batshit under some circumstances and claim either large
// positive or large negative numbers are buffered. These get things going again.
if (delay < 0) delay = 0;
if (delay > 1000000) delay = 10;
timeout = 20; // HACK 2: prevent ALSA from getting stuck when exiting a game
while ((delay <= nDSoundSegLen*4) && (timeout > 0))
{
m1sdr_Update();
snd_pcm_writei(pHandle, samples, nDSoundSegLen);
snd_pcm_prepare(pHandle);
snd_pcm_delay(pHandle, &delay);
timeout--;
}
break;
case 2: // OSS
audio_buf_info info;
if ((audiofd == -1) || (!oss_playing))
{
m1sdr_Update();
return;
}
ioctl(audiofd, SNDCTL_DSP_GETOSPACE, &info);
if (oss_nw)
{
int err;
m1sdr_Update();
// output the generated samples
err = write(audiofd, samples, nDSoundSegLen * 4);
if (err == -1)
{
perror("write\n");
}
}
else
{
while (info.bytes >= (nDSoundSegLen * 4))
{
m1sdr_Update();
// output the generated samples
write(audiofd, samples, nDSoundSegLen * 4);
ioctl(audiofd, SNDCTL_DSP_GETOSPACE, &info);
}
}
break;
}
usleep(50);
#endif
}
// m1sdr_Init - inits the output device and our global state
INT16 m1sdr_Init(int sample_rate)
{
int format, stereo, rate, fsize, err;
unsigned int nfreq, periodtime;
snd_pcm_hw_params_t *hwparams;
hw_present = 0;
m1sdr_Callback = NULL;
switch (lnxdrv_apimode)
{
case 0: // SDL
SDL_AudioSpec aspec;
SDL_InitSubSystem(SDL_INIT_AUDIO);
m1sdr_SetSamplesPerTick(sample_rate/60);
playbuf = 0;
writebuf = 1;
aspec.freq = sample_rate;
aspec.format = AUDIO_S16SYS; // keep endian independant
aspec.channels = 2;
aspec.samples = 512; // has to be a power of 2, and we want it smaller than our buffer size
aspec.callback = sdl_callback;
aspec.userdata = 0;
if (SDL_OpenAudio(&aspec, NULL) < 0)
{
printf("ERROR: can't open SDL audio\n");
return 0;
}
// make sure we don't start yet
SDL_PauseAudio(1);
break;
case 1: // ALSA
// Try to open audio device
if ((err = snd_pcm_open(&pHandle, "plughw:0,0", SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
fprintf(stderr, "ALSA: Could not open soundcard (%s)\n", snd_strerror(err));
hw_present = 0;
return 0;
}
if ((err = snd_pcm_hw_params_malloc(&hwparams)) < 0) {
fprintf (stderr, "cannot allocate hardware parameter structure (%s)\n",
snd_strerror(err));
return 0;
}
// Init hwparams with full configuration space
if ((err = snd_pcm_hw_params_any(pHandle, hwparams)) < 0) {
fprintf(stderr, "ALSA: couldn't set hw params (%s)\n", snd_strerror(err));
hw_present = 0;
return 0;
}
// Set access type
if ((err = snd_pcm_hw_params_set_access(pHandle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
fprintf(stderr, "ALSA: can't set access (%s)\n", snd_strerror(err));
return 0;
}
// Set sample format
if ((err = snd_pcm_hw_params_set_format(pHandle, hwparams, SND_PCM_FORMAT_S16)) < 0) {
fprintf(stderr, "ALSA: can't set format (%s)\n", snd_strerror(err));
return 0;
}
// Set sample rate (nearest possible)
nfreq = sample_rate;
if ((err = snd_pcm_hw_params_set_rate_near(pHandle, hwparams, &nfreq, 0)) < 0) {
fprintf(stderr, "ALSA: can't set sample rate (%s)\n", snd_strerror(err));
return 0;
}
// Set number of channels
if ((err = snd_pcm_hw_params_set_channels(pHandle, hwparams, 2)) < 0) {
fprintf(stderr, "ALSA: can't set stereo (%s)\n", snd_strerror(err));
return 0;
}
// Set period time (nearest possible)
periodtime = 20000;
if ((err = snd_pcm_hw_params_set_period_time_near(pHandle, hwparams, &periodtime, 0)) < 0) {
fprintf(stderr, "ALSA: can't set period time (%s)\n", snd_strerror(err));
return 0;
}
// Apply HW parameter settings to PCM device and prepare device
if ((err = snd_pcm_hw_params(pHandle, hwparams)) < 0) {
fprintf(stderr, "ALSA: unable to install hw_params (%s)\n", snd_strerror(err));
snd_pcm_hw_params_free(hwparams);
return 0;
}
snd_pcm_hw_params_free(hwparams);
if ((err = snd_pcm_prepare(pHandle)) < 0) {
fprintf (stderr, "cannot prepare audio interface for use (%s)\n", snd_strerror(err));
return 0;
}
break;
case 2: // OSS
audiofd = open("/dev/dsp", O_WRONLY, 0);
if (audiofd == -1)
{
perror("/dev/dsp");
printf("ERROR: unable to open soundcard. Aborting.\n");
return(0);
}
// reset things
ioctl(audiofd, SNDCTL_DSP_RESET, 0);
is_broken_driver = 0;
num_frags = NUM_FRAGS_NORMAL;
// set the buffer size we want
fsize = OSS_FRAGMENT;
if (ioctl(audiofd, SNDCTL_DSP_SETFRAGMENT, &fsize) == - 1)
{
perror("SNDCTL_DSP_SETFRAGMENT");
return(0);
}
// set 16-bit output
format = AFMT_S16_NE; // 16 bit signed "native"-endian
if (ioctl(audiofd, SNDCTL_DSP_SETFMT, &format) == - 1)
{
perror("SNDCTL_DSP_SETFMT");
return(0);
}
// now set stereo
stereo = 1;
if (ioctl(audiofd, SNDCTL_DSP_STEREO, &stereo) == - 1)
{
perror("SNDCTL_DSP_STEREO");
return(0);
}
// and the sample rate
rate = sample_rate;
if (ioctl(audiofd, SNDCTL_DSP_SPEED, &rate) == - 1)
{
perror("SNDCTL_DSP_SPEED");
return(0);
}
// and make sure that did what we wanted
ioctl(audiofd, SNDCTL_DSP_GETBLKSIZE, &fsize);
break;
}
hw_present = 1;
return (1);
}
void m1sdr_Exit(void)
{
int i;
if (!hw_present) return;
switch (lnxdrv_apimode)
{
case 0: // SDL
SDL_QuitSubSystem(SDL_INIT_AUDIO);
for (i = 0; i < kMaxBuffers; i++)
{
if (buffer[i])
{
free((void *)buffer[i]);
buffer[i] = (volatile INT16 *)NULL;
}
}
break;
case 1: // ALSA
snd_pcm_close(pHandle);
break;
case 2: // OSS
close(audiofd);
break;
}
}
void m1sdr_SetCallback(void *fn)
{
if (fn == (void *)NULL)
{
printf("ERROR: NULL CALLBACK!\n");
}
m1sdr_Callback = (void (*)(unsigned long, signed short *))fn;
}
INT16 m1sdr_IsThere(void)
{
int err;
if (lnxdrv_apimode == 1)
{
if ((err = snd_pcm_open(&pHandle, "plughw:0,0", SND_PCM_STREAM_PLAYBACK, 0)) != 0)
{
printf("Error accessing soundcard, sound will be disabled\n");
hw_present = 0;
return(0);
}
snd_pcm_close(pHandle);
hw_present = 1;
}
else if (lnxdrv_apimode == 2)
{
audiofd = open("/dev/dsp", O_WRONLY, 0);
if (audiofd == -1)
{
printf("Error accessing soundcard, sound will be disabled\n");
hw_present = 0;
return(0);
}
close(audiofd);
hw_present = 1;
}
else if (lnxdrv_apimode == 0)
{
hw_present = 1; // always say it's present for SDL
}
return (1);
}
INT32 m1sdr_HwPresent(void)
{
return hw_present;
}
void m1sdr_PlayStart(void)
{
if (lnxdrv_apimode == 0)
{
SDL_PauseAudio(0);
}
oss_playing = 1;
}
void m1sdr_PlayStop(void)
{
if (lnxdrv_apimode == 0)
{
SDL_PauseAudio(1);
}
oss_playing = 0;
}
void m1sdr_FlushAudio(void)
{
memset(samples, 0, nDSoundSegLen * 4);
if (lnxdrv_apimode == 2)
{
write(audiofd, samples, nDSoundSegLen * 4);
write(audiofd, samples, nDSoundSegLen * 4);
}
}
void m1sdr_Pause(int set)
{
oss_pause = set;
}
void m1sdr_SetNoWait(int nw)
{
oss_nw = nw;
}