mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-27 23:55:57 -05:00
[alsa] Add rewritten ALSA to generic outputs interface
This commit is contained in:
parent
f1fb86e7e1
commit
c5bb83480d
@ -26,7 +26,7 @@ MPD_SRC=mpd.c mpd.h
|
||||
endif
|
||||
|
||||
if COND_ALSA
|
||||
ALSA_SRC=laudio_alsa.c
|
||||
ALSA_SRC=outputs/alsa.c
|
||||
endif
|
||||
|
||||
if COND_OSS4
|
||||
|
@ -33,9 +33,6 @@
|
||||
#include "player.h"
|
||||
#include "laudio.h"
|
||||
|
||||
#ifdef ALSA
|
||||
extern audio_output audio_alsa;
|
||||
#endif
|
||||
#ifdef OSS4
|
||||
extern audio_output audio_oss4;
|
||||
#endif
|
||||
@ -43,9 +40,6 @@ extern audio_output audio_oss4;
|
||||
extern audio_output audio_dummy;
|
||||
|
||||
static audio_output *outputs[] = {
|
||||
#ifdef ALSA
|
||||
&audio_alsa,
|
||||
#endif
|
||||
#ifdef OSS4
|
||||
&audio_oss4,
|
||||
#endif
|
||||
|
@ -32,14 +32,14 @@
|
||||
#include "outputs.h"
|
||||
|
||||
extern struct output_definition output_raop;
|
||||
extern struct output_definition output_streaming;
|
||||
#ifdef CHROMECAST
|
||||
extern struct output_definition output_cast;
|
||||
#endif
|
||||
extern struct output_definition output_streaming;
|
||||
/* TODO
|
||||
#ifdef ALSA
|
||||
extern struct output_definition output_alsa;
|
||||
#endif
|
||||
/* TODO
|
||||
#ifdef OSS4
|
||||
extern struct output_definition output_oss4;
|
||||
#endif
|
||||
@ -49,14 +49,14 @@ extern struct output_definition output_dummy;
|
||||
// Must be in sync with enum output_types
|
||||
static struct output_definition *outputs[] = {
|
||||
&output_raop,
|
||||
&output_streaming,
|
||||
#ifdef CHROMECAST
|
||||
&output_cast,
|
||||
#endif
|
||||
&output_streaming,
|
||||
/* TODO
|
||||
#ifdef ALSA
|
||||
&output_alsa,
|
||||
#endif
|
||||
/* TODO
|
||||
#ifdef OSS4
|
||||
&output_oss4,
|
||||
#endif
|
||||
|
@ -14,18 +14,48 @@
|
||||
* When a device is started the output backend will typically create a session.
|
||||
* This session is only passed around as an opaque object in this interface.
|
||||
*
|
||||
* Here is the sequence of commands from the player to the outputs, and the
|
||||
* callback from the output once the command has been executed. Commands marked
|
||||
* with * may make multiple callbacks if multiple sessions are affected.
|
||||
* (TODO should callbacks always be deferred?)
|
||||
*
|
||||
* PLAYER OUTPUT PLAYER CB
|
||||
* speaker_activate -> device_start -> device_activate_cb
|
||||
* -> (if playback) -> playback_start -> device_streaming_cb* (or no cb)
|
||||
* -> (else if playback not active) -> device_streaming_cb
|
||||
* -> (fail) -> device_stop -> device_lost_cb
|
||||
* speaker_activate -> device_probe -> device_probe_cb
|
||||
* speaker_deactivate -> device_stop -> device_shutdown_cb
|
||||
* volume_set -> device_volume_set -> device_command_cb
|
||||
* -> -> device_streaming_cb
|
||||
* (volume_setrel/abs_speaker is the same)
|
||||
* playback_start_item -> device_start -> device_restart_cb
|
||||
* -> (success) -> device_streaming_cb
|
||||
* -> (fail) -> device_stop -> device_lost_cb
|
||||
* playback_start_bh -> playback_start -> device_streaming_cb* (or no cb)
|
||||
* playback_stop -> flush -> device_command_cb*
|
||||
* -> -> device_streaming_cb*
|
||||
* -> -> playback_stop -> device_streaming_cb*
|
||||
* playback_pause -> flush -> device_command_cb*
|
||||
* -> -> device_streaming_cb*
|
||||
* -> -> playback_stop -> device_streaming_cb*
|
||||
* playback_abort -> playback_stop -> device_streaming_cb* (or no cb)
|
||||
* device_streaming_cb -> device_streaming_cb (re-add)
|
||||
*
|
||||
*/
|
||||
|
||||
// Must be in sync with outputs[] in outputs.c
|
||||
enum output_types
|
||||
{
|
||||
OUTPUT_TYPE_RAOP,
|
||||
OUTPUT_TYPE_STREAMING,
|
||||
#ifdef CHROMECAST
|
||||
OUTPUT_TYPE_CAST,
|
||||
#endif
|
||||
OUTPUT_TYPE_STREAMING,
|
||||
/* TODO
|
||||
#ifdef ALSA
|
||||
OUTPUT_TYPE_ALSA,
|
||||
#endif
|
||||
/* TODO
|
||||
OUTPUT_TYPE_OSS,
|
||||
OUTPUT_TYPE_DUMMY,
|
||||
*/
|
||||
|
934
src/outputs/alsa.c
Normal file
934
src/outputs/alsa.c
Normal file
@ -0,0 +1,934 @@
|
||||
/*
|
||||
* Copyright (C) 2015-2016 Espen Jürgensen <espenjurgensen@gmail.com>
|
||||
* Copyright (C) 2010 Julien BLACHE <jb@jblache.org>
|
||||
*
|
||||
* 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, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
# include <config.h>
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <event2/event.h>
|
||||
#include <asoundlib.h>
|
||||
|
||||
#include "conffile.h"
|
||||
#include "logger.h"
|
||||
#include "player.h"
|
||||
#include "outputs.h"
|
||||
|
||||
#define ALSA_PACKETS_WRITE_MAX 10
|
||||
#define PACKET_SIZE STOB(AIRTUNES_V2_PACKET_SAMPLES)
|
||||
|
||||
// TODO Unglobalise these and add support for multiple sound cards
|
||||
static char *card_name;
|
||||
static char *mixer_name;
|
||||
static snd_pcm_t *hdl;
|
||||
static snd_mixer_t *mixer_hdl;
|
||||
static snd_mixer_elem_t *vol_elem;
|
||||
static long vol_min;
|
||||
static long vol_max;
|
||||
|
||||
#define ALSA_F_STARTED (1 << 15)
|
||||
|
||||
enum alsa_state
|
||||
{
|
||||
ALSA_STATE_STOPPED = 0,
|
||||
ALSA_STATE_STARTED = ALSA_F_STARTED,
|
||||
ALSA_STATE_STREAMING = ALSA_F_STARTED | 0x01,
|
||||
|
||||
ALSA_STATE_FAILED = -1,
|
||||
};
|
||||
|
||||
struct alsa_session
|
||||
{
|
||||
enum alsa_state state;
|
||||
|
||||
char *devname;
|
||||
|
||||
uint64_t pos;
|
||||
uint64_t start_pos;
|
||||
|
||||
// An array that will hold the packets we prebuffer. The length of the array
|
||||
// is prebuf_len (measured in rtp_packets)
|
||||
uint8_t *prebuf;
|
||||
uint32_t prebuf_len;
|
||||
uint32_t prebuf_head;
|
||||
uint32_t prebuf_tail;
|
||||
|
||||
int volume;
|
||||
|
||||
struct event *deferredev;
|
||||
output_status_cb defer_cb;
|
||||
|
||||
/* Do not dereference - only passed to the status cb */
|
||||
struct output_device *device;
|
||||
struct output_session *output_session;
|
||||
output_status_cb status_cb;
|
||||
|
||||
struct alsa_session *next;
|
||||
};
|
||||
|
||||
/* From player.c */
|
||||
extern struct event_base *evbase_player;
|
||||
|
||||
static struct alsa_session *sessions;
|
||||
|
||||
/* Forwards */
|
||||
static void
|
||||
defer_cb(int fd, short what, void *arg);
|
||||
|
||||
/* ---------------------------- SESSION HANDLING ---------------------------- */
|
||||
|
||||
static void
|
||||
prebuf_free(struct alsa_session *as)
|
||||
{
|
||||
if (as->prebuf)
|
||||
free(as->prebuf);
|
||||
|
||||
as->prebuf = NULL;
|
||||
as->prebuf_len = 0;
|
||||
as->prebuf_head = 0;
|
||||
as->prebuf_tail = 0;
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_session_free(struct alsa_session *as)
|
||||
{
|
||||
event_free(as->deferredev);
|
||||
|
||||
prebuf_free(as);
|
||||
|
||||
free(as->output_session);
|
||||
free(as);
|
||||
|
||||
as = NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_session_cleanup(struct alsa_session *as)
|
||||
{
|
||||
struct alsa_session *s;
|
||||
|
||||
if (as == sessions)
|
||||
sessions = sessions->next;
|
||||
else
|
||||
{
|
||||
for (s = sessions; s && (s->next != as); s = s->next)
|
||||
; /* EMPTY */
|
||||
|
||||
if (!s)
|
||||
DPRINTF(E_WARN, L_LAUDIO, "WARNING: struct alsa_session not found in list; BUG!\n");
|
||||
else
|
||||
s->next = as->next;
|
||||
}
|
||||
|
||||
alsa_session_free(as);
|
||||
}
|
||||
|
||||
static struct alsa_session *
|
||||
alsa_session_make(struct output_device *device, output_status_cb cb)
|
||||
{
|
||||
struct output_session *os;
|
||||
struct alsa_session *as;
|
||||
|
||||
os = calloc(1, sizeof(struct output_session));
|
||||
as = calloc(1, sizeof(struct alsa_session));
|
||||
if (!os || !as)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA session\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
as->deferredev = evtimer_new(evbase_player, defer_cb, as);
|
||||
if (!as->deferredev)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA deferred event\n");
|
||||
free(os);
|
||||
free(as);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
os->session = as;
|
||||
os->type = device->type;
|
||||
|
||||
as->output_session = os;
|
||||
as->state = ALSA_STATE_STOPPED;
|
||||
as->device = device;
|
||||
as->status_cb = cb;
|
||||
as->volume = device->volume;
|
||||
as->devname = card_name;
|
||||
|
||||
as->next = sessions;
|
||||
sessions = as;
|
||||
|
||||
return as;
|
||||
}
|
||||
|
||||
|
||||
/* ---------------------------- STATUS HANDLERS ----------------------------- */
|
||||
|
||||
// Maps our internal state to the generic output state and then makes a callback
|
||||
// to the player to tell that state
|
||||
static void
|
||||
defer_cb(int fd, short what, void *arg)
|
||||
{
|
||||
struct alsa_session *as = arg;
|
||||
enum output_device_state state;
|
||||
|
||||
switch (as->state)
|
||||
{
|
||||
case ALSA_STATE_FAILED:
|
||||
state = OUTPUT_STATE_FAILED;
|
||||
break;
|
||||
case ALSA_STATE_STOPPED:
|
||||
state = OUTPUT_STATE_STOPPED;
|
||||
break;
|
||||
case ALSA_STATE_STARTED:
|
||||
state = OUTPUT_STATE_CONNECTED;
|
||||
break;
|
||||
case ALSA_STATE_STREAMING:
|
||||
state = OUTPUT_STATE_STREAMING;
|
||||
break;
|
||||
default:
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Bug! Unhandled state in alsa_status()\n");
|
||||
state = OUTPUT_STATE_FAILED;
|
||||
}
|
||||
|
||||
if (as->defer_cb)
|
||||
as->defer_cb(as->device, as->output_session, state);
|
||||
|
||||
if (!(as->state & ALSA_F_STARTED))
|
||||
alsa_session_cleanup(as);
|
||||
}
|
||||
|
||||
// Note: alsa_states also nukes the session if it is not ALSA_F_STARTED
|
||||
static void
|
||||
alsa_status(struct alsa_session *as)
|
||||
{
|
||||
as->defer_cb = as->status_cb;
|
||||
event_active(as->deferredev, 0, 0);
|
||||
as->status_cb = NULL;
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------- MISC HELPERS ----------------------------- */
|
||||
|
||||
/*static int
|
||||
start_threshold_set(snd_pcm_uframes_t threshold)
|
||||
{
|
||||
snd_pcm_sw_params_t *sw_params;
|
||||
int ret;
|
||||
|
||||
ret = snd_pcm_sw_params_malloc(&sw_params);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not allocate sw params: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_sw_params_current(hdl, sw_params);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve current sw params: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_sw_params_set_start_threshold(hdl, sw_params, threshold);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not set start threshold: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_sw_params(hdl, sw_params);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not set sw params: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
out_fail:
|
||||
snd_pcm_sw_params_free(sw_params);
|
||||
|
||||
return -1;
|
||||
}
|
||||
*/
|
||||
|
||||
static int
|
||||
mixer_open(void)
|
||||
{
|
||||
snd_mixer_elem_t *elem;
|
||||
snd_mixer_elem_t *master;
|
||||
snd_mixer_elem_t *pcm;
|
||||
snd_mixer_elem_t *custom;
|
||||
snd_mixer_selem_id_t *sid;
|
||||
int ret;
|
||||
|
||||
ret = snd_mixer_open(&mixer_hdl, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Failed to open mixer: %s\n", snd_strerror(ret));
|
||||
|
||||
mixer_hdl = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = snd_mixer_attach(mixer_hdl, card_name);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Failed to attach mixer: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_close;
|
||||
}
|
||||
|
||||
ret = snd_mixer_selem_register(mixer_hdl, NULL, NULL);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Failed to register mixer: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_detach;
|
||||
}
|
||||
|
||||
ret = snd_mixer_load(mixer_hdl);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Failed to load mixer: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_detach;
|
||||
}
|
||||
|
||||
// Grab interesting elements
|
||||
snd_mixer_selem_id_alloca(&sid);
|
||||
|
||||
pcm = NULL;
|
||||
master = NULL;
|
||||
custom = NULL;
|
||||
for (elem = snd_mixer_first_elem(mixer_hdl); elem; elem = snd_mixer_elem_next(elem))
|
||||
{
|
||||
snd_mixer_selem_get_id(elem, sid);
|
||||
|
||||
if (mixer_name && (strcmp(snd_mixer_selem_id_get_name(sid), mixer_name) == 0))
|
||||
{
|
||||
custom = elem;
|
||||
break;
|
||||
}
|
||||
else if (strcmp(snd_mixer_selem_id_get_name(sid), "PCM") == 0)
|
||||
pcm = elem;
|
||||
else if (strcmp(snd_mixer_selem_id_get_name(sid), "Master") == 0)
|
||||
master = elem;
|
||||
}
|
||||
|
||||
if (mixer_name)
|
||||
{
|
||||
if (custom)
|
||||
vol_elem = custom;
|
||||
else
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Failed to open configured mixer element '%s'\n", mixer_name);
|
||||
|
||||
goto out_detach;
|
||||
}
|
||||
}
|
||||
else if (pcm)
|
||||
vol_elem = pcm;
|
||||
else if (master)
|
||||
vol_elem = master;
|
||||
else
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Failed to open PCM or Master mixer element\n");
|
||||
|
||||
goto out_detach;
|
||||
}
|
||||
|
||||
// Get min & max volume
|
||||
snd_mixer_selem_get_playback_volume_range(vol_elem, &vol_min, &vol_max);
|
||||
|
||||
return 0;
|
||||
|
||||
out_detach:
|
||||
snd_mixer_detach(mixer_hdl, card_name);
|
||||
out_close:
|
||||
snd_mixer_close(mixer_hdl);
|
||||
mixer_hdl = NULL;
|
||||
vol_elem = NULL;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
device_open(void)
|
||||
{
|
||||
snd_pcm_hw_params_t *hw_params;
|
||||
snd_pcm_uframes_t bufsize;
|
||||
int ret;
|
||||
|
||||
hw_params = NULL;
|
||||
|
||||
ret = snd_pcm_open(&hdl, card_name, SND_PCM_STREAM_PLAYBACK, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not open playback device: %s\n", snd_strerror(ret));
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// HW params
|
||||
ret = snd_pcm_hw_params_malloc(&hw_params);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not allocate hw params: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params_any(hdl, hw_params);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not retrieve hw params: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params_set_access(hdl, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not set access method: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params_set_format(hdl, hw_params, SND_PCM_FORMAT_S16_LE);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not set S16LE format: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params_set_channels(hdl, hw_params, 2);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not set stereo output: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params_set_rate(hdl, hw_params, 44100, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Hardware doesn't support 44.1 kHz: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params_get_buffer_size_max(hw_params, &bufsize);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not get max buffer size: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params_set_buffer_size_max(hdl, hw_params, &bufsize);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not set buffer size to max: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
ret = snd_pcm_hw_params(hdl, hw_params);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not set hw params: %s\n", snd_strerror(ret));
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
snd_pcm_hw_params_free(hw_params);
|
||||
hw_params = NULL;
|
||||
|
||||
ret = mixer_open();
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not open mixer\n");
|
||||
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
out_fail:
|
||||
if (hw_params)
|
||||
snd_pcm_hw_params_free(hw_params);
|
||||
|
||||
snd_pcm_close(hdl);
|
||||
hdl = NULL;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void
|
||||
device_close(void)
|
||||
{
|
||||
snd_pcm_close(hdl);
|
||||
hdl = NULL;
|
||||
|
||||
if (mixer_hdl)
|
||||
{
|
||||
snd_mixer_detach(mixer_hdl, card_name);
|
||||
snd_mixer_close(mixer_hdl);
|
||||
|
||||
mixer_hdl = NULL;
|
||||
vol_elem = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
playback_start(struct alsa_session *as, uint64_t pos, uint64_t start_pos)
|
||||
{
|
||||
snd_output_t *output;
|
||||
char *debug_pcm_cfg;
|
||||
int ret;
|
||||
|
||||
ret = snd_pcm_prepare(hdl);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not prepare ALSA device '%s': %s\n", as->devname, snd_strerror(ret));
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear prebuffer in case start somehow got called twice without a stop in between
|
||||
prebuf_free(as);
|
||||
|
||||
// The difference between pos and start_pos should match the 2 second
|
||||
// buffer that AirPlay uses. We will not use alsa's buffer for the initial
|
||||
// buffering, because my sound card's start_threshold is not to be counted on.
|
||||
// Instead we allocate our own buffer, and when it is time to play we write as
|
||||
// much as we can to alsa's buffer.
|
||||
as->prebuf_len = (start_pos - pos) / AIRTUNES_V2_PACKET_SAMPLES + 1;
|
||||
if (as->prebuf_len > 2*126)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Sanity check of prebuf_len (%" PRIu32 " packets) failed\n", as->prebuf_len);
|
||||
return;
|
||||
}
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Will prebuffer %d packets\n", as->prebuf_len);
|
||||
|
||||
as->prebuf = malloc(as->prebuf_len * PACKET_SIZE);
|
||||
if (!as->prebuf)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for audio buffer (requested %" PRIu32 " packets)\n", as->prebuf_len);
|
||||
return;
|
||||
}
|
||||
|
||||
as->pos = pos;
|
||||
as->start_pos = start_pos;
|
||||
|
||||
// Dump PCM config data for E_DBG logging
|
||||
ret = snd_output_buffer_open(&output);
|
||||
if (ret == 0)
|
||||
{
|
||||
if (snd_pcm_dump_setup(hdl, output) == 0)
|
||||
{
|
||||
snd_output_buffer_string(output, &debug_pcm_cfg);
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Dump of sound device config:\n%s\n", debug_pcm_cfg);
|
||||
}
|
||||
|
||||
snd_output_close(output);
|
||||
}
|
||||
|
||||
as->state = ALSA_STATE_STREAMING;
|
||||
}
|
||||
|
||||
static void
|
||||
playback_write(struct alsa_session *as, uint8_t *buf, uint64_t rtptime)
|
||||
{
|
||||
snd_pcm_sframes_t ret;
|
||||
snd_pcm_sframes_t avail;
|
||||
snd_pcm_sframes_t nsamp;
|
||||
uint8_t *pkt;
|
||||
int prebuffering;
|
||||
int prebuf_empty;
|
||||
int npackets;
|
||||
|
||||
prebuffering = (as->pos < as->start_pos);
|
||||
prebuf_empty = (as->prebuf_head == as->prebuf_tail);
|
||||
|
||||
as->pos += AIRTUNES_V2_PACKET_SAMPLES;
|
||||
|
||||
// We need to copy to the prebuffer if we are prebuffering OR if the
|
||||
// prebuffer has not been emptied yet
|
||||
if (prebuffering || !prebuf_empty)
|
||||
{
|
||||
pkt = &as->prebuf[as->prebuf_head * PACKET_SIZE];
|
||||
|
||||
memcpy(pkt, buf, PACKET_SIZE);
|
||||
|
||||
as->prebuf_head = (as->prebuf_head + 1) % as->prebuf_len;
|
||||
}
|
||||
|
||||
if (prebuffering)
|
||||
return;
|
||||
|
||||
ret = snd_pcm_avail_update(hdl);
|
||||
if (ret < 0)
|
||||
goto alsa_error;
|
||||
|
||||
avail = ret;
|
||||
if (avail < AIRTUNES_V2_PACKET_SAMPLES)
|
||||
return;
|
||||
|
||||
// If we have data in prebuf we send as much as we can
|
||||
if (!prebuf_empty)
|
||||
{
|
||||
buf = &as->prebuf[as->prebuf_tail * PACKET_SIZE];
|
||||
|
||||
if (as->prebuf_head > as->prebuf_tail)
|
||||
npackets = as->prebuf_head - as->prebuf_tail;
|
||||
else
|
||||
npackets = as->prebuf_len - as->prebuf_tail;
|
||||
|
||||
nsamp = npackets * AIRTUNES_V2_PACKET_SAMPLES;
|
||||
while (nsamp > avail)
|
||||
{
|
||||
npackets -= 1;
|
||||
nsamp -= AIRTUNES_V2_PACKET_SAMPLES;
|
||||
}
|
||||
|
||||
as->prebuf_tail = (as->prebuf_tail + npackets) % as->prebuf_len;
|
||||
}
|
||||
else
|
||||
nsamp = AIRTUNES_V2_PACKET_SAMPLES;
|
||||
|
||||
ret = snd_pcm_writei(hdl, buf, nsamp);
|
||||
if (ret < 0)
|
||||
goto alsa_error;
|
||||
else if (ret != nsamp)
|
||||
DPRINTF(E_WARN, L_LAUDIO, "ALSA partial write detected\n");
|
||||
|
||||
return;
|
||||
|
||||
alsa_error:
|
||||
if (ret == -EPIPE)
|
||||
{
|
||||
DPRINTF(E_WARN, L_LAUDIO, "ALSA buffer underrun\n");
|
||||
|
||||
ret = snd_pcm_prepare(hdl);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_WARN, L_LAUDIO, "ALSA couldn't recover from underrun: %s\n", snd_strerror(ret));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill the prebuf with audio before restarting, so we don't underrun again
|
||||
as->start_pos = as->pos + AIRTUNES_V2_PACKET_SAMPLES * (as->prebuf_len - 1);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DPRINTF(E_LOG, L_LAUDIO, "ALSA write error: %s\n", snd_strerror(ret));
|
||||
|
||||
as->state = ALSA_STATE_FAILED;
|
||||
alsa_status(as);
|
||||
}
|
||||
|
||||
static void
|
||||
playback_pos_get(uint64_t *pos, uint64_t next_pkt)
|
||||
{
|
||||
uint64_t cur_pos;
|
||||
struct timespec now;
|
||||
int ret;
|
||||
|
||||
ret = player_get_current_pos(&cur_pos, &now, 0);
|
||||
if (ret < 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Could not get playback position, setting to next_pkt - 2 seconds\n");
|
||||
cur_pos = next_pkt - 88200;
|
||||
}
|
||||
|
||||
// Make pos the rtptime of the packet containing cur_pos
|
||||
*pos = next_pkt;
|
||||
while (*pos > cur_pos)
|
||||
*pos -= AIRTUNES_V2_PACKET_SAMPLES;
|
||||
}
|
||||
|
||||
/* ------------------ INTERFACE FUNCTIONS CALLED BY OUTPUTS.C --------------- */
|
||||
|
||||
static int
|
||||
alsa_device_start(struct output_device *device, output_status_cb cb, uint64_t rtptime)
|
||||
{
|
||||
struct alsa_session *as;
|
||||
int ret;
|
||||
|
||||
as = alsa_session_make(device, cb);
|
||||
if (!as)
|
||||
return -1;
|
||||
|
||||
ret = device_open();
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
as->state = ALSA_STATE_STARTED;
|
||||
alsa_status(as);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_device_stop(struct output_session *session)
|
||||
{
|
||||
struct alsa_session *as = session->session;
|
||||
|
||||
device_close();
|
||||
|
||||
as->state = ALSA_STATE_STOPPED;
|
||||
alsa_status(as);
|
||||
}
|
||||
|
||||
static int
|
||||
alsa_device_probe(struct output_device *device, output_status_cb cb)
|
||||
{
|
||||
struct alsa_session *as;
|
||||
int ret;
|
||||
|
||||
as = alsa_session_make(device, cb);
|
||||
if (!as)
|
||||
return -1;
|
||||
|
||||
ret = device_open();
|
||||
if (ret < 0)
|
||||
{
|
||||
alsa_session_cleanup(as);
|
||||
return -1;
|
||||
}
|
||||
|
||||
device_close();
|
||||
|
||||
as->state = ALSA_STATE_STOPPED;
|
||||
alsa_status(as);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
alsa_device_volume_set(struct output_device *device, output_status_cb cb)
|
||||
{
|
||||
struct alsa_session *as;
|
||||
int pcm_vol;
|
||||
|
||||
if (!device->session || !device->session->session)
|
||||
return 0;
|
||||
|
||||
as = device->session->session;
|
||||
|
||||
if (!mixer_hdl || !vol_elem)
|
||||
return -1;
|
||||
|
||||
snd_mixer_handle_events(mixer_hdl);
|
||||
|
||||
if (!snd_mixer_selem_is_active(vol_elem))
|
||||
return -1;
|
||||
|
||||
switch (device->volume)
|
||||
{
|
||||
case 0:
|
||||
pcm_vol = vol_min;
|
||||
break;
|
||||
|
||||
case 100:
|
||||
pcm_vol = vol_max;
|
||||
break;
|
||||
|
||||
default:
|
||||
pcm_vol = vol_min + (device->volume * (vol_max - vol_min)) / 100;
|
||||
break;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Setting ALSA volume to %d (%d)\n", pcm_vol, device->volume);
|
||||
|
||||
snd_mixer_selem_set_playback_volume_all(vol_elem, pcm_vol);
|
||||
|
||||
alsa_status(as);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_playback_start(uint64_t next_pkt, struct timespec *ts)
|
||||
{
|
||||
struct alsa_session *as;
|
||||
uint64_t pos;
|
||||
|
||||
if (!sessions)
|
||||
return;
|
||||
|
||||
playback_pos_get(&pos, next_pkt);
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA audio (pos %" PRIu64 ", next_pkt %" PRIu64 ")\n", pos, next_pkt);
|
||||
|
||||
for (as = sessions; as; as = as->next)
|
||||
playback_start(as, pos, next_pkt);
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_playback_stop(void)
|
||||
{
|
||||
struct alsa_session *as;
|
||||
|
||||
for (as = sessions; as; as = as->next)
|
||||
{
|
||||
snd_pcm_drop(hdl);
|
||||
prebuf_free(as);
|
||||
|
||||
as->state = ALSA_STATE_STARTED;
|
||||
alsa_status(as);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_write(uint8_t *buf, uint64_t rtptime)
|
||||
{
|
||||
struct alsa_session *as;
|
||||
uint64_t pos;
|
||||
|
||||
for (as = sessions; as; as = as->next)
|
||||
{
|
||||
if (as->state == ALSA_STATE_STARTED)
|
||||
{
|
||||
playback_pos_get(&pos, rtptime);
|
||||
|
||||
DPRINTF(E_DBG, L_LAUDIO, "Starting ALSA device '%s' (pos %" PRIu64 ", rtptime %" PRIu64 ")\n", as->devname, pos, rtptime);
|
||||
|
||||
playback_start(as, pos, rtptime);
|
||||
}
|
||||
|
||||
playback_write(as, buf, rtptime);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
alsa_flush(output_status_cb cb, uint64_t rtptime)
|
||||
{
|
||||
struct alsa_session *as;
|
||||
int i;
|
||||
|
||||
i = 0;
|
||||
for (as = sessions; as; as = as->next)
|
||||
{
|
||||
i++;
|
||||
|
||||
snd_pcm_drop(hdl);
|
||||
prebuf_free(as);
|
||||
|
||||
as->status_cb = cb;
|
||||
as->state = ALSA_STATE_STARTED;
|
||||
alsa_status(as);
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_set_status_cb(struct output_session *session, output_status_cb cb)
|
||||
{
|
||||
struct alsa_session *as = session->session;
|
||||
|
||||
as->status_cb = cb;
|
||||
}
|
||||
|
||||
static int
|
||||
alsa_init(void)
|
||||
{
|
||||
struct output_device *device;
|
||||
cfg_t *cfg_audio;
|
||||
char *nickname;
|
||||
char *type;
|
||||
|
||||
cfg_audio = cfg_getsec(cfg, "audio");
|
||||
type = cfg_getstr(cfg_audio, "type");
|
||||
|
||||
if (type && (strcasecmp(type, "alsa") != 0))
|
||||
return -1;
|
||||
|
||||
card_name = cfg_getstr(cfg_audio, "card");
|
||||
mixer_name = cfg_getstr(cfg_audio, "mixer");
|
||||
nickname = cfg_getstr(cfg_audio, "nickname");
|
||||
|
||||
device = calloc(1, sizeof(struct output_device));
|
||||
if (!device)
|
||||
{
|
||||
DPRINTF(E_LOG, L_LAUDIO, "Out of memory for ALSA device\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
device->id = 0;
|
||||
device->name = nickname;
|
||||
device->type = OUTPUT_TYPE_ALSA;
|
||||
device->type_name = outputs_name(device->type);
|
||||
device->advertised = 1;
|
||||
device->has_video = 0;
|
||||
|
||||
DPRINTF(E_INFO, L_LAUDIO, "Adding ALSA device '%s' using friendly name '%s'\n", card_name, nickname);
|
||||
|
||||
player_device_add(device);
|
||||
|
||||
snd_lib_error_set_handler(logger_alsa);
|
||||
|
||||
hdl = NULL;
|
||||
mixer_hdl = NULL;
|
||||
vol_elem = NULL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
alsa_deinit(void)
|
||||
{
|
||||
snd_lib_error_set_handler(NULL);
|
||||
}
|
||||
|
||||
struct output_definition output_alsa =
|
||||
{
|
||||
.name = "ALSA",
|
||||
.type = OUTPUT_TYPE_ALSA,
|
||||
.priority = 3,
|
||||
.disabled = 0,
|
||||
.init = alsa_init,
|
||||
.deinit = alsa_deinit,
|
||||
.device_start = alsa_device_start,
|
||||
.device_stop = alsa_device_stop,
|
||||
.device_probe = alsa_device_probe,
|
||||
.device_volume_set = alsa_device_volume_set,
|
||||
.playback_start = alsa_playback_start,
|
||||
.playback_stop = alsa_playback_stop,
|
||||
.write = alsa_write,
|
||||
.flush = alsa_flush,
|
||||
.status_cb = alsa_set_status_cb,
|
||||
};
|
28
src/player.c
28
src/player.c
@ -1770,6 +1770,8 @@ device_streaming_cb(struct output_device *device, struct output_session *session
|
||||
{
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_streaming_cb\n", outputs_name(device->type));
|
||||
|
||||
ret = device_check(device);
|
||||
if (ret < 0)
|
||||
{
|
||||
@ -1814,6 +1816,8 @@ device_streaming_cb(struct output_device *device, struct output_session *session
|
||||
static void
|
||||
device_command_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_command_cb\n", outputs_name(device->type));
|
||||
|
||||
cur_cmd->output_requests_pending--;
|
||||
|
||||
outputs_status_cb(session, device_streaming_cb);
|
||||
@ -1837,6 +1841,8 @@ device_shutdown_cb(struct output_device *device, struct output_session *session,
|
||||
{
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_shutdown_cb\n", outputs_name(device->type));
|
||||
|
||||
cur_cmd->output_requests_pending--;
|
||||
|
||||
if (output_sessions)
|
||||
@ -1871,6 +1877,8 @@ device_shutdown_cb(struct output_device *device, struct output_session *session,
|
||||
static void
|
||||
device_lost_cb(struct output_device *device, struct output_session *session, enum output_device_state status)
|
||||
{
|
||||
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_lost_cb\n", outputs_name(device->type));
|
||||
|
||||
/* We lost that device during startup for some reason, not much we can do here */
|
||||
if (status == OUTPUT_STATE_FAILED)
|
||||
DPRINTF(E_WARN, L_PLAYER, "Failed to stop lost device\n");
|
||||
@ -1884,6 +1892,8 @@ device_activate_cb(struct output_device *device, struct output_session *session,
|
||||
struct timespec ts;
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_activate_cb\n", outputs_name(device->type));
|
||||
|
||||
cur_cmd->output_requests_pending--;
|
||||
|
||||
ret = device_check(device);
|
||||
@ -1955,6 +1965,8 @@ device_probe_cb(struct output_device *device, struct output_session *session, en
|
||||
{
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_probe_cb\n", outputs_name(device->type));
|
||||
|
||||
cur_cmd->output_requests_pending--;
|
||||
|
||||
ret = device_check(device);
|
||||
@ -2002,6 +2014,8 @@ device_restart_cb(struct output_device *device, struct output_session *session,
|
||||
{
|
||||
int ret;
|
||||
|
||||
DPRINTF(E_DBG, L_PLAYER, "Callback from %s to device_restart_cb\n", outputs_name(device->type));
|
||||
|
||||
cur_cmd->output_requests_pending--;
|
||||
|
||||
ret = device_check(device);
|
||||
@ -2233,7 +2247,7 @@ playback_stop(struct player_command *cmd)
|
||||
|
||||
metadata_purge();
|
||||
|
||||
/* We're async if we need to flush RAOP devices */
|
||||
/* We're async if we need to flush devices */
|
||||
if (cmd->output_requests_pending > 0)
|
||||
return 1; /* async */
|
||||
|
||||
@ -2408,7 +2422,7 @@ playback_start_item(struct player_command *cmd, struct queue_item *qii)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* We're async if we need to start RAOP devices */
|
||||
/* We're async if we need to start devices */
|
||||
if (cmd->output_requests_pending > 0)
|
||||
return 1; /* async */
|
||||
|
||||
@ -2663,7 +2677,7 @@ playback_pause(struct player_command *cmd)
|
||||
|
||||
metadata_purge();
|
||||
|
||||
/* We're async if we need to flush RAOP devices */
|
||||
/* We're async if we need to flush devices */
|
||||
if (cmd->output_requests_pending > 0)
|
||||
return 1; /* async */
|
||||
|
||||
@ -2809,8 +2823,8 @@ speaker_set(struct player_command *cmd)
|
||||
if (cmd->ret != -2)
|
||||
cmd->ret = -1;
|
||||
}
|
||||
|
||||
cmd->output_requests_pending += 1;
|
||||
else
|
||||
cmd->output_requests_pending++;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -2830,8 +2844,8 @@ speaker_set(struct player_command *cmd)
|
||||
if (cmd->ret != -2)
|
||||
cmd->ret = -1;
|
||||
}
|
||||
|
||||
cmd->output_requests_pending += 1;
|
||||
else
|
||||
cmd->output_requests_pending++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user