1008 lines
33 KiB
C
1008 lines
33 KiB
C
|
/*
|
||
|
* sound_pulseaudio.c is part of Quisk, and is Copyright the following
|
||
|
* authors:
|
||
|
*
|
||
|
* Philip G. Lee <rocketman768@gmail.com>, 2014
|
||
|
* Jim Ahlstrom, N2ADR, October, 2014
|
||
|
* Eric Thornton, KM4DSJ, September, 2015
|
||
|
*
|
||
|
* This code replaces the pulseaudio-simple version by Philip G. Lee. It
|
||
|
* uses the asynchronous pulseaudio API. It was written by Eric Thornton, 2015.
|
||
|
*
|
||
|
* Quisk 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 3 of the License, or
|
||
|
* (at your option) any later version.
|
||
|
|
||
|
* Quisk 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.
|
||
|
|
||
|
* You should have received a copy of the GNU General Public License
|
||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
|
||
|
/* This pulseaudio interface was rewritten to utilize the features in the
|
||
|
* asynchronous API and threaded mainloop.
|
||
|
* Eric Thornton, KM4DSJ 2015
|
||
|
*/
|
||
|
/*
|
||
|
* 2017 Nov by N2ADR Remove most exit(1) statements.
|
||
|
* 2017 Nov by N2ADR Add code to enable PulseAudio to continue with mis-spelled device names.
|
||
|
*/
|
||
|
|
||
|
#include <Python.h>
|
||
|
#include <stdio.h>
|
||
|
#include <string.h>
|
||
|
#include <complex.h>
|
||
|
#include <assert.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <unistd.h>
|
||
|
#include "quisk.h"
|
||
|
#include <pulse/pulseaudio.h>
|
||
|
|
||
|
// From pulsecore/macro.h
|
||
|
#define pa_memzero(x,l) (memset((x), 0, (l)))
|
||
|
#define pa_zero(x) (pa_memzero(&(x), sizeof(x)))
|
||
|
|
||
|
// Current sound status
|
||
|
extern struct sound_conf quisk_sound_state;
|
||
|
|
||
|
//pointers for aychronous threaded loop
|
||
|
static pa_threaded_mainloop *pa_ml;
|
||
|
static pa_mainloop_api *pa_mlapi;
|
||
|
static pa_context *pa_ctx; //local context
|
||
|
static pa_context *pa_IQ_ctx; //remote context for IQ audio
|
||
|
volatile int streams_ready = 0; //This is ++/-- by the mainloop thread
|
||
|
volatile int streams_to_start;
|
||
|
|
||
|
// remember all open devices for easy cleanup on exit
|
||
|
static pa_stream *OpenPulseDevices[PA_LIST_SIZE * 2] = {NULL};
|
||
|
|
||
|
static int have_QuiskDigitalInput; // Do we need to create this null sink?
|
||
|
static int have_QuiskDigitalOutput; // Do we need to create this null sink?
|
||
|
|
||
|
/* This callback happens any time a stream changes state. Here, it's primary used to
|
||
|
* tell the quisk thread when streams are ready.
|
||
|
*/
|
||
|
|
||
|
void stream_state_callback(pa_stream *s, void *userdata) {
|
||
|
struct sound_dev *dev = userdata;
|
||
|
assert(s);
|
||
|
assert(dev);
|
||
|
|
||
|
dev->pulse_stream_state = pa_stream_get_state(s);
|
||
|
switch (dev->pulse_stream_state) {
|
||
|
case PA_STREAM_CREATING:
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("\n**Stream %s state Creating\n", dev->name);
|
||
|
break;
|
||
|
|
||
|
case PA_STREAM_TERMINATED:
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("\n**Stream %s state Terminated\n", dev->name);
|
||
|
streams_ready--;
|
||
|
break;
|
||
|
|
||
|
case PA_STREAM_READY:
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("\n**Stream %s state Ready\n", dev->name);
|
||
|
streams_ready++; //increment counter to tell other thread that this stream is ready
|
||
|
streams_to_start++;
|
||
|
if (quisk_sound_state.verbose_pulse) {
|
||
|
const pa_buffer_attr *a;
|
||
|
printf("Connected to device %s (%u, %ssuspended). ",
|
||
|
pa_stream_get_device_name(s), pa_stream_get_device_index(s),
|
||
|
pa_stream_is_suspended(s) ? "" : "not ");
|
||
|
|
||
|
if (!(a = pa_stream_get_buffer_attr(s)))
|
||
|
printf("pa_stream_get_buffer_attr() failed: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
else if (!(a->prebuf)) {
|
||
|
printf("Buffer metrics %s: maxlength=%u, fragsize=%u\n", dev->name, a->maxlength, a->fragsize);
|
||
|
}
|
||
|
else {
|
||
|
printf("Buffer metrics %s: maxlength=%u, prebuf=%u, tlength=%u minreq=%u\n",
|
||
|
dev->name, a->maxlength, a->prebuf, a->tlength, a->minreq);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case PA_STREAM_FAILED:
|
||
|
default:
|
||
|
snprintf(quisk_sound_state.err_msg, QUISK_SC_SIZE,
|
||
|
"Stream error: %.40s - %.40s", dev->name, pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("\n**Stream %s state Failed\n", dev->name);
|
||
|
printf("%s\n", quisk_sound_state.err_msg);
|
||
|
streams_to_start++;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
//Indicates underflow on passed stream (playback)
|
||
|
static void stream_underflow_callback(pa_stream *s, void *userdata) {
|
||
|
struct sound_dev *dev = userdata;
|
||
|
assert(s);
|
||
|
assert(dev);
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Stream underrun %s\n", dev->name);
|
||
|
dev->dev_error++;
|
||
|
}
|
||
|
|
||
|
//Indicates overflow on passed stream (record)
|
||
|
static void stream_overflow_callback(pa_stream *s, void *userdata) {
|
||
|
struct sound_dev *dev = userdata;
|
||
|
assert(s);
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Stream overrun %s\n", dev->name);
|
||
|
dev->dev_error++;
|
||
|
}
|
||
|
|
||
|
//Indicates stream has started
|
||
|
static void stream_started_callback(pa_stream *s, void *userdata) {
|
||
|
struct sound_dev *dev = userdata;
|
||
|
assert(s);
|
||
|
assert(dev);
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Stream started %s\n", dev->name);
|
||
|
}
|
||
|
|
||
|
//Called when cork/uncork has succeeded on passed stream. Signals mainloop when complete.
|
||
|
static void stream_corked_callback(pa_stream *s, int success, void *userdata) {
|
||
|
assert(s);
|
||
|
struct sound_dev *dev = userdata;
|
||
|
|
||
|
if (s) {
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Stream cork/uncork %s success\n", dev->name);
|
||
|
pa_threaded_mainloop_signal(pa_ml, 0);
|
||
|
}
|
||
|
else {
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Stream cork/uncork %s Failure!\n", dev->name);
|
||
|
exit(11);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Called when stream flush has completed.
|
||
|
static void stream_flushed_callback(pa_stream *s, int success, void *userdata) {
|
||
|
assert(s);
|
||
|
struct sound_dev *dev = userdata;
|
||
|
|
||
|
if (s) {
|
||
|
printf("Stream flush %s success\n", dev->name);
|
||
|
pa_threaded_mainloop_signal(pa_ml, 0);
|
||
|
}
|
||
|
else {
|
||
|
printf("Stream flush %s Failure!\n", dev->name);
|
||
|
exit(12);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// This is called by the play function when the timing structure is updated.
|
||
|
static void stream_timing_callback(pa_stream *s, int success, void *userdata) {
|
||
|
struct sound_dev *dev = userdata;
|
||
|
pa_usec_t l;
|
||
|
int negative = 0;
|
||
|
assert(s);
|
||
|
|
||
|
if (!success || pa_stream_get_latency(s, &l, &negative) < 0) {
|
||
|
printf("pa_stream_get_latency() failed: %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
dev->dev_latency = (int)l;
|
||
|
if (negative)
|
||
|
dev->dev_latency *= -1;
|
||
|
pa_threaded_mainloop_signal(pa_ml, 0);
|
||
|
}
|
||
|
|
||
|
|
||
|
/* This callback allows us to read in the server side stream information so that we
|
||
|
* can match stream formats and sizes to what is configured in pulseaudio.
|
||
|
*/
|
||
|
|
||
|
static void server_info_cb(pa_context *c, const pa_server_info *info, void *userdata) {
|
||
|
struct sound_dev **pDevices = userdata;
|
||
|
pa_buffer_attr rec_ba;
|
||
|
pa_buffer_attr play_ba;
|
||
|
pa_sample_spec ss;
|
||
|
pa_sample_spec default_ss;
|
||
|
pa_stream_flags_t pb_flags = PA_STREAM_NOFLAGS;
|
||
|
pa_stream_flags_t rec_flags = PA_STREAM_ADJUST_LATENCY;
|
||
|
default_ss = info->sample_spec;
|
||
|
|
||
|
printf("Connected to %s \n", info->host_name);
|
||
|
|
||
|
while(*pDevices) {
|
||
|
struct sound_dev *dev = *pDevices++;
|
||
|
const char *dev_name;
|
||
|
pa_stream *s;
|
||
|
|
||
|
pa_zero(rec_ba);
|
||
|
pa_zero(play_ba);
|
||
|
|
||
|
if (dev->name[5] == ':')
|
||
|
dev_name = dev->name + 6; // the device name is given; "pulse:alsa_input.pci-0000_00_1b.0.analog-stereo"
|
||
|
else
|
||
|
dev_name = NULL; // the device name is "pulse" for the default device
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Opening Device %s\n", dev_name);
|
||
|
|
||
|
//Construct sample specification. Use S16LE if availiable. Default to Float32 for others.
|
||
|
//If the source/sink is not Float32, Pulseaudio will convert it (uses CPU)
|
||
|
//dev->sample_bytes = (int)pa_frame_size(&ss) / ss.channels;
|
||
|
if (default_ss.format == PA_SAMPLE_S16LE) {
|
||
|
dev->sample_bytes = 2;
|
||
|
ss.format = default_ss.format;
|
||
|
}
|
||
|
else {
|
||
|
dev->sample_bytes = 4;
|
||
|
ss.format = PA_SAMPLE_FLOAT32LE;
|
||
|
}
|
||
|
|
||
|
ss.rate = dev->sample_rate;
|
||
|
ss.channels = dev->num_channels;
|
||
|
|
||
|
rec_ba.maxlength = (uint32_t) -1;
|
||
|
rec_ba.fragsize = (uint32_t) SAMP_BUFFER_SIZE / 16; //higher numbers eat cpu on reading monitor streams.
|
||
|
|
||
|
play_ba.maxlength = (uint32_t) -1;
|
||
|
play_ba.prebuf = (uint32_t) (dev->sample_bytes * ss.channels * dev->latency_frames);
|
||
|
//play_ba.tlength = (uint32_t) -1;
|
||
|
play_ba.tlength = play_ba.prebuf;
|
||
|
|
||
|
if (dev->latency_frames == 0)
|
||
|
play_ba.minreq = (uint32_t) 0; //verify this is sane
|
||
|
else
|
||
|
play_ba.minreq = (uint32_t) -1;
|
||
|
|
||
|
if (dev->stream_dir_record) {
|
||
|
|
||
|
if (!(s = pa_stream_new(c, dev->stream_description, &ss, NULL))) {
|
||
|
printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));
|
||
|
continue;
|
||
|
}
|
||
|
if (pa_stream_connect_record(s, dev_name, &rec_ba, rec_flags) < 0) {
|
||
|
printf("pa_stream_connect_record() failed: %s", pa_strerror(pa_context_errno(c)));
|
||
|
continue;
|
||
|
}
|
||
|
pa_stream_set_overflow_callback(s, stream_overflow_callback, dev);
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
pa_cvolume cv;
|
||
|
pa_volume_t volume = PA_VOLUME_NORM;
|
||
|
|
||
|
if (!(s = pa_stream_new(c, dev->stream_description, &ss, NULL))) {
|
||
|
printf("pa_stream_new() failed: %s", pa_strerror(pa_context_errno(c)));
|
||
|
continue;
|
||
|
}
|
||
|
if (pa_stream_connect_playback(s, dev_name, &play_ba, pb_flags, pa_cvolume_set(&cv, ss.channels, volume), NULL) < 0) {
|
||
|
printf("pa_stream_connect_playback() failed: %s", pa_strerror(pa_context_errno(c)));
|
||
|
continue;
|
||
|
}
|
||
|
pa_stream_set_underflow_callback(s, stream_underflow_callback, dev);
|
||
|
}
|
||
|
|
||
|
|
||
|
pa_stream_set_state_callback(s, stream_state_callback, dev);
|
||
|
pa_stream_set_started_callback(s, stream_started_callback, dev);
|
||
|
|
||
|
dev->handle = (void*)s; //save memory address for stream in handle
|
||
|
|
||
|
int i;
|
||
|
for(i=0;i < PA_LIST_SIZE;i++) { //save address for stream for easy exit
|
||
|
if (!(OpenPulseDevices[i])) {
|
||
|
OpenPulseDevices[i] = dev->handle;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
//Context state callback. Basically here to pass initialization to server_info_cb
|
||
|
void state_cb(pa_context *c, void *userdata) {
|
||
|
pa_context_state_t state;
|
||
|
state = pa_context_get_state(c);
|
||
|
switch (state) {
|
||
|
// There are just here for reference
|
||
|
case PA_CONTEXT_UNCONNECTED:
|
||
|
case PA_CONTEXT_CONNECTING:
|
||
|
case PA_CONTEXT_AUTHORIZING:
|
||
|
case PA_CONTEXT_SETTING_NAME:
|
||
|
default:
|
||
|
break;
|
||
|
case PA_CONTEXT_FAILED:
|
||
|
case PA_CONTEXT_TERMINATED:
|
||
|
printf("Context Terminated\n");
|
||
|
break;
|
||
|
case PA_CONTEXT_READY: {
|
||
|
pa_operation *o;
|
||
|
if (!(o = pa_context_get_server_info(c, server_info_cb, userdata)))
|
||
|
printf("pa_context_get_server_info() failed: %s", pa_strerror(pa_context_errno(c)));
|
||
|
else
|
||
|
pa_operation_unref(o);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
#if 0
|
||
|
/* Stream draining complete */
|
||
|
static void stream_drain_complete(pa_stream*s, int success, void *userdata) {
|
||
|
struct sound_dev *dev = userdata;
|
||
|
|
||
|
if (!success) {
|
||
|
printf("Failed to drain stream: %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
quit(1);
|
||
|
}
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Playback stream %s drained.\n", dev->name);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*drain stream function*/
|
||
|
void quisk_drain_cork_stream(struct sound_dev *dev) {
|
||
|
pa_stream *s = dev->handle;
|
||
|
pa_operation *o;
|
||
|
|
||
|
if (!(o = pa_stream_drain(s, stream_drain_complete, NULL))) {
|
||
|
printf("pa_stream_drain(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
exit(1);
|
||
|
}
|
||
|
|
||
|
pa_operation_unref(o);
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|
||
|
//Cork function. Holds mainloop lock until operation is completed.
|
||
|
void quisk_cork_pulseaudio(struct sound_dev *dev, int b) {
|
||
|
pa_stream *s = dev->handle;
|
||
|
pa_operation *o;
|
||
|
|
||
|
pa_threaded_mainloop_lock(pa_ml);
|
||
|
|
||
|
if (!(o = pa_stream_cork(s, b, stream_corked_callback, dev))) {
|
||
|
printf("pa_stream_cork(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
exit(13);
|
||
|
}
|
||
|
else {
|
||
|
while(pa_operation_get_state(o) == PA_OPERATION_RUNNING)
|
||
|
pa_threaded_mainloop_wait(pa_ml);
|
||
|
|
||
|
pa_operation_unref(o);
|
||
|
}
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
|
||
|
if (b)
|
||
|
dev->cork_status = 1;
|
||
|
else
|
||
|
dev->cork_status = 0;
|
||
|
}
|
||
|
|
||
|
//Flush function. Holds mainloop lock until operation is completed.
|
||
|
void quisk_flush_pulseaudio(struct sound_dev *dev) {
|
||
|
pa_stream *s = dev->handle;
|
||
|
pa_operation *o;
|
||
|
|
||
|
pa_threaded_mainloop_lock(pa_ml);
|
||
|
|
||
|
if (!(o = pa_stream_flush(s, stream_flushed_callback, dev))) {
|
||
|
printf("pa_stream_flush(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
exit(14);
|
||
|
}
|
||
|
else {
|
||
|
while(pa_operation_get_state(o) == PA_OPERATION_RUNNING)
|
||
|
pa_threaded_mainloop_wait(pa_ml);
|
||
|
pa_operation_unref(o);
|
||
|
}
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
}
|
||
|
|
||
|
|
||
|
static void WaitForPoll(void) { // Implement a blocking read
|
||
|
static double time0 = 0; // start time in seconds
|
||
|
double timer; // time remaining from last poll usec
|
||
|
|
||
|
timer = quisk_sound_state.data_poll_usec - (QuiskTimeSec() - time0) * 1e6;
|
||
|
|
||
|
if (timer > 1000.0) // see if enough time has elapsed
|
||
|
QuiskSleepMicrosec((int)timer); // wait for the remainder of the poll interval
|
||
|
|
||
|
time0 = QuiskTimeSec(); // reset starting time value
|
||
|
}
|
||
|
|
||
|
|
||
|
/* Read samples from the PulseAudio device.
|
||
|
* Samples are converted to complex form based upon format, counted
|
||
|
* and returned via cSamples pointer.
|
||
|
* Returns the number of samples placed into cSamples
|
||
|
*/
|
||
|
int quisk_read_pulseaudio(struct sound_dev *dev, complex double *cSamples)
|
||
|
{ // cSamples can be NULL to discard samples
|
||
|
int i, nSamples;
|
||
|
int read_frames; // A frame is a sample from each channel
|
||
|
const void * fbuffer;
|
||
|
pa_stream *s = dev->handle;
|
||
|
size_t read_bytes;
|
||
|
|
||
|
if (!dev || dev->pulse_stream_state != PA_STREAM_READY)
|
||
|
return 0;
|
||
|
|
||
|
if (dev->cork_status) {
|
||
|
if (dev->read_frames != 0) {
|
||
|
WaitForPoll();
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// Note: Access to PulseAudio data from our sound thread requires locking the threaded mainloop.
|
||
|
if (dev->read_frames == 0) { // non-blocking: read available frames
|
||
|
pa_threaded_mainloop_lock(pa_ml);
|
||
|
read_frames = pa_stream_readable_size(s) / dev->num_channels / dev->sample_bytes;
|
||
|
|
||
|
if (read_frames == 0) {
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
return 0;
|
||
|
}
|
||
|
dev->dev_latency = read_frames * dev->num_channels * 1000 / (dev->sample_rate / 1000);
|
||
|
|
||
|
}
|
||
|
|
||
|
else { // Blocking read for dev->read_frames total frames
|
||
|
WaitForPoll();
|
||
|
pa_threaded_mainloop_lock(pa_ml);
|
||
|
read_frames = pa_stream_readable_size(s) / dev->num_channels / dev->sample_bytes;
|
||
|
|
||
|
if (read_frames == 0) {
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
return 0;
|
||
|
}
|
||
|
dev->dev_latency = read_frames * dev->num_channels * 1000 / (dev->sample_rate / 1000);
|
||
|
}
|
||
|
|
||
|
|
||
|
nSamples = 0;
|
||
|
|
||
|
while (nSamples < read_frames) { // read samples in chunks until we have enough samples
|
||
|
if (pa_stream_peek (s, &fbuffer, &read_bytes) < 0) {
|
||
|
printf("Failure of pa_stream_peek in quisk_read_pulseaudio\n");
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
return nSamples;
|
||
|
}
|
||
|
|
||
|
if (fbuffer == NULL && read_bytes == 0) { // buffer is empty
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (fbuffer == NULL) { // there is a hole in the buffer
|
||
|
pa_stream_drop(s);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (nSamples * dev->num_channels * dev->sample_bytes + read_bytes >= SAMP_BUFFER_SIZE * 8 / 10) {
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("buffer full on %s\n", dev->name);
|
||
|
pa_stream_drop(s); // limit read request to buffer size
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Convert sampled data to complex data. dev->num_channels must be 2.
|
||
|
if (dev->sample_bytes == 4) { //float32
|
||
|
float fi, fq;
|
||
|
|
||
|
for( i = 0; i < read_bytes; i += (dev->num_channels * 4)) {
|
||
|
memcpy(&fi, fbuffer + i + (dev->channel_I * 4), 4);
|
||
|
memcpy(&fq, fbuffer + i + (dev->channel_Q * 4), 4);
|
||
|
if (fi >= 1.0 || fi <= -1.0)
|
||
|
dev->overrange++;
|
||
|
if (fq >= 1.0 || fq <= -1.0)
|
||
|
dev->overrange++;
|
||
|
if (cSamples)
|
||
|
cSamples[nSamples] = (fi + I * fq) * CLIP32;
|
||
|
nSamples++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
else if (dev->sample_bytes == 2) { //16bit integer little-endian
|
||
|
int16_t si, sq;
|
||
|
for( i = 0; i < read_bytes; i += (dev->num_channels * 2)) {
|
||
|
memcpy(&si, fbuffer + i + (dev->channel_I * 2), 2);
|
||
|
memcpy(&sq, fbuffer + i + (dev->channel_Q * 2), 2);
|
||
|
if (si >= CLIP16 || si <= -CLIP16)
|
||
|
dev->overrange++;
|
||
|
if (sq >= CLIP16 || sq <= -CLIP16)
|
||
|
dev->overrange++;
|
||
|
int ii = si << 16;
|
||
|
int qq = sq << 16;
|
||
|
if (cSamples)
|
||
|
cSamples[nSamples] = ii + I * qq;
|
||
|
nSamples++;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
printf("Unknown sample size for %s", dev->name);
|
||
|
}
|
||
|
pa_stream_drop(s);
|
||
|
}
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
return nSamples;
|
||
|
}
|
||
|
|
||
|
/*!
|
||
|
* \Write outgoing samples directly to pulseaudio server.
|
||
|
* \param playdev Input. Device to which to play the samples.
|
||
|
* \param nSamples Input. Number of samples to play.
|
||
|
* \param cSamples Input. Sample buffer to play from.
|
||
|
* \param report_latency Input. 1 to update \c quisk_sound_state.latencyPlay, 0 otherwise.
|
||
|
* \param volume Input. Ratio in [0,1] by which to scale the played samples.
|
||
|
*/
|
||
|
void quisk_play_pulseaudio(struct sound_dev *dev, int nSamples, complex double *cSamples,
|
||
|
int report_latency, double volume) {
|
||
|
pa_stream *s = dev->handle;
|
||
|
int i=0, n=0;
|
||
|
void *fbuffer;
|
||
|
int fbuffer_bytes = 0;
|
||
|
|
||
|
if( !dev || nSamples <= 0 || dev->pulse_stream_state != PA_STREAM_READY)
|
||
|
return;
|
||
|
|
||
|
if (dev->cork_status)
|
||
|
return;
|
||
|
|
||
|
if (report_latency) { // Report the latency, if requested.
|
||
|
pa_operation *o;
|
||
|
|
||
|
pa_threaded_mainloop_lock(pa_ml);
|
||
|
|
||
|
if (!(o = pa_stream_update_timing_info(s, stream_timing_callback, dev))) {
|
||
|
printf("pa_stream_update_timing(): %s\n", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
|
||
|
}
|
||
|
else {
|
||
|
while (pa_operation_get_state(o) == PA_OPERATION_RUNNING)
|
||
|
pa_threaded_mainloop_wait(pa_ml);
|
||
|
pa_operation_unref(o);
|
||
|
}
|
||
|
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
fbuffer = pa_xmalloc(nSamples * dev->num_channels * dev->sample_bytes);
|
||
|
|
||
|
// Convert from complex data to framebuffer
|
||
|
|
||
|
if (dev->sample_bytes == 4) {
|
||
|
float fi=0.f, fq=0.f;
|
||
|
for(i = 0, n = 0; n < nSamples; i += (dev->num_channels * 4), ++n) {
|
||
|
fi = (volume * creal(cSamples[n])) / CLIP32;
|
||
|
fq = (volume * cimag(cSamples[n])) / CLIP32;
|
||
|
memcpy(fbuffer + i + (dev->channel_I * 4), &fi, 4);
|
||
|
memcpy(fbuffer + i + (dev->channel_Q * 4), &fq, 4);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
else if (dev->sample_bytes == 2) {
|
||
|
int ii, qq;
|
||
|
for(i = 0, n = 0; n < nSamples; i += (dev->num_channels * 2), ++n) {
|
||
|
ii = (int)(volume * creal(cSamples[n]) / 65536);
|
||
|
qq = (int)(volume * cimag(cSamples[n]) / 65536);
|
||
|
memcpy(fbuffer + i + (dev->channel_I * 2), &ii, 2);
|
||
|
memcpy(fbuffer + i + (dev->channel_Q * 2), &qq, 2);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
else {
|
||
|
printf("Unknown sample size for %s", dev->name);
|
||
|
exit(15);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
fbuffer_bytes = nSamples * dev->num_channels * dev->sample_bytes;
|
||
|
pa_threaded_mainloop_lock(pa_ml);
|
||
|
size_t writable = pa_stream_writable_size(s);
|
||
|
|
||
|
|
||
|
if (writable > 0) {
|
||
|
if ( writable > 1024*1000 ) //sanity check to prevent pa_xmalloc from crashing on monitor streams
|
||
|
writable = 1024*1000;
|
||
|
if (fbuffer_bytes > writable) {
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Truncating write by %u bytes\n", fbuffer_bytes - (int)writable);
|
||
|
fbuffer_bytes = writable;
|
||
|
}
|
||
|
pa_stream_write(dev->handle, fbuffer, (size_t)fbuffer_bytes, NULL, 0, PA_SEEK_RELATIVE);
|
||
|
//printf("wrote %d to %s\n", writable, dev->name);
|
||
|
}
|
||
|
else {
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Can't write to stream %s. Dropping %d bytes\n", dev->name, fbuffer_bytes);
|
||
|
}
|
||
|
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
pa_xfree(fbuffer);
|
||
|
fbuffer=NULL;
|
||
|
}
|
||
|
|
||
|
|
||
|
//This is a function to sort the device list into local and remote lists.
|
||
|
void sort_devices(struct sound_dev **plist, struct sound_dev **pLocal, struct sound_dev **pRemote) {
|
||
|
|
||
|
while(*plist) {
|
||
|
struct sound_dev *dev = *plist++;
|
||
|
|
||
|
dev->pulse_stream_state = PA_STREAM_UNCONNECTED;
|
||
|
// Not a PulseAudio device
|
||
|
if( dev->driver != DEV_DRIVER_PULSEAUDIO )
|
||
|
continue;
|
||
|
|
||
|
// Device without name: sad.
|
||
|
if( !dev->name[0] )
|
||
|
continue;
|
||
|
|
||
|
// This is a remote device
|
||
|
if(dev->server[0]) {
|
||
|
int i;
|
||
|
for(i=0;i < PA_LIST_SIZE;i++) {
|
||
|
if (!(*(pRemote+i))) {
|
||
|
*(pRemote+i) = dev;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// This is a local device
|
||
|
else {
|
||
|
int i;
|
||
|
for(i=0;i < PA_LIST_SIZE; i++) {
|
||
|
if (!(*(pLocal+i))) {
|
||
|
*(pLocal+i) = dev;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/*!
|
||
|
* \brief Search for and open PulseAudio devices.
|
||
|
*
|
||
|
* \param pCapture Input/Output. Array of capture devices to search through.
|
||
|
* If a device has its \c sound_dev.driver field set to
|
||
|
* \c DEV_DRIVER_PULSEAUDIO, it will be opened for recording.
|
||
|
* \param pPlayback Input/Output. Array of playback devices to search through.
|
||
|
* If a device has its \c sound_dev.driver field set to
|
||
|
* \c DEV_DRIVER_PULSEAUDIO, it will be opened for recording.
|
||
|
*/
|
||
|
|
||
|
//sound_dev ** pointer(address) for list of addresses
|
||
|
//sound_dev * fpointer(address) for list of addresses
|
||
|
|
||
|
void quisk_start_sound_pulseaudio(struct sound_dev **pCapture, struct sound_dev **pPlayback) {
|
||
|
int num_pa_devices = 0;
|
||
|
int i;
|
||
|
//sorted lists of local and remote devices
|
||
|
struct sound_dev *LocalPulseDevices[PA_LIST_SIZE] = {NULL};
|
||
|
struct sound_dev *RemotePulseDevices[PA_LIST_SIZE] = {NULL};
|
||
|
|
||
|
sort_devices(pCapture, LocalPulseDevices, RemotePulseDevices);
|
||
|
sort_devices(pPlayback, LocalPulseDevices, RemotePulseDevices);
|
||
|
pa_IQ_ctx = NULL;
|
||
|
pa_ctx = NULL;
|
||
|
pa_ml = NULL;
|
||
|
pa_mlapi = NULL;
|
||
|
streams_to_start = 0;
|
||
|
//quisk_sound_state.verbose_pulse = 1;
|
||
|
|
||
|
if (!RemotePulseDevices[0] && !LocalPulseDevices[0]) {
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("No pulseaudio devices to open!\n");
|
||
|
return; //nothing to open. No need to start the mainloop.
|
||
|
}
|
||
|
|
||
|
// Create a mainloop API
|
||
|
pa_ml = pa_threaded_mainloop_new();
|
||
|
pa_mlapi = pa_threaded_mainloop_get_api(pa_ml);
|
||
|
|
||
|
assert(pa_signal_init(pa_mlapi) == 0);
|
||
|
|
||
|
if (pa_threaded_mainloop_start(pa_ml) < 0) {
|
||
|
printf("pa_mainloop_run() failed.");
|
||
|
return;
|
||
|
}
|
||
|
else
|
||
|
printf("Pulseaudio threaded mainloop started\n");
|
||
|
|
||
|
pa_threaded_mainloop_lock(pa_ml);
|
||
|
|
||
|
if (RemotePulseDevices[0]) { //we've got at least 1 remote device
|
||
|
pa_IQ_ctx = pa_context_new(pa_mlapi, "Quisk-remote");
|
||
|
if (pa_context_connect(pa_IQ_ctx, quisk_sound_state.IQ_server, 0, NULL) < 0)
|
||
|
printf("Failed to connect to remote Pulseaudio server\n");
|
||
|
pa_context_set_state_callback(pa_IQ_ctx, state_cb, RemotePulseDevices); //send a list of remote devices to open
|
||
|
}
|
||
|
|
||
|
if (LocalPulseDevices[0]) { //we've got at least 1 local device
|
||
|
pa_ctx = pa_context_new(pa_mlapi, "Quisk-local");
|
||
|
if (pa_context_connect(pa_ctx, NULL, 0, NULL) < 0)
|
||
|
printf("Failed to connect to local Pulseaudio server\n");
|
||
|
pa_context_set_state_callback(pa_ctx, state_cb, LocalPulseDevices);
|
||
|
}
|
||
|
|
||
|
|
||
|
pa_threaded_mainloop_unlock(pa_ml);
|
||
|
|
||
|
for(i=0; LocalPulseDevices[i]; i++) {
|
||
|
num_pa_devices++;
|
||
|
}
|
||
|
|
||
|
for(i=0; RemotePulseDevices[i]; i++) {
|
||
|
num_pa_devices++;
|
||
|
}
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Waiting for %d streams to start\n", num_pa_devices);
|
||
|
|
||
|
while (streams_to_start < num_pa_devices); // wait for all the devices to open
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("All streams started\n");
|
||
|
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
// Close all streams/contexts/loops and return
|
||
|
void quisk_close_sound_pulseaudio() {
|
||
|
int i = 0;
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Closing Pulseaudio interfaces \n ");
|
||
|
|
||
|
while (OpenPulseDevices[i]) {
|
||
|
pa_stream_disconnect(OpenPulseDevices[i]);
|
||
|
pa_stream_unref(OpenPulseDevices[i]);
|
||
|
OpenPulseDevices[i] = '\0';
|
||
|
i++;
|
||
|
}
|
||
|
|
||
|
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("Waiting for %d streams to disconnect\n", streams_ready);
|
||
|
|
||
|
while(streams_ready > 0);
|
||
|
|
||
|
if (pa_IQ_ctx) {
|
||
|
pa_context_disconnect(pa_IQ_ctx);
|
||
|
pa_context_unref(pa_IQ_ctx);
|
||
|
pa_IQ_ctx = NULL;
|
||
|
}
|
||
|
|
||
|
if (pa_ctx) {
|
||
|
pa_context_disconnect(pa_ctx);
|
||
|
pa_context_unref(pa_ctx);
|
||
|
pa_ctx = NULL;
|
||
|
}
|
||
|
|
||
|
if (pa_ml) {
|
||
|
pa_threaded_mainloop_stop(pa_ml);
|
||
|
pa_threaded_mainloop_free(pa_ml);
|
||
|
pa_ml = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
// Additional bugs added by N2ADR below this point.
|
||
|
// Code for quisk_pa_sound_devices is based on code by Igor Brezac and Eric Connell, and Jan Newmarch.
|
||
|
// This should only be called when Quisk first starts, but names changed to protect the other mainloop.
|
||
|
|
||
|
// This callback gets called when our context changes state. We really only
|
||
|
// care about when it's ready or if it has failed.
|
||
|
static void pa_names_state_cb(pa_context *c, void *userdata) {
|
||
|
pa_context_state_t ctx_state;
|
||
|
int *main_state = userdata;
|
||
|
|
||
|
ctx_state = pa_context_get_state(c);
|
||
|
switch (ctx_state) {
|
||
|
// There are just here for reference
|
||
|
case PA_CONTEXT_UNCONNECTED:
|
||
|
case PA_CONTEXT_CONNECTING:
|
||
|
case PA_CONTEXT_AUTHORIZING:
|
||
|
case PA_CONTEXT_SETTING_NAME:
|
||
|
default:
|
||
|
break;
|
||
|
case PA_CONTEXT_FAILED:
|
||
|
case PA_CONTEXT_TERMINATED:
|
||
|
*main_state = 9;
|
||
|
break;
|
||
|
case PA_CONTEXT_READY:
|
||
|
*main_state = 1;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void source_sink(const char * name, const char * descr, pa_proplist * props, PyObject * pylist) {
|
||
|
const char * value;
|
||
|
char buf300[300];
|
||
|
PyObject * pytup;
|
||
|
|
||
|
pytup = PyTuple_New(3);
|
||
|
PyList_Append(pylist, pytup);
|
||
|
PyTuple_SET_ITEM(pytup, 0, PyString_FromString(name));
|
||
|
PyTuple_SET_ITEM(pytup, 1, PyString_FromString(descr));
|
||
|
value = pa_proplist_gets(props, "device.api");
|
||
|
|
||
|
if (value && ! strcmp(value, "alsa")) {
|
||
|
snprintf(buf300, 300, "%s %s (hw:%s,%s)", pa_proplist_gets(props, "alsa.card_name"), pa_proplist_gets(props, "alsa.name"),
|
||
|
pa_proplist_gets(props, "alsa.card"), pa_proplist_gets(props, "alsa.device"));
|
||
|
|
||
|
PyTuple_SET_ITEM(pytup, 2, PyString_FromString(buf300));
|
||
|
}
|
||
|
else {
|
||
|
PyTuple_SET_ITEM(pytup, 2, PyString_FromString(""));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
// pa_mainloop will call this function when it's ready to tell us about a sink.
|
||
|
static void pa_sinklist_cb(pa_context *c, const pa_sink_info *l, int eol, void *userdata) {
|
||
|
if (eol > 0) // If eol is set to a positive number, you're at the end of the list
|
||
|
return;
|
||
|
source_sink(l->name, l->description, l->proplist, (PyObject *)userdata);
|
||
|
if ( ! strcmp(l->name, "QuiskDigitalInput"))
|
||
|
have_QuiskDigitalInput = 1;
|
||
|
if ( ! strcmp(l->name, "QuiskDigitalOutput"))
|
||
|
have_QuiskDigitalOutput = 1;
|
||
|
}
|
||
|
|
||
|
static void pa_sourcelist_cb(pa_context *c, const pa_source_info *l, int eol, void *userdata) {
|
||
|
if (eol > 0)
|
||
|
return;
|
||
|
source_sink(l->name, l->description, l->proplist, (PyObject *)userdata);
|
||
|
}
|
||
|
|
||
|
static void index_callback(pa_context *c, uint32_t idx, void *userdata) {
|
||
|
//printf("%u\n", idx);
|
||
|
}
|
||
|
|
||
|
PyObject * quisk_pa_sound_devices(PyObject * self, PyObject * args)
|
||
|
{ // Return a list of PulseAudio device names [pycapt, pyplay]
|
||
|
PyObject * pylist, * pycapt, * pyplay;
|
||
|
pa_mainloop *pa_names_ml;
|
||
|
pa_mainloop_api *pa_names_mlapi;
|
||
|
pa_operation *pa_op=NULL;
|
||
|
pa_context *pa_names_ctx;
|
||
|
int state = 0;
|
||
|
|
||
|
if (!PyArg_ParseTuple (args, ""))
|
||
|
return NULL;
|
||
|
// Each pycapt and pyplay is (dev name, description, alsa name)
|
||
|
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);
|
||
|
|
||
|
//printf("Starting name loop\n");
|
||
|
|
||
|
// Create a mainloop API and connection to the default server
|
||
|
pa_names_ml = pa_mainloop_new();
|
||
|
pa_names_mlapi = pa_mainloop_get_api(pa_names_ml);
|
||
|
pa_names_ctx = pa_context_new(pa_names_mlapi, "DeviceNames");
|
||
|
|
||
|
// This function connects to the pulse server
|
||
|
if (pa_context_connect(pa_names_ctx, NULL, 0, NULL) < 0) {
|
||
|
if (quisk_sound_state.verbose_pulse)
|
||
|
printf("No local daemon to connect to for show_pulse_audio_devices option\n");
|
||
|
return pylist;
|
||
|
}
|
||
|
|
||
|
// This function defines a callback so the server will tell us it's state.
|
||
|
pa_context_set_state_callback(pa_names_ctx, pa_names_state_cb, &state);
|
||
|
|
||
|
// Now we'll enter into an infinite loop until we get the data we receive or if there's an error
|
||
|
while (state < 10) {
|
||
|
switch (state) {
|
||
|
case 0: // We can't do anything until PA is ready
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
break;
|
||
|
case 1:
|
||
|
// This sends an operation to the server. pa_sinklist_info is
|
||
|
// our callback function and a pointer to our devicelist will
|
||
|
// be passed to the callback.
|
||
|
pa_op = pa_context_get_sink_info_list(pa_names_ctx, pa_sinklist_cb, pyplay);
|
||
|
// Update state for next iteration through the loop
|
||
|
state++;
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
break;
|
||
|
case 2:
|
||
|
// Now we wait for our operation to complete. When it's
|
||
|
// complete our pa_output_devicelist is filled out, and we move
|
||
|
// along to the next state
|
||
|
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
|
||
|
pa_operation_unref(pa_op);
|
||
|
// Now we perform another operation to get the source
|
||
|
// (input device) list just like before.
|
||
|
pa_op = pa_context_get_source_info_list(pa_names_ctx, pa_sourcelist_cb, pycapt);
|
||
|
// Update the state so we know what to do next
|
||
|
state++;
|
||
|
}
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
break;
|
||
|
case 3:
|
||
|
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
|
||
|
pa_operation_unref(pa_op);
|
||
|
state = 4;
|
||
|
}
|
||
|
else
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
break;
|
||
|
// The following loads modules for digital input and output if they are not present.
|
||
|
case 4:
|
||
|
if ( ! have_QuiskDigitalInput) {
|
||
|
pa_op = pa_context_load_module(pa_names_ctx, "module-null-sink",
|
||
|
"sink_name=QuiskDigitalInput sink_properties=device.description=QuiskDigitalInput",
|
||
|
index_callback, NULL);
|
||
|
state = 5;
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
}
|
||
|
else
|
||
|
state = 6;
|
||
|
break;
|
||
|
case 5:
|
||
|
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
|
||
|
pa_operation_unref(pa_op);
|
||
|
state = 6;
|
||
|
}
|
||
|
else
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
break;
|
||
|
case 6:
|
||
|
if ( ! have_QuiskDigitalOutput) {
|
||
|
pa_op = pa_context_load_module(pa_names_ctx, "module-null-sink",
|
||
|
"sink_name=QuiskDigitalOutput sink_properties=device.description=QuiskDigitalOutput",
|
||
|
index_callback, NULL);
|
||
|
state = 7;
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
}
|
||
|
else
|
||
|
state = 9;
|
||
|
break;
|
||
|
case 7:
|
||
|
if (pa_operation_get_state(pa_op) == PA_OPERATION_DONE) {
|
||
|
pa_operation_unref(pa_op);
|
||
|
state = 9;
|
||
|
}
|
||
|
else
|
||
|
pa_mainloop_iterate(pa_names_ml, 1, NULL);
|
||
|
break;
|
||
|
case 9: // Now we're done, clean up and disconnect and return
|
||
|
pa_context_disconnect(pa_names_ctx);
|
||
|
pa_context_unref(pa_names_ctx);
|
||
|
pa_mainloop_free(pa_names_ml);
|
||
|
state = 99;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
//printf("Finished with name loop\n");
|
||
|
return pylist;
|
||
|
}
|