quisk-kc4upr/sound_portaudio.c

519 lines
15 KiB
C
Executable File

/*
* This module provides sound access for QUISK using the portaudio library.
*/
#include <Python.h>
#include <complex.h>
#include <math.h>
#include <portaudio.h>
#include <sys/time.h>
#include <time.h>
#include "quisk.h"
/*
The sample rate is in frames per second. Each frame has a number of channels,
and each channel has a sample of size sample_bytes. The channels are interleaved:
(channel0, channel1), (channel0, channel1), ...
*/
extern struct sound_conf quisk_sound_state; // Current sound status
static float fbuffer[SAMP_BUFFER_SIZE]; // Buffer for float32 samples from sound
int quisk_read_portaudio(struct sound_dev * dev, complex double * cSamples)
{ // cSamples can be NULL to discard samples
// Read sound samples from the soundcard.
// Samples are converted to 32 bits with a range of +/- CLIP32 and placed into cSamples.
int i;
long avail;
int nSamples;
PaError error;
float fi, fq;
if (!dev->handle)
return -1;
avail = Pa_GetStreamReadAvailable((PaStream * )dev->handle);
dev->dev_latency = avail;
if (dev->read_frames == 0) { // non-blocking: read available frames
if (avail > SAMP_BUFFER_SIZE / dev->num_channels) // limit read request to buffer size
avail = SAMP_BUFFER_SIZE / dev->num_channels;
}
else { // size of read request
avail = dev->read_frames;
}
error = Pa_ReadStream ((PaStream * )dev->handle, fbuffer, avail);
if (error != paNoError) {
dev->dev_error++;
}
nSamples = 0;
for (i = 0; avail; i += dev->num_channels, avail--) {
fi = fbuffer[i + dev->channel_I];
fq = fbuffer[i + dev->channel_Q];
if (fi >= 1.0 || fi <= -1.0)
dev->overrange++; // assume overrange returns max int
if (fq >= 1.0 || fq <= -1.0)
dev->overrange++;
if (cSamples)
cSamples[nSamples] = (fi + I * fq) * CLIP32;
nSamples++;
if (nSamples > SAMP_BUFFER_SIZE * 8 / 10)
break;
}
return nSamples;
}
void quisk_play_portaudio(struct sound_dev * playdev, int nSamples, complex double * cSamples,
int report_latency, double volume)
{ // play the samples; write them to the portaudio soundcard
int i, n, index;
long delay;
float fi, fq;
PaError error;
if (!playdev->handle || nSamples <= 0)
return;
// "delay" is the number of samples left in the play buffer
delay = playdev->play_buf_size - Pa_GetStreamWriteAvailable(playdev->handle);
//printf ("play available %ld\n", Pa_GetStreamWriteAvailable(playdev->handle));
playdev->dev_latency = delay;
if (report_latency) { // Report for main playback device
quisk_sound_state.latencyPlay = delay;
}
//printf ("nSamples %d, delay %ld\n", nSamples, delay);
index = 0;
#if 0
// Timing is too crude to support this logic
if (nSamples + delay > playdev->latency_frames * 9 / 10) {
nSamples--;
#if DEBUG_IO
printf("Remove a sample %s nSamples %4d delay %4d total %4d\n", playdev->name, nSamples, (int)delay, nSamples + (int)delay);
#endif
}
else if(nSamples + delay < playdev->latency_frames * 5 / 10) {
cSamples[nSamples] = cSamples[nSamples - 1];
nSamples++;
#if DEBUG_IO
printf("Add a sample %s nSamples %4d delay %4d total %4d\n", playdev->name, nSamples, (int)delay, nSamples + (int)delay);
#endif
}
#endif
if (nSamples + delay > playdev->latency_frames) { // too many samples
index = nSamples + delay - playdev->latency_frames; // write only the most recent samples
if (index > nSamples)
index = nSamples;
quisk_sound_state.write_error++;
playdev->dev_error++;
#if DEBUG_IO
printf("Discard %d of %d samples at %d delay\n", index, nSamples, (int)delay);
#endif
if (nSamples == index) // no samples to play
return;
}
else if (delay < 16) { // Buffer is too empty; fill it back up with zeros.
n = playdev->latency_frames * 7 / 10 - nSamples;
#if DEBUG_IO
printf("Add %d zero samples at %ld delay\n", n, delay);
#endif
for (i = 0; i < n; i++)
cSamples[nSamples++] = 0;
}
for (i = 0, n = index; n < nSamples; i += playdev->num_channels, n++) {
fi = volume * creal(cSamples[n]);
fq = volume * cimag(cSamples[n]);
fbuffer[i + playdev->channel_I] = fi / CLIP32;
fbuffer[i + playdev->channel_Q] = fq / CLIP32;
}
error = Pa_WriteStream ((PaStream * )playdev->handle, fbuffer, nSamples - index);
//printf ("Write %d\n", nSamples - index);
if (error == paNoError)
;
else if (error == paOutputUnderflowed) {
quisk_sound_state.underrun_error++;
playdev->dev_underrun++;
}
else {
quisk_sound_state.write_error++;
playdev->dev_error++;
#if DEBUG_IO
printf ("Play error: %s\n", Pa_GetErrorText(error));
#endif
}
}
static void info_portaudio (struct sound_dev * cDev, struct sound_dev * pDev)
{ // Return information about the device
const PaDeviceInfo * info;
PaStreamParameters params;
int index, rate;
if (cDev)
index = cDev->portaudio_index;
else if (pDev)
index = pDev->portaudio_index;
else
return;
info = Pa_GetDeviceInfo(index);
if ( ! info)
return;
params.device = index;
params.channelCount = 1;
params.sampleFormat = paFloat32;
params.suggestedLatency = 0.10;
params.hostApiSpecificStreamInfo = NULL;
if (cDev) {
cDev->chan_min = 1;
cDev->chan_max = info->maxInputChannels;
cDev->rate_min = cDev->rate_max = 0;
cDev->portaudio_latency = info->defaultHighInputLatency;
#if DEBUG_IO
printf ("Capture latency low %lf, high %lf\n",
info->defaultLowInputLatency, info->defaultHighInputLatency);
#endif
for (rate = 8000; rate <= 384000; rate += 8000) {
if (Pa_IsFormatSupported(&params, NULL, rate) == paFormatIsSupported) {
if (cDev->rate_min == 0)
cDev->rate_min = rate;
cDev->rate_max = rate;
}
}
}
if (pDev) {
pDev->chan_min = 1;
pDev->chan_max = info->maxOutputChannels;
pDev->rate_min = pDev->rate_max = 0;
pDev->portaudio_latency = quisk_sound_state.latency_millisecs / 1000.0 * 2.0;
if (pDev->portaudio_latency < info->defaultHighOutputLatency)
pDev->portaudio_latency = info->defaultHighOutputLatency;
#if DEBUG_IO
printf ("Play latency low %lf, high %lf\n",
info->defaultLowOutputLatency, info->defaultHighOutputLatency);
#endif
for (rate = 8000; rate <= 384000; rate += 8000) {
if (Pa_IsFormatSupported(&params, NULL, rate) == paFormatIsSupported) {
if (pDev->rate_min == 0)
pDev->rate_min = rate;
pDev->rate_max = rate;
}
}
}
}
static int quisk_pa_name2index (struct sound_dev * dev, int is_capture)
{ // Based on the device name, set the portaudio index, or -1.
// Return non-zero for error. Not a portaudio device is not an error.
const PaDeviceInfo * pInfo;
int i, count;
if (strncmp (dev->name, "portaudio", 9)) {
dev->portaudio_index = -1; // Name does not start with "portaudio"
return 0; // Not a portaudio device, not an error
}
if ( ! strcmp (dev->name, "portaudiodefault")) {
if (is_capture) // Fill in the default device index
dev->portaudio_index = Pa_GetDefaultInputDevice();
else
dev->portaudio_index = Pa_GetDefaultOutputDevice();
strncpy (dev->msg1, "Using default portaudio device", QUISK_SC_SIZE);
return 0;
}
if ( ! strncmp (dev->name, "portaudio#", 10)) { // Integer index follows "#"
dev->portaudio_index = i = atoi(dev->name + 10);
pInfo = Pa_GetDeviceInfo(i);
if (pInfo) {
snprintf (dev->msg1, QUISK_SC_SIZE, "PortAudio device %s", pInfo->name);
return 0;
}
else {
snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE,
"Can not find portaudio device number %s", dev->name + 10);
}
return 1;
}
if ( ! strncmp (dev->name, "portaudio:", 10)) {
dev->portaudio_index = -1;
count = Pa_GetDeviceCount(); // Search for string in device name
for (i = 0; i < count; i++) {
pInfo = Pa_GetDeviceInfo(i);
if (pInfo && strstr(pInfo->name, dev->name + 10)) {
dev->portaudio_index = i;
snprintf (dev->msg1, QUISK_SC_SIZE, "PortAudio device %s", pInfo->name);
break;
}
}
if (dev->portaudio_index == -1) { // Error
snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE,
"Can not find portaudio device named %s", dev->name + 10);
return 1;
}
return 0;
}
snprintf (quisk_sound_state.err_msg, QUISK_SC_SIZE,
"Did not recognize portaudio device %.90s", dev->name);
return 1;
}
static int quisk_open_portaudio (struct sound_dev * cDev, struct sound_dev * pDev)
{ // Open the portaudio soundcard for capture on cDev and playback on pDev (or NULL).
// Return non-zero for error.
PaStreamParameters cParams, pParams;
PaError error;
PaStream * hndl;
info_portaudio (cDev, pDev);
if (pDev && cDev && pDev->sample_rate != cDev->sample_rate) {
strncpy(quisk_sound_state.err_msg, "Capture and Play sample rates must be equal.", QUISK_SC_SIZE);
return 1;
}
cParams.sampleFormat = paFloat32;
pParams.sampleFormat = paFloat32;
cParams.hostApiSpecificStreamInfo = NULL;
pParams.hostApiSpecificStreamInfo = NULL;
if (cDev) {
cDev->handle = NULL;
cParams.device = cDev->portaudio_index;
cParams.channelCount = cDev->num_channels;
cParams.suggestedLatency = cDev->portaudio_latency;
}
if (pDev) {
pDev->handle = NULL;
pParams.device = pDev->portaudio_index;
pParams.channelCount = pDev->num_channels;
pParams.suggestedLatency = pDev->portaudio_latency;
}
if (cDev && pDev) {
error = Pa_OpenStream (&hndl, &cParams, &pParams,
(double)cDev->sample_rate, cDev->read_frames, 0, NULL, NULL);
pDev->handle = cDev->handle = (void *)hndl;
}
else if (cDev) {
error = Pa_OpenStream (&hndl, &cParams, NULL,
(double)cDev->sample_rate, cDev->read_frames, 0, NULL, NULL);
cDev->handle = (void *)hndl;
}
else if (pDev) {
error = Pa_OpenStream (&hndl, NULL, &pParams,
(double)pDev->sample_rate, 0, 0, NULL, NULL);
pDev->handle = (void *)hndl;
}
else {
error = paNoError;
}
if (pDev) {
pDev->play_buf_size = Pa_GetStreamWriteAvailable(pDev->handle);
if (pDev->latency_frames > pDev->play_buf_size) {
#if DEBUG_IO
printf("Latency frames %d limited to buffer size %d\n",
pDev->latency_frames, pDev->play_buf_size);
#endif
pDev->latency_frames = pDev->play_buf_size;
}
}
#if DEBUG_IO
if (pDev) {
printf ("play_buf_size %d\n", pDev->play_buf_size);
printf ("latency_frames %d\n", pDev->latency_frames);
}
#endif
if (error == paNoError)
return 0;
strncpy(quisk_sound_state.err_msg, Pa_GetErrorText(error), QUISK_SC_SIZE);
return 1;
}
void quisk_start_sound_portaudio(struct sound_dev ** pCapture, struct sound_dev ** pPlayback)
{
int index, err, match;
struct sound_dev ** pCapt, ** pPlay;
Pa_Initialize();
// Set the portaudio index from the name. Return on error.
pCapt = pCapture;
while (*pCapt) {
if( (*pCapt)->driver == DEV_DRIVER_PORTAUDIO && quisk_pa_name2index (*pCapt, 1))
return; // Error
pCapt++;
}
pPlay = pPlayback;
while (*pPlay) {
if( (*pPlay)->driver == DEV_DRIVER_PORTAUDIO && quisk_pa_name2index (*pPlay, 0))
return;
pPlay++;
}
// Open the sound cards. If a capture device equals a playback device, they must be opened jointly.
pCapt = pCapture;
while (*pCapt) {
index = (*pCapt)->portaudio_index;
if((*pCapt)->driver == DEV_DRIVER_PORTAUDIO && index >= 0) { // This is a portaudio device
pPlay = pPlayback;
match = 0;
while (*pPlay) {
if((*pPlay)->driver == DEV_DRIVER_PORTAUDIO &&
(*pPlay)->portaudio_index == index) { // same device, open both
err = quisk_open_portaudio (*pCapt, *pPlay);
match = 1;
break;
}
pPlay++;
}
if ( ! match)
err = quisk_open_portaudio (*pCapt, NULL); // no matching device
if (err)
return; // error
}
pCapt++;
}
strncpy (quisk_sound_state.msg1, (*pCapture)->msg1, QUISK_SC_SIZE); // Primary capture device
// Open remaining portaudio devices
pPlay = pPlayback;
while (*pPlay) {
if ((*pPlay)->driver == DEV_DRIVER_PORTAUDIO
&& (*pPlay)->portaudio_index >= 0
&& ! (*pPlay)->handle
) {
err = quisk_open_portaudio (NULL, *pPlay);
if (err)
return; // error
}
pPlay++;
}
if ( ! quisk_sound_state.msg1[0]) // Primary playback device
strncpy (quisk_sound_state.msg1, (*pPlayback)->msg1, QUISK_SC_SIZE);
pCapt = pCapture;
while (*pCapt) {
if ((*pCapt)->handle)
Pa_StartStream((PaStream * )(*pCapt)->handle);
pCapt++;
}
pPlay = pPlayback;
while (*pPlay) {
if ((*pPlay)->handle && Pa_IsStreamStopped((PaStream * )(*pPlay)->handle))
Pa_StartStream((PaStream * )(*pPlay)->handle);
pPlay++;
}
}
void quisk_close_sound_portaudio(void)
{
Pa_Terminate();
}
// Changes for MacOS support (__MACH__) thanks to Mario, DL3LSM.
#if defined(__MACH__)
static int device_list(PyObject * py, int input)
{
int retNum = 0;
char buf100[100];
PaError err;
err = Pa_Initialize();
if ( err == paNoError ) {
PaDeviceIndex numDev = Pa_GetDeviceCount();
for (PaDeviceIndex dev = 0; dev < numDev; dev++) {
const PaDeviceInfo *info = Pa_GetDeviceInfo(dev);
#if (0)
printf("found audio device: %d, name=%s, #inp %d, #outp %d defsample %f\n", dev, info->name, info->maxInputChannels,
info->maxOutputChannels, info->defaultSampleRate);
#endif
if ((input && info->maxInputChannels > 0) ||
(!input && info->maxOutputChannels > 0)) { // check the available channels for the type)
// found one
if (py) {
snprintf(buf100, 100, "%s", info->name);
PyList_Append(py, PyString_FromString(buf100));
}
}
}
Pa_Terminate();
}
return retNum;
}
PyObject * quisk_sound_devices(PyObject * self, PyObject * args)
{ // Return a list of ALSA device names [pycapt, pyplay]
PyObject * pylist, * pycapt, * pyplay;
if (!PyArg_ParseTuple (args, ""))
return NULL;
// Each pycapt and pyplay is [pydev, pyname]
pylist = PyList_New(0); // list [pycapt, pyplay]
pycapt = PyList_New(0); // list of capture devices
pyplay = PyList_New(0); // list of play devices
PyList_Append(pylist, pycapt);
PyList_Append(pylist, pyplay);
device_list(pycapt, 1);
device_list(pyplay, 0);
return pylist;
}
void quisk_play_alsa(struct sound_dev * dev, int nSamples,
complex double * cSamples, int report_latency, double volume)
{
}
void quisk_start_sound_alsa(struct sound_dev ** pCapture, struct sound_dev ** pPlayback)
{
}
void quisk_close_sound_alsa(struct sound_dev ** pCapture, struct sound_dev ** pPlayback)
{
}
int quisk_read_alsa(struct sound_dev * dev, complex double * samp)
{
return 0;
}
void quisk_mixer_set(char * card_name, int numid, PyObject * value, char * err_msg, int err_size)
{
err_msg[0] = 0;
}
#if !defined USE_MACPORTS
int quisk_read_pulseaudio(struct sound_dev * dev, complex double * samp)
{
return 0;
}
void quisk_play_pulseaudio(struct sound_dev * dev, int j, complex double * samp, int i, double volume)
{
}
void quisk_start_sound_pulseaudio(struct sound_dev ** pCapture, struct sound_dev ** pPlayback)
{
}
void quisk_close_sound_pulseaudio()
{
}
PyObject * quisk_pa_sound_devices(PyObject * self, PyObject * args)
{ // Return a list of PulseAudio device names [pycapt, pyplay]
PyObject * pylist, * pycapt, * pyplay;
if (!PyArg_ParseTuple (args, ""))
return NULL;
pylist = PyList_New(0); // list [pycapt, pyplay]
pycapt = PyList_New(0); // list of capture devices
pyplay = PyList_New(0); // list of play devices
PyList_Append(pylist, pycapt);
PyList_Append(pylist, pyplay);
return pylist;
}
#endif
#endif