496 lines
10 KiB
C
496 lines
10 KiB
C
/*
|
|
* Copyright 2008-2013 Various Authors
|
|
* Copyright 2004-2006 Timo Hirvonen
|
|
*
|
|
* 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/>.
|
|
*/
|
|
|
|
#include "output.h"
|
|
#include "op.h"
|
|
#include "mixer.h"
|
|
#include "sf.h"
|
|
#include "utils.h"
|
|
#include "xmalloc.h"
|
|
#include "list.h"
|
|
#include "debug.h"
|
|
#include "ui_curses.h"
|
|
#include "options.h"
|
|
#include "xstrjoin.h"
|
|
#include "misc.h"
|
|
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <sys/types.h>
|
|
#include <dirent.h>
|
|
#include <dlfcn.h>
|
|
|
|
struct output_plugin {
|
|
struct list_head node;
|
|
char *name;
|
|
void *handle;
|
|
|
|
const unsigned *abi_version_ptr;
|
|
const struct output_plugin_ops *pcm_ops;
|
|
const struct mixer_plugin_ops *mixer_ops;
|
|
const struct output_plugin_opt *pcm_options;
|
|
const struct mixer_plugin_opt *mixer_options;
|
|
int priority;
|
|
|
|
unsigned int pcm_initialized : 1;
|
|
unsigned int mixer_initialized : 1;
|
|
unsigned int mixer_open : 1;
|
|
};
|
|
|
|
static const char *plugin_dir;
|
|
static LIST_HEAD(op_head);
|
|
static struct output_plugin *op = NULL;
|
|
|
|
/* volume is between 0 and volume_max */
|
|
int volume_max = 0;
|
|
int volume_l = -1;
|
|
int volume_r = -1;
|
|
|
|
static void add_plugin(struct output_plugin *plugin)
|
|
{
|
|
struct list_head *item = op_head.next;
|
|
|
|
while (item != &op_head) {
|
|
struct output_plugin *o = container_of(item, struct output_plugin, node);
|
|
|
|
if (plugin->priority < o->priority)
|
|
break;
|
|
item = item->next;
|
|
}
|
|
|
|
/* add before item */
|
|
list_add_tail(&plugin->node, item);
|
|
}
|
|
|
|
void op_load_plugins(void)
|
|
{
|
|
DIR *dir;
|
|
struct dirent *d;
|
|
|
|
plugin_dir = xstrjoin(cmus_lib_dir, "/op");
|
|
dir = opendir(plugin_dir);
|
|
if (dir == NULL) {
|
|
error_msg("couldn't open directory `%s': %s", plugin_dir, strerror(errno));
|
|
return;
|
|
}
|
|
while ((d = (struct dirent *) readdir(dir)) != NULL) {
|
|
char filename[512];
|
|
struct output_plugin *plug;
|
|
void *so, *symptr;
|
|
char *ext;
|
|
bool err = false;
|
|
|
|
if (d->d_name[0] == '.')
|
|
continue;
|
|
ext = strrchr(d->d_name, '.');
|
|
if (ext == NULL)
|
|
continue;
|
|
if (strcmp(ext, ".so"))
|
|
continue;
|
|
|
|
snprintf(filename, sizeof(filename), "%s/%s", plugin_dir, d->d_name);
|
|
|
|
so = dlopen(filename, RTLD_NOW);
|
|
if (so == NULL) {
|
|
d_print("%s: %s\n", filename, dlerror());
|
|
continue;
|
|
}
|
|
|
|
plug = xnew(struct output_plugin, 1);
|
|
|
|
plug->pcm_ops = dlsym(so, "op_pcm_ops");
|
|
plug->pcm_options = dlsym(so, "op_pcm_options");
|
|
symptr = dlsym(so, "op_priority");
|
|
plug->abi_version_ptr = dlsym(so, "op_abi_version");
|
|
if (!plug->pcm_ops || !plug->pcm_options || !symptr) {
|
|
error_msg("%s: missing symbol", filename);
|
|
err = true;
|
|
}
|
|
if (!plug->abi_version_ptr || *plug->abi_version_ptr != OP_ABI_VERSION) {
|
|
error_msg("%s: incompatible plugin version", filename);
|
|
err = true;
|
|
}
|
|
if (err) {
|
|
free(plug);
|
|
dlclose(so);
|
|
continue;
|
|
}
|
|
plug->priority = *(int *)symptr;
|
|
|
|
plug->mixer_ops = dlsym(so, "op_mixer_ops");
|
|
plug->mixer_options = dlsym(so, "op_mixer_options");
|
|
if (plug->mixer_ops == NULL || plug->mixer_options == NULL) {
|
|
plug->mixer_ops = NULL;
|
|
plug->mixer_options = NULL;
|
|
}
|
|
|
|
plug->name = xstrndup(d->d_name, ext - d->d_name);
|
|
plug->handle = so;
|
|
plug->pcm_initialized = 0;
|
|
plug->mixer_initialized = 0;
|
|
plug->mixer_open = 0;
|
|
|
|
add_plugin(plug);
|
|
}
|
|
closedir(dir);
|
|
}
|
|
|
|
static void init_plugin(struct output_plugin *o)
|
|
{
|
|
if (!o->mixer_initialized && o->mixer_ops) {
|
|
if (o->mixer_ops->init() == 0) {
|
|
d_print("initialized mixer for %s\n", o->name);
|
|
o->mixer_initialized = 1;
|
|
} else {
|
|
d_print("could not initialize mixer `%s'\n", o->name);
|
|
}
|
|
}
|
|
if (!o->pcm_initialized) {
|
|
if (o->pcm_ops->init() == 0) {
|
|
d_print("initialized pcm for %s\n", o->name);
|
|
o->pcm_initialized = 1;
|
|
} else {
|
|
d_print("could not initialize pcm `%s'\n", o->name);
|
|
}
|
|
}
|
|
}
|
|
|
|
void op_exit_plugins(void)
|
|
{
|
|
struct output_plugin *o;
|
|
|
|
list_for_each_entry(o, &op_head, node) {
|
|
if (o->mixer_initialized && o->mixer_ops)
|
|
o->mixer_ops->exit();
|
|
if (o->pcm_initialized)
|
|
o->pcm_ops->exit();
|
|
}
|
|
}
|
|
|
|
void mixer_close(void)
|
|
{
|
|
volume_max = 0;
|
|
if (op && op->mixer_open) {
|
|
BUG_ON(op->mixer_ops == NULL);
|
|
op->mixer_ops->close();
|
|
op->mixer_open = 0;
|
|
}
|
|
}
|
|
|
|
void mixer_open(void)
|
|
{
|
|
if (op == NULL)
|
|
return;
|
|
|
|
BUG_ON(op->mixer_open);
|
|
if (op->mixer_ops && op->mixer_initialized) {
|
|
int rc;
|
|
|
|
rc = op->mixer_ops->open(&volume_max);
|
|
if (rc == 0) {
|
|
op->mixer_open = 1;
|
|
mixer_read_volume();
|
|
} else {
|
|
volume_max = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
static int select_plugin(struct output_plugin *o)
|
|
{
|
|
/* try to initialize if not initialized yet */
|
|
init_plugin(o);
|
|
|
|
if (!o->pcm_initialized)
|
|
return -OP_ERROR_NOT_INITIALIZED;
|
|
op = o;
|
|
return 0;
|
|
}
|
|
|
|
int op_select(const char *name)
|
|
{
|
|
struct output_plugin *o;
|
|
|
|
list_for_each_entry(o, &op_head, node) {
|
|
if (strcasecmp(name, o->name) == 0)
|
|
return select_plugin(o);
|
|
}
|
|
return -OP_ERROR_NO_PLUGIN;
|
|
}
|
|
|
|
int op_select_any(void)
|
|
{
|
|
struct output_plugin *o;
|
|
int rc = -OP_ERROR_NO_PLUGIN;
|
|
sample_format_t sf = sf_channels(2) | sf_rate(44100) | sf_bits(16) | sf_signed(1);
|
|
|
|
list_for_each_entry(o, &op_head, node) {
|
|
rc = select_plugin(o);
|
|
if (rc != 0)
|
|
continue;
|
|
rc = o->pcm_ops->open(sf, NULL);
|
|
if (rc == 0) {
|
|
o->pcm_ops->close();
|
|
break;
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
int op_open(sample_format_t sf, const channel_position_t *channel_map)
|
|
{
|
|
if (op == NULL)
|
|
return -OP_ERROR_NOT_INITIALIZED;
|
|
return op->pcm_ops->open(sf, channel_map);
|
|
}
|
|
|
|
int op_drop(void)
|
|
{
|
|
if (op->pcm_ops->drop == NULL)
|
|
return -OP_ERROR_NOT_SUPPORTED;
|
|
return op->pcm_ops->drop();
|
|
}
|
|
|
|
int op_close(void)
|
|
{
|
|
return op->pcm_ops->close();
|
|
}
|
|
|
|
int op_write(const char *buffer, int count)
|
|
{
|
|
return op->pcm_ops->write(buffer, count);
|
|
}
|
|
|
|
int op_pause(void)
|
|
{
|
|
if (op->pcm_ops->pause == NULL)
|
|
return 0;
|
|
return op->pcm_ops->pause();
|
|
}
|
|
|
|
int op_unpause(void)
|
|
{
|
|
if (op->pcm_ops->unpause == NULL)
|
|
return 0;
|
|
return op->pcm_ops->unpause();
|
|
}
|
|
|
|
int op_buffer_space(void)
|
|
{
|
|
return op->pcm_ops->buffer_space();
|
|
}
|
|
|
|
int mixer_set_volume(int left, int right)
|
|
{
|
|
if (op == NULL)
|
|
return -OP_ERROR_NOT_INITIALIZED;
|
|
if (!op->mixer_open)
|
|
return -OP_ERROR_NOT_OPEN;
|
|
return op->mixer_ops->set_volume(left, right);
|
|
}
|
|
|
|
int mixer_read_volume(void)
|
|
{
|
|
if (op == NULL)
|
|
return -OP_ERROR_NOT_INITIALIZED;
|
|
if (!op->mixer_open)
|
|
return -OP_ERROR_NOT_OPEN;
|
|
return op->mixer_ops->get_volume(&volume_l, &volume_r);
|
|
}
|
|
|
|
int mixer_get_fds(int what, int *fds)
|
|
{
|
|
if (op == NULL)
|
|
return -OP_ERROR_NOT_INITIALIZED;
|
|
if (!op->mixer_open)
|
|
return -OP_ERROR_NOT_OPEN;
|
|
switch (*op->abi_version_ptr) {
|
|
case 1:
|
|
if (!op->mixer_ops->get_fds.abi_1)
|
|
return -OP_ERROR_NOT_SUPPORTED;
|
|
if (what != MIXER_FDS_VOLUME)
|
|
return 0;
|
|
return op->mixer_ops->get_fds.abi_1(fds);
|
|
default:
|
|
if (!op->mixer_ops->get_fds.abi_2)
|
|
return -OP_ERROR_NOT_SUPPORTED;
|
|
return op->mixer_ops->get_fds.abi_2(what, fds);
|
|
}
|
|
}
|
|
|
|
extern int soft_vol;
|
|
|
|
static void option_error(int rc)
|
|
{
|
|
char *msg = op_get_error_msg(rc, "setting option");
|
|
error_msg("%s", msg);
|
|
free(msg);
|
|
}
|
|
|
|
static void set_dsp_option(void *data, const char *val)
|
|
{
|
|
const struct output_plugin_opt *o = data;
|
|
int rc;
|
|
|
|
rc = o->set(val);
|
|
if (rc)
|
|
option_error(rc);
|
|
}
|
|
|
|
static bool option_of_current_mixer(const struct mixer_plugin_opt *opt)
|
|
{
|
|
const struct mixer_plugin_opt *mpo;
|
|
|
|
if (!op)
|
|
return false;
|
|
for (mpo = op->mixer_options; mpo && mpo->name; mpo++) {
|
|
if (mpo == opt)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void set_mixer_option(void *data, const char *val)
|
|
{
|
|
const struct mixer_plugin_opt *o = data;
|
|
int rc;
|
|
|
|
rc = o->set(val);
|
|
if (rc) {
|
|
option_error(rc);
|
|
} else if (option_of_current_mixer(o)) {
|
|
/* option of the current op was set
|
|
* try to reopen the mixer */
|
|
mixer_close();
|
|
if (!soft_vol || pause_on_output_change)
|
|
mixer_open();
|
|
}
|
|
}
|
|
|
|
static void get_dsp_option(void *data, char *buf, size_t size)
|
|
{
|
|
const struct output_plugin_opt *o = data;
|
|
char *val = NULL;
|
|
|
|
o->get(&val);
|
|
if (val) {
|
|
strscpy(buf, val, size);
|
|
free(val);
|
|
}
|
|
}
|
|
|
|
static void get_mixer_option(void *data, char *buf, size_t size)
|
|
{
|
|
const struct mixer_plugin_opt *o = data;
|
|
char *val = NULL;
|
|
|
|
o->get(&val);
|
|
if (val) {
|
|
strscpy(buf, val, size);
|
|
free(val);
|
|
}
|
|
}
|
|
|
|
void op_add_options(void)
|
|
{
|
|
struct output_plugin *o;
|
|
const struct output_plugin_opt *opo;
|
|
const struct mixer_plugin_opt *mpo;
|
|
char key[64];
|
|
|
|
list_for_each_entry(o, &op_head, node) {
|
|
for (opo = o->pcm_options; opo->name; opo++) {
|
|
snprintf(key, sizeof(key), "dsp.%s.%s", o->name,
|
|
opo->name);
|
|
option_add(xstrdup(key), opo, get_dsp_option,
|
|
set_dsp_option, NULL, 0);
|
|
}
|
|
for (mpo = o->mixer_options; mpo && mpo->name; mpo++) {
|
|
snprintf(key, sizeof(key), "mixer.%s.%s", o->name,
|
|
mpo->name);
|
|
option_add(xstrdup(key), mpo, get_mixer_option,
|
|
set_mixer_option, NULL, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
char *op_get_error_msg(int rc, const char *arg)
|
|
{
|
|
char buffer[1024];
|
|
|
|
switch (-rc) {
|
|
case OP_ERROR_ERRNO:
|
|
snprintf(buffer, sizeof(buffer), "%s: %s", arg, strerror(errno));
|
|
break;
|
|
case OP_ERROR_NO_PLUGIN:
|
|
snprintf(buffer, sizeof(buffer),
|
|
"%s: no such plugin", arg);
|
|
break;
|
|
case OP_ERROR_NOT_INITIALIZED:
|
|
snprintf(buffer, sizeof(buffer),
|
|
"%s: couldn't initialize required output plugin", arg);
|
|
break;
|
|
case OP_ERROR_NOT_SUPPORTED:
|
|
snprintf(buffer, sizeof(buffer),
|
|
"%s: function not supported", arg);
|
|
break;
|
|
case OP_ERROR_NOT_OPEN:
|
|
snprintf(buffer, sizeof(buffer),
|
|
"%s: mixer is not open", arg);
|
|
break;
|
|
case OP_ERROR_SAMPLE_FORMAT:
|
|
snprintf(buffer, sizeof(buffer),
|
|
"%s: sample format not supported", arg);
|
|
break;
|
|
case OP_ERROR_NOT_OPTION:
|
|
snprintf(buffer, sizeof(buffer),
|
|
"%s: no such option", arg);
|
|
break;
|
|
case OP_ERROR_INTERNAL:
|
|
snprintf(buffer, sizeof(buffer), "%s: internal error", arg);
|
|
break;
|
|
case OP_ERROR_SUCCESS:
|
|
default:
|
|
snprintf(buffer, sizeof(buffer),
|
|
"%s: this is not an error (%d), this is a bug",
|
|
arg, rc);
|
|
break;
|
|
}
|
|
return xstrdup(buffer);
|
|
}
|
|
|
|
void op_dump_plugins(void)
|
|
{
|
|
struct output_plugin *o;
|
|
|
|
printf("\nOutput Plugins: %s\n", plugin_dir);
|
|
list_for_each_entry(o, &op_head, node) {
|
|
printf(" %s\n", o->name);
|
|
}
|
|
}
|
|
|
|
const char *op_get_current(void)
|
|
{
|
|
if (op)
|
|
return op->name;
|
|
return NULL;
|
|
}
|