650 lines
16 KiB
C
650 lines
16 KiB
C
/*
|
|
* Copyright 2014 Niko Efthymiou <nefthy-cmus@nefthy.de>
|
|
*
|
|
* 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.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/* TODO
|
|
*
|
|
* - configurable maping of channels to ports
|
|
*/
|
|
|
|
#include <jack/jack.h>
|
|
#include <jack/ringbuffer.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <stdbool.h>
|
|
|
|
#if HAVE_CONFIG
|
|
#include "../config/samplerate.h"
|
|
#ifdef HAVE_SAMPLERATE
|
|
#include <samplerate.h>
|
|
#endif
|
|
#endif
|
|
|
|
#include "../op.h"
|
|
#include "../utils.h"
|
|
#include "../channelmap.h"
|
|
#include "../xmalloc.h"
|
|
#include "../debug.h"
|
|
|
|
#define CHANNELS 2
|
|
#define BUFFER_MULTIPLYER (sizeof(jack_default_audio_sample_t) * 16)
|
|
#define BUFFER_SIZE_MIN 16384
|
|
|
|
static char *server_name;
|
|
|
|
static jack_client_t *client;
|
|
static jack_port_t *output_ports[CHANNELS];
|
|
static jack_ringbuffer_t *ringbuffer[CHANNELS];
|
|
|
|
static jack_nframes_t jack_sample_rate;
|
|
|
|
#ifdef HAVE_SAMPLERATE
|
|
static SRC_STATE* src_state[CHANNELS];
|
|
static int src_quality = SRC_SINC_BEST_QUALITY;
|
|
static float resample_ratio = 1.0f;
|
|
#endif
|
|
|
|
static size_t buffer_size;
|
|
static sample_format_t sample_format;
|
|
static unsigned int sample_bytes;
|
|
static const channel_position_t *channel_map;
|
|
static volatile bool paused = true;
|
|
static volatile bool drop = false;
|
|
static volatile bool drop_done = true;
|
|
|
|
/* fail on the next call */
|
|
static int fail;
|
|
|
|
/* function pointer to appropriate read function */
|
|
static float (*read_sample) (const char *buff);
|
|
|
|
static int op_jack_init(void);
|
|
static int op_jack_exit(void);
|
|
static int op_jack_open(sample_format_t sf, const channel_position_t* cm);
|
|
static int op_jack_close(void);
|
|
static int op_jack_write(const char *buffer, int count);
|
|
static int op_jack_drop(void);
|
|
static int op_jack_buffer_space(void);
|
|
static int op_jack_pause(void);
|
|
static int op_jack_unpause(void);
|
|
|
|
/* read functions for various sample formats */
|
|
|
|
static jack_default_audio_sample_t read_sample_le16(const char *buffer)
|
|
{
|
|
int16_t s = (int16_t)read_le16(buffer);
|
|
uint16_t upper_bound = (uint16_t)INT16_MAX + (s <= 0);
|
|
return (jack_default_audio_sample_t)s / (jack_default_audio_sample_t)upper_bound;
|
|
}
|
|
|
|
static jack_default_audio_sample_t read_sample_le24(const char *buffer)
|
|
{
|
|
int32_t s = read_le24i(buffer);
|
|
uint32_t upper_bound = 0x7FFFFF + (s < 0);
|
|
return (jack_default_audio_sample_t) s / (jack_default_audio_sample_t)upper_bound;
|
|
}
|
|
|
|
static jack_default_audio_sample_t read_sample_le32(const char *buffer)
|
|
{
|
|
int32_t s = (int32_t)read_le32(buffer);
|
|
uint32_t upper_bound = (uint32_t)INT32_MAX + (s <= 0);
|
|
return (jack_default_audio_sample_t)s / (jack_default_audio_sample_t)upper_bound;
|
|
}
|
|
|
|
static jack_default_audio_sample_t read_sample_le16u(const char *buffer)
|
|
{
|
|
uint32_t u = read_le16(buffer);
|
|
return (((jack_default_audio_sample_t) u)
|
|
/ ((jack_default_audio_sample_t) UINT16_MAX)) * 2.0 - 2.0;
|
|
}
|
|
|
|
static jack_default_audio_sample_t read_sample_le24u(const char *buffer)
|
|
{
|
|
uint32_t u = read_le24(buffer);
|
|
return (((jack_default_audio_sample_t) u)
|
|
/ ((jack_default_audio_sample_t) 0xFFFFFFU)) * 2.0 - 2.0;
|
|
}
|
|
|
|
static jack_default_audio_sample_t read_sample_le32u(const char *buffer)
|
|
{
|
|
uint32_t u = read_le32(buffer);
|
|
return (((jack_default_audio_sample_t) u)
|
|
/ ((jack_default_audio_sample_t) UINT32_MAX)) * 2.0 - 2.0;
|
|
}
|
|
|
|
#ifdef HAVE_SAMPLERATE
|
|
static void op_jack_reset_src(void) {
|
|
for (int c = 0; c < CHANNELS; c++) {
|
|
src_reset(src_state[c]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* jack callbacks */
|
|
static void op_jack_error_cb(const char *msg) {
|
|
d_print("jackd error: %s\n", msg);
|
|
fail = 1;
|
|
}
|
|
|
|
static int op_jack_cb(jack_nframes_t frames, void *arg)
|
|
{
|
|
size_t bytes_want = frames * sizeof(jack_default_audio_sample_t);
|
|
|
|
if (drop) {
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
jack_ringbuffer_reset(ringbuffer[i]);
|
|
}
|
|
drop = false;
|
|
drop_done = true;
|
|
}
|
|
|
|
size_t bytes_min = SIZE_MAX;
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
size_t bytes_available = jack_ringbuffer_read_space(ringbuffer[i]);
|
|
if (bytes_available < bytes_min) {
|
|
bytes_min = bytes_available;
|
|
}
|
|
}
|
|
|
|
/* if there is less than frames available play silence */
|
|
if (paused || bytes_min < bytes_want) {
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
jack_default_audio_sample_t* jack_buf = jack_port_get_buffer(output_ports[i], frames);
|
|
memset(jack_buf, 0, bytes_want);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
jack_default_audio_sample_t* jack_buf = jack_port_get_buffer(output_ports[i], frames);
|
|
size_t bytes_read = jack_ringbuffer_read(ringbuffer[i], (char*) jack_buf, bytes_want);
|
|
|
|
if (bytes_read < bytes_want) {
|
|
/* This should not happen[TM] - just in case set fail = 1 */
|
|
d_print("underrun! wanted %zu only got %zu bytes\n", bytes_want, bytes_read);
|
|
fail = 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* init or resize buffers if needed */
|
|
static int op_jack_buffer_init(jack_nframes_t samples, void *arg)
|
|
{
|
|
if (buffer_size > samples * BUFFER_MULTIPLYER) {
|
|
/* we just don't shrink buffers, since this could result
|
|
* in gaps and they won't get that big anyway
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
buffer_size = samples * BUFFER_MULTIPLYER;
|
|
if (buffer_size < BUFFER_SIZE_MIN) {
|
|
buffer_size = BUFFER_SIZE_MIN;
|
|
}
|
|
d_print("new buffer size %zu\n", buffer_size);
|
|
|
|
char *tmp = xmalloc(buffer_size);
|
|
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
jack_ringbuffer_t *new_buffer = jack_ringbuffer_create(buffer_size);
|
|
|
|
if (!new_buffer) {
|
|
d_print("ringbuffer alloc failed\n");
|
|
free(tmp);
|
|
fail = 1;
|
|
op_jack_exit();
|
|
return 1;
|
|
}
|
|
if (ringbuffer[i] != NULL) {
|
|
size_t length = jack_ringbuffer_read_space(ringbuffer[i]);
|
|
|
|
/* actually this could both read/write less than length.
|
|
* In that case, which should not happen[TM], there will
|
|
* be a gap in playback.
|
|
*/
|
|
jack_ringbuffer_read(ringbuffer[i], tmp, length);
|
|
jack_ringbuffer_write(new_buffer, tmp, length);
|
|
|
|
jack_ringbuffer_free(ringbuffer[i]);
|
|
}
|
|
|
|
ringbuffer[i] = new_buffer;
|
|
}
|
|
|
|
free(tmp);
|
|
return 0;
|
|
}
|
|
|
|
static int op_jack_sample_rate_cb(jack_nframes_t samples, void *arg)
|
|
{
|
|
#ifdef HAVE_SAMPLERATE
|
|
resample_ratio = (float) sf_get_rate(sample_format) / (float) samples;
|
|
#else
|
|
if (jack_sample_rate != samples) {
|
|
/* this cannot be handled */
|
|
fail = 1;
|
|
return 1;
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
static void op_jack_shutdown_cb(void *arg)
|
|
{
|
|
d_print("jackd went away");
|
|
|
|
/* calling op_jack_exit() will cause a segfault if op_jack_write or
|
|
* anything else is in the middle of something...
|
|
* the fail flag is checked by op_jack_write and op_jack_buffer_space
|
|
*
|
|
* op_jack_open will try to reinitialize jack
|
|
*/
|
|
fail = 1;
|
|
}
|
|
|
|
/* cmus callbacks */
|
|
|
|
static int op_jack_init(void)
|
|
{
|
|
#ifdef HAVE_SAMPLERATE
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
src_state[i] = src_new(src_quality, 1, NULL);
|
|
if (src_state[i] == NULL) {
|
|
d_print("src_new failed");
|
|
for (i = i - 1; i >= 0; i--) {
|
|
src_delete(src_state[i]);
|
|
}
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
}
|
|
#endif
|
|
jack_set_error_function(op_jack_error_cb);
|
|
|
|
jack_options_t options = JackNullOption;
|
|
if (fail) {
|
|
/* since jackd failed, it will not be autostarted. Either jackd
|
|
* was killed intentionally or it died by heartattack.
|
|
* Until it is restarted, init will happily fail again
|
|
* and again and again..
|
|
*/
|
|
options |= JackNoStartServer;
|
|
}
|
|
|
|
jack_status_t status;
|
|
client = jack_client_open("cmus", options, &status, server_name);
|
|
if (client == NULL) {
|
|
d_print("jack_client_new failed\n");
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
|
|
if (status & JackServerStarted) {
|
|
d_print("jackd started\n");
|
|
}
|
|
|
|
jack_nframes_t jack_buffer_size = jack_get_buffer_size(client);
|
|
jack_sample_rate = jack_get_sample_rate(client);
|
|
op_jack_buffer_init(jack_buffer_size, NULL);
|
|
|
|
jack_set_process_callback(client, op_jack_cb, NULL);
|
|
jack_set_sample_rate_callback(client, op_jack_sample_rate_cb, NULL);
|
|
jack_set_buffer_size_callback(client, op_jack_buffer_init, NULL);
|
|
jack_on_shutdown(client, op_jack_shutdown_cb, NULL);
|
|
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
char port_name[20];
|
|
snprintf(port_name, sizeof(port_name)-1, "output %d", i);
|
|
|
|
output_ports[i] = jack_port_register(
|
|
client,
|
|
port_name,
|
|
JACK_DEFAULT_AUDIO_TYPE,
|
|
JackPortIsOutput,
|
|
0
|
|
);
|
|
if (output_ports[i] == NULL) {
|
|
d_print("no jack ports available\n");
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
}
|
|
|
|
if (jack_activate(client)) {
|
|
d_print("jack_client_activate failed\n");
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
|
|
const char **ports = jack_get_ports(client, NULL, NULL, JackPortIsPhysical | JackPortIsInput);
|
|
if (ports == NULL) {
|
|
d_print("cannot get playback ports\n");
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
if (ports[i] == NULL) {
|
|
d_print("could not connect output %d. too few ports.\n", i);
|
|
break;
|
|
}
|
|
if (jack_connect(client, jack_port_name(output_ports[i]), ports[i])) {
|
|
d_print("cannot connect port %s\n", ports[i]);
|
|
jack_free(ports);
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
}
|
|
|
|
jack_free(ports);
|
|
fail = 0;
|
|
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_exit(void)
|
|
{
|
|
if (client != NULL) {
|
|
jack_deactivate(client);
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
if (output_ports[i] != NULL) {
|
|
jack_port_unregister(client, output_ports[i]);
|
|
}
|
|
}
|
|
jack_client_close(client);
|
|
}
|
|
|
|
for (int i = 0; i < CHANNELS; i++) {
|
|
if (ringbuffer[i] != NULL) {
|
|
jack_ringbuffer_free(ringbuffer[i]);
|
|
}
|
|
ringbuffer[i] = NULL;
|
|
}
|
|
|
|
buffer_size = 0;
|
|
client = NULL;
|
|
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_open(sample_format_t sf, const channel_position_t *cm)
|
|
{
|
|
sample_format = sf;
|
|
|
|
if (fail) {
|
|
/* jack went away so lets see if we can recover */
|
|
if (client != NULL) {
|
|
op_jack_exit();
|
|
}
|
|
if (op_jack_init() != OP_ERROR_SUCCESS) {
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
}
|
|
|
|
if (cm == NULL) {
|
|
d_print("no channel_map\n");
|
|
return -OP_ERROR_NOT_SUPPORTED;
|
|
}
|
|
channel_map = cm;
|
|
|
|
#ifdef HAVE_SAMPLERATE
|
|
op_jack_reset_src();
|
|
resample_ratio = (float) jack_sample_rate / (float) sf_get_rate(sf);
|
|
#else
|
|
if (jack_sample_rate != sf_get_rate(sf)) {
|
|
d_print("jack sample rate of %d does not match %d\n", jack_get_sample_rate(client), sf_get_rate(sf));
|
|
return -OP_ERROR_SAMPLE_FORMAT;
|
|
}
|
|
#endif
|
|
|
|
if (sf_get_channels(sf) < CHANNELS) {
|
|
d_print("%d channels not supported\n", sf_get_channels(sf));
|
|
return -OP_ERROR_SAMPLE_FORMAT;
|
|
}
|
|
|
|
int bits = sf_get_bits(sf);
|
|
|
|
if (bits == 16) {
|
|
sample_bytes = 2;
|
|
read_sample = sf_get_signed(sf) ? &read_sample_le16 : &read_sample_le16u;
|
|
} else if (bits == 24) {
|
|
sample_bytes = 3;
|
|
read_sample = sf_get_signed(sf) ? &read_sample_le24 : &read_sample_le24u;
|
|
} else if (bits == 32) {
|
|
sample_bytes = 4;
|
|
read_sample = sf_get_signed(sf) ? &read_sample_le32 : &read_sample_le32u;
|
|
} else {
|
|
d_print("%d bits not supported\n", sf_get_bits(sf));
|
|
return -OP_ERROR_SAMPLE_FORMAT;
|
|
}
|
|
|
|
paused = false;
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_close(void)
|
|
{
|
|
paused = true;
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_drop(void)
|
|
{
|
|
if (!drop_done) {
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
drop_done = false;
|
|
drop = true;
|
|
while (!drop_done) {
|
|
/* wait till op_jack_cb is done with dropping */
|
|
usleep(1000);
|
|
}
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_write(const char *buffer, int count)
|
|
{
|
|
if (fail) {
|
|
op_jack_exit();
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
|
|
if (!drop_done) {
|
|
return 0;
|
|
}
|
|
|
|
int frame_size = sf_get_frame_size(sample_format);
|
|
int channels = sf_get_channels(sample_format);
|
|
size_t frames = count / frame_size;
|
|
|
|
/* since this is the only place where the ringbuffers get
|
|
* written, available space will only grow, therefore frames_min
|
|
* is safe.
|
|
*/
|
|
size_t frames_min = SIZE_MAX;
|
|
for (int c = 0; c < CHANNELS; c++) {
|
|
size_t frames_available = jack_ringbuffer_write_space(ringbuffer[c]) / sizeof(jack_default_audio_sample_t);
|
|
if (frames_available < frames_min) {
|
|
frames_min = frames_available;
|
|
}
|
|
}
|
|
|
|
if (frames > frames_min) {
|
|
frames = frames_min;
|
|
}
|
|
|
|
jack_default_audio_sample_t buf[CHANNELS][buffer_size];
|
|
|
|
/* demux and convert to float */
|
|
for (int pos = 0; pos < count; ) {
|
|
int frame = pos / frame_size;
|
|
for (int c = 0; c < channels; c++) {
|
|
int idx = pos + c * sample_bytes;
|
|
/* for now, only 2 channels and mono are supported */
|
|
if (channel_map[c] == CHANNEL_POSITION_LEFT || channel_map[c] == CHANNEL_POSITION_MONO) {
|
|
buf[0][frame] = read_sample(&buffer[idx]);
|
|
} else if (channel_map[c] == CHANNEL_POSITION_RIGHT || channel_map[c] == CHANNEL_POSITION_MONO) {
|
|
buf[1][frame] = read_sample(&buffer[idx]);
|
|
}
|
|
}
|
|
pos += frame_size;
|
|
}
|
|
|
|
#ifdef HAVE_SAMPLERATE
|
|
if (resample_ratio > 1.01f || resample_ratio < 0.99) {
|
|
jack_default_audio_sample_t converted[buffer_size];
|
|
SRC_DATA src_data;
|
|
for (int c = 0; c < CHANNELS; c++) {
|
|
src_data.data_in = buf[c];
|
|
src_data.data_out = converted;
|
|
src_data.input_frames = frames;
|
|
src_data.output_frames = frames_min;
|
|
src_data.src_ratio = resample_ratio;
|
|
src_data.end_of_input = 0;
|
|
|
|
int err = src_process(src_state[c], &src_data);
|
|
if (err) {
|
|
d_print("libsamplerate err %s\n", src_strerror(err));
|
|
}
|
|
|
|
int byte_length = src_data.output_frames_gen * sizeof(jack_default_audio_sample_t);
|
|
jack_ringbuffer_write(ringbuffer[c], (const char*) converted, byte_length);
|
|
}
|
|
return src_data.input_frames_used * frame_size;
|
|
} else {
|
|
#endif
|
|
int byte_length = frames * sizeof(jack_default_audio_sample_t);
|
|
for (int c = 0; c < CHANNELS; c++) {
|
|
jack_ringbuffer_write(ringbuffer[c], (const char*) buf[c], byte_length);
|
|
}
|
|
|
|
return frames * frame_size;
|
|
#ifdef HAVE_SAMPLERATE
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static int op_jack_buffer_space(void)
|
|
{
|
|
if (fail) {
|
|
op_jack_exit();
|
|
return -OP_ERROR_INTERNAL;
|
|
}
|
|
|
|
int bytes = jack_ringbuffer_write_space(ringbuffer[0]);
|
|
for (int c = 1; c < CHANNELS; c++) {
|
|
int tmp = jack_ringbuffer_write_space(ringbuffer[0]);
|
|
if (bytes > tmp) {
|
|
bytes = tmp;
|
|
}
|
|
}
|
|
|
|
int frames = bytes / sizeof(jack_default_audio_sample_t);
|
|
int frame_size = sf_get_frame_size(sample_format);
|
|
|
|
#ifdef HAVE_SAMPLERATE
|
|
return (int) ((float) (frames) / resample_ratio) * frame_size;
|
|
#else
|
|
return frames * frame_size;
|
|
#endif
|
|
}
|
|
|
|
static int op_jack_pause(void)
|
|
{
|
|
paused = true;
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_unpause(void)
|
|
{
|
|
#ifdef HAVE_SAMPLERATE
|
|
op_jack_reset_src();
|
|
#endif
|
|
paused = false;
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_set_server_name(const char *val)
|
|
{
|
|
free(server_name);
|
|
server_name = val[0] != '\0' ? xstrdup(val) : NULL;
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_get_server_name(char **val)
|
|
{
|
|
*val = xstrdup(server_name != NULL ? server_name : "");
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
#ifdef HAVE_SAMPLERATE
|
|
static int op_jack_set_resampling_quality(const char *val)
|
|
{
|
|
if (strlen(val) != 1) {
|
|
return -OP_ERROR_NOT_SUPPORTED;
|
|
}
|
|
switch (val[0]) {
|
|
default:
|
|
case '2':
|
|
src_quality = SRC_SINC_BEST_QUALITY;
|
|
break;
|
|
case '1':
|
|
src_quality = SRC_SINC_MEDIUM_QUALITY;
|
|
break;
|
|
case '0':
|
|
src_quality = SRC_SINC_FASTEST;
|
|
break;
|
|
}
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
|
|
static int op_jack_get_resampling_quality(char **val)
|
|
{
|
|
switch (src_quality) {
|
|
case SRC_SINC_BEST_QUALITY:
|
|
*val = xstrdup("2");
|
|
break;
|
|
case SRC_SINC_MEDIUM_QUALITY:
|
|
*val = xstrdup("1");
|
|
break;
|
|
case SRC_SINC_FASTEST:
|
|
*val = xstrdup("0");
|
|
break;
|
|
}
|
|
return OP_ERROR_SUCCESS;
|
|
}
|
|
#endif
|
|
|
|
const struct output_plugin_ops op_pcm_ops = {
|
|
.init = op_jack_init,
|
|
.exit = op_jack_exit,
|
|
.open = op_jack_open,
|
|
.close = op_jack_close,
|
|
.drop = op_jack_drop,
|
|
.write = op_jack_write,
|
|
.buffer_space = op_jack_buffer_space,
|
|
.pause = op_jack_pause,
|
|
.unpause = op_jack_unpause,
|
|
};
|
|
|
|
const struct output_plugin_opt op_pcm_options[] = {
|
|
OPT(op_jack, server_name),
|
|
#ifdef HAVE_SAMPLERATE
|
|
OPT(op_jack, resampling_quality),
|
|
#endif
|
|
{ NULL },
|
|
};
|
|
|
|
const int op_priority = 2;
|
|
const unsigned op_abi_version = OP_ABI_VERSION;
|