688 lines
18 KiB
C++
688 lines
18 KiB
C++
#include <pulse/pulseaudio.h>
|
|
#include <pulse/thread-mainloop.h>
|
|
#include <iostream>
|
|
#include <pulse/error.h>
|
|
#include <pulse/volume.h>
|
|
#include <pulse/stream.h>
|
|
#include <pulse/introspect.h>
|
|
#include <format>
|
|
#include <cstring>
|
|
#include <thread>
|
|
#include <vector>
|
|
#include <iostream>
|
|
#include <cstdio>
|
|
#include <unistd.h>
|
|
#include <cstring>
|
|
#include <cerrno>
|
|
#include <fcntl.h>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
|
|
// https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/simple.c
|
|
// https://habr.com/en/articles/663352/#linux-and-pulseaudio
|
|
|
|
|
|
/// The pa_system structure holds all the PulseAudio structures together
|
|
struct pa_system
|
|
{
|
|
pa_threaded_mainloop* mainloop;
|
|
pa_context* context;
|
|
pa_stream* stream;
|
|
pa_stream_direction_t direction;
|
|
pa_mainloop_api* api;
|
|
|
|
const void *read_data;
|
|
size_t read_index, read_length;
|
|
|
|
int operation_success;
|
|
};
|
|
|
|
static void context_state_cb(pa_context *c, void *userdata)
|
|
{
|
|
std::cout << "context state callback: ";
|
|
|
|
auto *p = static_cast<pa_system *>(userdata);
|
|
switch(pa_context_get_state(c)) {
|
|
case PA_CONTEXT_READY:
|
|
std::cout << "Context Ready" << std::endl;
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
break;
|
|
case PA_CONTEXT_TERMINATED:
|
|
std::cout << "Context Terminated" << std::endl;
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
break;
|
|
case PA_CONTEXT_FAILED:
|
|
std::cout << "Context Failed" << std::endl;
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
break;
|
|
case PA_CONTEXT_UNCONNECTED:
|
|
std::cout << "Context Unconnected" << std::endl;
|
|
break;
|
|
case PA_CONTEXT_CONNECTING:
|
|
std::cout << "Context Connecting" << std::endl;
|
|
break;
|
|
case PA_CONTEXT_AUTHORIZING:
|
|
std::cout << "Context Authorizing" << std::endl;
|
|
break;
|
|
case PA_CONTEXT_SETTING_NAME:
|
|
std::cout << "Context Setting Name" << std::endl;
|
|
break;
|
|
default:
|
|
std::cerr << "The fuck?" << std::endl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void stream_state_cb(pa_stream* s, void* userdata)
|
|
{
|
|
std::cout << "stream state callback: ";
|
|
|
|
auto *p = static_cast<pa_system *>(userdata);
|
|
|
|
auto state = pa_stream_get_state(s);
|
|
switch(state) {
|
|
case PA_STREAM_READY:
|
|
std::cout << "Stream Ready" << std::endl;
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
break;
|
|
case PA_STREAM_FAILED:
|
|
std::cout << "Stream Failed" << std::endl;
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
break;
|
|
case PA_STREAM_TERMINATED:
|
|
std::cout << "Stream Terminated" << std::endl;
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
break;
|
|
case PA_STREAM_UNCONNECTED:
|
|
std::cout << "Stream Unconnected" << std::endl;
|
|
break;
|
|
case PA_STREAM_CREATING:
|
|
std::cout << "Stream Creating" << std::endl;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void stream_request_cb(pa_stream* s, size_t length, void* userdata) {
|
|
std::cout << "Received stream request callback: " << length << std::endl;
|
|
|
|
auto *p = static_cast<pa_system *>(userdata);
|
|
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
}
|
|
|
|
static void stream_latency_update_cb(pa_stream *s, void* userdata) {
|
|
std::cout << "Received stream latency update callback" << std::endl;
|
|
|
|
auto *p = static_cast<pa_system*>(userdata);
|
|
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
}
|
|
|
|
|
|
pa_system* pa_system_new(
|
|
const char* server,
|
|
const char* name,
|
|
pa_stream_direction_t dir,
|
|
const char* dev,
|
|
const char* stream_name,
|
|
const pa_sample_spec *ss,
|
|
const pa_channel_map *map,
|
|
const pa_buffer_attr *attr,
|
|
int *rerror) {
|
|
pa_system *p;
|
|
|
|
p = pa_xnew(pa_system, 1);
|
|
p->direction = dir;
|
|
|
|
//const char *server = NULL;
|
|
//const char *name = "This Dick";
|
|
//p->direction = PA_STREAM_PLAYBACK;
|
|
//const char *dev = NULL;
|
|
//const char *stream_name = "playback";
|
|
//static const pa_sample_spec ss = {
|
|
// .format = PA_SAMPLE_S16LE,
|
|
// .rate = 44100,
|
|
// .channels = 2
|
|
//};
|
|
//const pa_channel_map *map = NULL;
|
|
//const pa_buffer_attr *attr = NULL;
|
|
int error = PA_ERR_INTERNAL;
|
|
int r;
|
|
|
|
if (!(p->mainloop = pa_threaded_mainloop_new()))
|
|
throw std::runtime_error("Failed to create a PulseAudio mainloop");
|
|
// goto fail;
|
|
|
|
p->api = pa_threaded_mainloop_get_api(p->mainloop);
|
|
|
|
// TODO: Research pa_context_new_with_proplist
|
|
if (!(p->context = pa_context_new( p->api, "Sound Channel?")))
|
|
{
|
|
// goto fail;
|
|
throw std::runtime_error("Failed to create a context");
|
|
}
|
|
|
|
pa_context_flags flags = pa_context_flags::PA_CONTEXT_NOAUTOSPAWN;
|
|
|
|
pa_context_set_state_callback(p->context, context_state_cb, p);
|
|
|
|
if (pa_context_connect(p->context, server, flags, NULL) < 0) {
|
|
error = pa_context_errno(p->context);
|
|
// goto fail;
|
|
throw std::runtime_error(std::format("Failed to connect a context: {}", error));
|
|
}
|
|
|
|
|
|
pa_threaded_mainloop_lock(p->mainloop);
|
|
|
|
if (pa_threaded_mainloop_start(p->mainloop) < 0) {
|
|
// goto unlock_and_fail
|
|
throw std::runtime_error("Failed to start threaded mainloop");
|
|
}
|
|
|
|
for (;;) {
|
|
pa_context_state_t state;
|
|
state = pa_context_get_state(p->context);
|
|
|
|
if (state == PA_CONTEXT_READY)
|
|
break;
|
|
|
|
if (!PA_CONTEXT_IS_GOOD(state)) {
|
|
error = pa_context_errno(p->context);
|
|
// goto unlock_and_fail
|
|
throw std::runtime_error(std::format("Context was not good! {}", error));
|
|
}
|
|
|
|
// Wait until the context is ready
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
}
|
|
|
|
if (!(p->stream = pa_stream_new(p->context, stream_name, ss, map))) {
|
|
error = pa_context_errno(p->context);
|
|
// goto unlock_and_fail
|
|
throw std::runtime_error(std::format("Failed to create a stream. Error code {}", error));
|
|
}
|
|
|
|
pa_stream_set_state_callback(p->stream, stream_state_cb, p);
|
|
pa_stream_set_read_callback(p->stream, stream_request_cb, p);
|
|
pa_stream_set_write_callback(p->stream, stream_request_cb, p);
|
|
pa_stream_set_latency_update_callback(p->stream, stream_latency_update_cb, p);
|
|
|
|
pa_stream_flags my_flags = static_cast<pa_stream_flags>(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY |
|
|
PA_STREAM_AUTO_TIMING_UPDATE);
|
|
|
|
if (p->direction == PA_STREAM_PLAYBACK)
|
|
r = pa_stream_connect_playback(p->stream, dev, attr, my_flags, NULL, NULL);
|
|
else
|
|
r = pa_stream_connect_record(p->stream, dev, attr, my_flags);
|
|
|
|
if (r < 0) {
|
|
error = pa_context_errno(p->context);
|
|
// goto unlock_and_fail
|
|
throw std::runtime_error("Failed to connect the stream.");
|
|
}
|
|
|
|
for (;;) {
|
|
pa_stream_state_t state;
|
|
state = pa_stream_get_state(p->stream);
|
|
|
|
if (state == PA_STREAM_READY)
|
|
break;
|
|
|
|
if (!PA_STREAM_IS_GOOD(state)) {
|
|
error = pa_context_errno(p->context);
|
|
// goto unlock_and_fail
|
|
throw std::runtime_error("Stream state is bad!");
|
|
}
|
|
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
|
|
return p;
|
|
|
|
// unlock_and_fail:
|
|
// pa_threaded_mainloop_unlock(p->mainloop);
|
|
|
|
// fail:
|
|
// if (rerror)
|
|
// *rerror = error;
|
|
// pa_simple_free(p)
|
|
// return NULL;
|
|
}
|
|
|
|
void pa_system_free(pa_system* p)
|
|
{
|
|
if (p->mainloop)
|
|
pa_threaded_mainloop_stop(p->mainloop);
|
|
|
|
if (p->stream)
|
|
pa_stream_unref(p->stream);
|
|
|
|
if (p->context)
|
|
{
|
|
pa_context_disconnect(p->context);
|
|
pa_context_unref(p->context);
|
|
}
|
|
|
|
if (p->mainloop)
|
|
pa_threaded_mainloop_free(p->mainloop);
|
|
|
|
pa_xfree(p);
|
|
}
|
|
|
|
|
|
void on_device_sink(pa_context* c, const pa_sink_info* info, int eol, void* userdata)
|
|
{
|
|
if (eol != 0)
|
|
{
|
|
pa_system* p = static_cast<pa_system *>(userdata);
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
}
|
|
|
|
if (info)
|
|
{
|
|
const char* device_id = info->name;
|
|
std::cout << "Device found: " << device_id << std::endl;
|
|
}
|
|
}
|
|
|
|
void on_device_source(pa_context* c, const pa_source_info* info, int eol, void *userdata)
|
|
{
|
|
if (eol != 0)
|
|
{
|
|
pa_system* p = static_cast<pa_system *>(userdata);
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
}
|
|
if (info)
|
|
{
|
|
const char* device_id = info->name;
|
|
std::cout << "Device found: " << device_id << std::endl;
|
|
}
|
|
}
|
|
|
|
int pa_system_enumerate_devices(pa_system* p)
|
|
{
|
|
pa_threaded_mainloop_lock(p->mainloop);
|
|
|
|
pa_operation * op;
|
|
if (p->direction == PA_STREAM_PLAYBACK)
|
|
op = pa_context_get_sink_info_list(p->context, on_device_sink, p);
|
|
else
|
|
op = pa_context_get_source_info_list(p->context, on_device_source, p);
|
|
|
|
for (;;) {
|
|
int r = pa_operation_get_state(op);
|
|
|
|
if (r == PA_OPERATION_DONE || r == PA_OPERATION_CANCELLED)
|
|
break;
|
|
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
}
|
|
|
|
pa_operation_unref(op);
|
|
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
return 0;
|
|
}
|
|
|
|
int pa_system_write(pa_system* p, const void* data, size_t length, int *rerror)
|
|
{
|
|
pa_threaded_mainloop_lock(p->mainloop);
|
|
|
|
while (length > 0)
|
|
{
|
|
size_t l;
|
|
int r;
|
|
while(!(l = pa_stream_writable_size(p->stream))) {
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
}
|
|
if (l > length)
|
|
l = length;
|
|
|
|
r = pa_stream_write(p->stream, data, l, NULL, 0LL, PA_SEEK_RELATIVE);
|
|
|
|
data = (const uint8_t*) data + l;
|
|
length -= l;
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
return 0;
|
|
}
|
|
|
|
int pa_system_read(pa_system* p, void* data, size_t length, int *rerror)
|
|
{
|
|
pa_threaded_mainloop_lock(p->mainloop);
|
|
|
|
while (length > 0) {
|
|
size_t l;
|
|
while(!p->read_data) {
|
|
int r;
|
|
|
|
r = pa_stream_peek(p->stream, &p->read_data, &p->read_length);
|
|
|
|
if (p->read_length <= 0) {
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
} else if (!p->read_data) {
|
|
// There's a hole in the stream, skip it.
|
|
// We could generate silence, but that wouldn't work for compressed streams.
|
|
pa_stream_drop(p->stream);
|
|
} else
|
|
p->read_index = 0;
|
|
}
|
|
l = p->read_length < length ? p->read_length : length;
|
|
memcpy(data, (const uint8_t*) p->read_data+p->read_index, l);
|
|
|
|
data = (uint8_t*) data + l;
|
|
|
|
length -= l;
|
|
|
|
p->read_index += l;
|
|
p->read_length -= l;
|
|
|
|
if (!p->read_length) {
|
|
int r;
|
|
r = pa_stream_drop(p->stream);
|
|
p->read_data = NULL;
|
|
p->read_length = 0;
|
|
p->read_index;
|
|
}
|
|
}
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
return 0;
|
|
}
|
|
|
|
static void success_cb(pa_stream *s, int success, void * userdata)
|
|
{
|
|
pa_system* p = static_cast<pa_system *>(userdata);
|
|
|
|
p->operation_success = success;
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
}
|
|
|
|
int pa_system_drain(pa_system* p, int *rerror)
|
|
{
|
|
pa_operation *o = nullptr;
|
|
|
|
pa_threaded_mainloop_lock(p->mainloop);
|
|
|
|
o = pa_stream_drain(p->stream, success_cb, p);
|
|
|
|
if (!o)
|
|
{
|
|
throw std::runtime_error("pa_stream_drain did not fill our pa_operation structure!");
|
|
}
|
|
p->operation_success = 0;
|
|
|
|
while (pa_operation_get_state(o) == PA_OPERATION_RUNNING) {
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
}
|
|
|
|
pa_operation_unref(o);
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
|
|
return 0;
|
|
|
|
// unlock_and_fail:
|
|
if (o) {
|
|
pa_operation_cancel(o);
|
|
pa_operation_unref(o);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
return -1;
|
|
}
|
|
|
|
int pa_system_flush(pa_system* p, int *rerror)
|
|
{
|
|
pa_operation *o = NULL;
|
|
pa_threaded_mainloop_lock(p->mainloop);
|
|
|
|
o = pa_stream_flush(p->stream, success_cb, p);
|
|
|
|
p->operation_success = 0;
|
|
|
|
while(pa_operation_get_state(o) == PA_OPERATION_RUNNING) {
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
}
|
|
|
|
pa_operation_unref(o);
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
|
|
return 0;
|
|
}
|
|
|
|
pa_usec_t pa_system_get_latency(pa_system* p, int *rerror) {
|
|
pa_usec_t t;
|
|
|
|
pa_threaded_mainloop_lock(p->mainloop);
|
|
|
|
|
|
for (;;) {
|
|
int negative;
|
|
|
|
// CHECK_DEAD_GOTO()
|
|
|
|
if (pa_stream_get_latency(p->stream, &t, &negative) >= 0) {
|
|
if (p->direction == PA_STREAM_RECORD)
|
|
{
|
|
pa_usec_t already_read;
|
|
|
|
/*
|
|
*
|
|
*/
|
|
already_read = pa_bytes_to_usec(p->read_index, pa_stream_get_sample_spec(p->stream));
|
|
|
|
if (!negative) {
|
|
if (t > already_read)
|
|
t -= already_read;
|
|
else
|
|
t = 0;
|
|
}
|
|
}
|
|
if (negative)
|
|
t = 0;
|
|
|
|
break;
|
|
}
|
|
|
|
pa_threaded_mainloop_wait(p->mainloop);
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(p->mainloop);
|
|
|
|
return t;
|
|
}
|
|
|
|
#define BUFSIZE 1024
|
|
|
|
|
|
static void list_sink_info(pa_context* c, pa_sink_info* i, int eol, void* userdata)
|
|
{
|
|
|
|
if (eol != 0)
|
|
{
|
|
auto *p = static_cast<pa_system *>(userdata);
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
}
|
|
|
|
if (i == nullptr)
|
|
{
|
|
std::cerr << "Sink info is null!" << std::endl;
|
|
return;
|
|
}
|
|
std::cout << i->name << std::endl;
|
|
}
|
|
|
|
static void list_server_info(pa_context* c, pa_server_info* i, void* userdata)
|
|
{
|
|
auto *p = static_cast<pa_system *>(userdata);
|
|
pa_threaded_mainloop_signal(p->mainloop, 0);
|
|
}
|
|
|
|
char mix_sample_s16_pcm(char a, char b)
|
|
{
|
|
return (a + b) / 2.f;
|
|
}
|
|
|
|
// this dick
|
|
|
|
struct pcm_s16_data
|
|
{
|
|
std::vector<char> buffer;
|
|
explicit pcm_s16_data(const std::filesystem::path& file_path)
|
|
{
|
|
std::ifstream file(file_path, std::ios::binary | std::ios::ate);
|
|
file.unsetf(std::ios::skipws);
|
|
|
|
file.seekg(0, std::ios::end);
|
|
std::streamsize size = file.tellg();
|
|
file.seekg(0, std::ios::beg);
|
|
|
|
buffer.reserve(size);
|
|
|
|
buffer.insert(buffer.begin(),
|
|
std::istream_iterator<char>(file),
|
|
std::istream_iterator<char>());
|
|
|
|
}
|
|
explicit pcm_s16_data(const std::vector<char>& pcm_buf)
|
|
{
|
|
buffer = pcm_buf;
|
|
}
|
|
|
|
pcm_s16_data operator + (const pcm_s16_data& rhs) const
|
|
{
|
|
std::vector<char> sum;
|
|
auto rhs_buf = rhs.buffer;
|
|
auto lhs_buf = this->buffer;
|
|
for (int i = 0; i < std::max(lhs_buf.size(), rhs_buf.size()); i++)
|
|
{
|
|
int a = 0;
|
|
if (i >= lhs_buf.size() && i < rhs_buf.size())
|
|
{
|
|
sum.push_back(rhs_buf[i]);
|
|
continue;
|
|
}
|
|
if (i >= rhs_buf.size() && i < lhs_buf.size())
|
|
{
|
|
sum.push_back(lhs_buf[i]);
|
|
continue;
|
|
}
|
|
|
|
sum.push_back(mix_sample_s16_pcm(lhs_buf[i], rhs_buf[i]));
|
|
}
|
|
return pcm_s16_data(sum);
|
|
}
|
|
};
|
|
|
|
int main(int argc, char* argv[]) {
|
|
if (argc == 1 || argv[1] == "-h" || argv[1] == "help")
|
|
{
|
|
std::cout << "pacat - Tests playback of raw audio data via PulseAudio API." << std::endl;
|
|
std::cout << "usage: pacat <file> (presumably output.raw)\n" << std::endl;
|
|
}
|
|
|
|
//argv[1] = "output.raw";
|
|
|
|
static const pa_sample_spec ss = {
|
|
.format = PA_SAMPLE_S16LE,
|
|
.rate = 44100,
|
|
.channels = 2
|
|
};
|
|
|
|
pa_system *s = NULL;
|
|
|
|
int ret = 1;
|
|
int error;
|
|
|
|
/* Replace STDIN with the specified file if needed. */
|
|
|
|
if (argc < 4)
|
|
{
|
|
std::cerr << "Please provide two input PCM files!" << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
pcm_s16_data song = pcm_s16_data(argv[1]);
|
|
pcm_s16_data sfx = pcm_s16_data(argv[2]);
|
|
pcm_s16_data blm = pcm_s16_data(argv[3]);
|
|
|
|
pcm_s16_data combined = song + sfx + blm;
|
|
|
|
/*if (argc > 1) {
|
|
int fd;
|
|
if ((fd = open(argv[1], O_RDONLY)) < 0) {
|
|
fprintf(stderr, __FILE__": open() failed: %s\n", strerror(errno));
|
|
goto finish;
|
|
}
|
|
|
|
if (dup2(fd, STDIN_FILENO) < 0) {
|
|
fprintf(stderr, __FILE__": dup2() failed: %s\n", strerror(errno));
|
|
goto finish;
|
|
}
|
|
close(fd);
|
|
}*/
|
|
|
|
/* Create a new playback stream */
|
|
if (!(s = pa_system_new(NULL, argv[0], PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) {
|
|
fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
|
|
return -1;
|
|
//goto finish;
|
|
}
|
|
|
|
pa_context_get_server_info(s->context, reinterpret_cast<pa_server_info_cb_t>(list_server_info), s);
|
|
pa_context_get_sink_info_list(s->context, reinterpret_cast<pa_sink_info_cb_t>(list_sink_info), s);
|
|
|
|
pa_system_enumerate_devices(s);
|
|
|
|
if (pa_system_write(s, combined.buffer.data(), combined.buffer.size(), &error) < 0) {
|
|
fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
|
|
goto finish;
|
|
}
|
|
|
|
/* for (;;) {
|
|
uint8_t buf[BUFSIZE];
|
|
ssize_t r;
|
|
|
|
#if 0
|
|
pa_usec_t latency;
|
|
|
|
if ((latency = pa_simple_get_latency(s, &error)) == (pa_usec_t)-1)
|
|
{
|
|
fprintf(stderr, __FILE__": pa_simple_get_latency() failed: %s\n", pa_strerror(error));
|
|
return;
|
|
}
|
|
fprintf(stderr, "%0.0f usec \r", (float)latency);
|
|
#endif
|
|
|
|
if ((r = read(STDIN_FILENO, buf, sizeof(buf))) <= 0) {
|
|
if (r == 0)
|
|
break;
|
|
|
|
fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno));
|
|
goto finish;
|
|
}
|
|
|
|
|
|
if (pa_system_write(s, buf, (size_t)r, &error) < 0) {
|
|
fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
|
|
goto finish;
|
|
}
|
|
}*/
|
|
|
|
/* Make sure that every single sample was played */
|
|
if (pa_system_drain(s, &error) < 0) {
|
|
fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));
|
|
goto finish;
|
|
}
|
|
|
|
ret = 0;
|
|
finish:
|
|
if (s)
|
|
pa_system_free(s);
|
|
|
|
return ret;
|
|
} |