output: play from a music_pipe object
Instead of passing individual buffers to audio_output_all_play(), pass music_chunk objects. Append all those chunks asynchronously to a music_pipe instance. All output threads may then read chunks from this pipe. This reduces MPD's internal latency by an order of magnitude.
This commit is contained in:
230
src/output_all.c
230
src/output_all.c
@@ -20,6 +20,12 @@
|
|||||||
#include "output_internal.h"
|
#include "output_internal.h"
|
||||||
#include "output_control.h"
|
#include "output_control.h"
|
||||||
#include "conf.h"
|
#include "conf.h"
|
||||||
|
#include "pipe.h"
|
||||||
|
#include "buffer.h"
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
#include "chunk.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -32,6 +38,17 @@ static struct audio_format input_audio_format;
|
|||||||
static struct audio_output *audio_outputs;
|
static struct audio_output *audio_outputs;
|
||||||
static unsigned int num_audio_outputs;
|
static unsigned int num_audio_outputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The #music_buffer object where consumed chunks are returned.
|
||||||
|
*/
|
||||||
|
static struct music_buffer *g_music_buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The #music_pipe object which feeds all audio outputs. It is filled
|
||||||
|
* by audio_output_all_play().
|
||||||
|
*/
|
||||||
|
static struct music_pipe *g_mp;
|
||||||
|
|
||||||
unsigned int audio_output_count(void)
|
unsigned int audio_output_count(void)
|
||||||
{
|
{
|
||||||
return num_audio_outputs;
|
return num_audio_outputs;
|
||||||
@@ -179,60 +196,63 @@ audio_output_all_update(void)
|
|||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
for (i = 0; i < num_audio_outputs; ++i)
|
||||||
ret = audio_output_update(&audio_outputs[i],
|
ret = audio_output_update(&audio_outputs[i],
|
||||||
&input_audio_format) || ret;
|
&input_audio_format, g_mp) || ret;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_all_play(const char *buffer, size_t length)
|
audio_output_all_play(struct music_chunk *chunk)
|
||||||
{
|
{
|
||||||
bool ret = false;
|
bool ret;
|
||||||
unsigned int i;
|
unsigned int i;
|
||||||
|
|
||||||
assert(length > 0);
|
assert(g_music_buffer != NULL);
|
||||||
/* no partial frames allowed */
|
assert(g_mp != NULL);
|
||||||
assert((length % audio_format_frame_size(&input_audio_format)) == 0);
|
assert(chunk != NULL);
|
||||||
|
assert(music_chunk_check_format(chunk, &input_audio_format));
|
||||||
|
|
||||||
audio_output_all_update();
|
ret = audio_output_all_update();
|
||||||
|
if (!ret)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
music_pipe_push(g_mp, chunk);
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
for (i = 0; i < num_audio_outputs; ++i)
|
||||||
if (audio_output_is_open(&audio_outputs[i]))
|
if (audio_output_is_open(&audio_outputs[i]))
|
||||||
audio_output_play(&audio_outputs[i],
|
audio_output_play(&audio_outputs[i]);
|
||||||
buffer, length);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
bool finished = true;
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i) {
|
|
||||||
struct audio_output *ao = &audio_outputs[i];
|
|
||||||
|
|
||||||
if (!audio_output_is_open(ao))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (audio_output_command_is_finished(ao))
|
|
||||||
ret = true;
|
|
||||||
else {
|
|
||||||
finished = false;
|
|
||||||
audio_output_signal(ao);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finished)
|
|
||||||
break;
|
|
||||||
|
|
||||||
notify_wait(&audio_output_client_notify);
|
|
||||||
};
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_all_open(const struct audio_format *audio_format)
|
audio_output_all_open(const struct audio_format *audio_format,
|
||||||
|
struct music_buffer *buffer)
|
||||||
{
|
{
|
||||||
bool ret = false, enabled = false;
|
bool ret = false, enabled = false;
|
||||||
unsigned int i;
|
unsigned int i;
|
||||||
|
|
||||||
|
assert(buffer != NULL);
|
||||||
|
assert(g_music_buffer == NULL || g_music_buffer == buffer);
|
||||||
|
assert((g_mp == NULL) == (g_music_buffer == NULL));
|
||||||
|
|
||||||
|
g_music_buffer = buffer;
|
||||||
|
|
||||||
|
/* the audio format must be the same as existing chunks in the
|
||||||
|
pipe */
|
||||||
|
assert(audio_format == NULL || g_mp == NULL ||
|
||||||
|
music_pipe_check_format(g_mp, audio_format));
|
||||||
|
|
||||||
|
if (g_mp == NULL)
|
||||||
|
g_mp = music_pipe_new();
|
||||||
|
else
|
||||||
|
/* if the pipe hasn't been cleared, the the audio
|
||||||
|
format must not have changed */
|
||||||
|
assert(music_pipe_size(g_mp) == 0 ||
|
||||||
|
audio_format == NULL ||
|
||||||
|
audio_format_equals(audio_format,
|
||||||
|
&input_audio_format));
|
||||||
|
|
||||||
if (audio_format != NULL)
|
if (audio_format != NULL)
|
||||||
input_audio_format = *audio_format;
|
input_audio_format = *audio_format;
|
||||||
|
|
||||||
@@ -257,6 +277,121 @@ audio_output_all_open(const struct audio_format *audio_format)
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has the specified audio output already consumed this chunk?
|
||||||
|
*/
|
||||||
|
static bool
|
||||||
|
chunk_is_consumed_in(const struct audio_output *ao,
|
||||||
|
const struct music_chunk *chunk)
|
||||||
|
{
|
||||||
|
if (ao->chunk == NULL)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
assert(chunk == ao->chunk || music_pipe_contains(g_mp, ao->chunk));
|
||||||
|
|
||||||
|
if (chunk != ao->chunk) {
|
||||||
|
assert(chunk->next != NULL);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ao->chunk_finished && chunk->next == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has this chunk been consumed by all audio outputs?
|
||||||
|
*/
|
||||||
|
static bool
|
||||||
|
chunk_is_consumed(const struct music_chunk *chunk)
|
||||||
|
{
|
||||||
|
for (unsigned i = 0; i < num_audio_outputs; ++i) {
|
||||||
|
const struct audio_output *ao = &audio_outputs[i];
|
||||||
|
bool consumed;
|
||||||
|
|
||||||
|
if (!ao->open)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
g_mutex_lock(ao->mutex);
|
||||||
|
consumed = chunk_is_consumed_in(ao, chunk);
|
||||||
|
g_mutex_unlock(ao->mutex);
|
||||||
|
|
||||||
|
if (!consumed)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There's only one chunk left in the pipe (#g_mp), and all audio
|
||||||
|
* outputs have consumed it already. Clear the reference.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
clear_tail_chunk(const struct music_chunk *chunk, bool *locked)
|
||||||
|
{
|
||||||
|
assert(chunk->next == NULL);
|
||||||
|
assert(music_pipe_contains(g_mp, chunk));
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < num_audio_outputs; ++i) {
|
||||||
|
struct audio_output *ao = &audio_outputs[i];
|
||||||
|
|
||||||
|
locked[i] = ao->open;
|
||||||
|
|
||||||
|
if (!locked[i])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
/* this mutex will be unlocked by the caller when it's
|
||||||
|
ready */
|
||||||
|
g_mutex_lock(ao->mutex);
|
||||||
|
|
||||||
|
assert(ao->chunk == chunk);
|
||||||
|
assert(ao->chunk_finished);
|
||||||
|
ao->chunk = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned
|
||||||
|
audio_output_all_check(void)
|
||||||
|
{
|
||||||
|
const struct music_chunk *chunk;
|
||||||
|
bool is_tail;
|
||||||
|
struct music_chunk *shifted;
|
||||||
|
bool locked[num_audio_outputs];
|
||||||
|
|
||||||
|
assert(g_music_buffer != NULL);
|
||||||
|
assert(g_mp != NULL);
|
||||||
|
|
||||||
|
while ((chunk = music_pipe_peek(g_mp)) != NULL) {
|
||||||
|
assert(music_pipe_size(g_mp) > 0);
|
||||||
|
|
||||||
|
if (!chunk_is_consumed(chunk))
|
||||||
|
/* at least one output is not finished playing
|
||||||
|
this chunk */
|
||||||
|
return music_pipe_size(g_mp) - 1;
|
||||||
|
|
||||||
|
is_tail = chunk->next == NULL;
|
||||||
|
if (is_tail)
|
||||||
|
/* this is the tail of the pipe - clear the
|
||||||
|
chunk reference in all outputs */
|
||||||
|
clear_tail_chunk(chunk, locked);
|
||||||
|
|
||||||
|
/* remove the chunk from the pipe */
|
||||||
|
shifted = music_pipe_shift(g_mp);
|
||||||
|
assert(shifted == chunk);
|
||||||
|
|
||||||
|
if (is_tail)
|
||||||
|
/* unlock all audio outputs which were locked
|
||||||
|
by clear_tail_chunk() */
|
||||||
|
for (unsigned i = 0; i < num_audio_outputs; ++i)
|
||||||
|
if (locked[i])
|
||||||
|
g_mutex_unlock(audio_outputs[i].mutex);
|
||||||
|
|
||||||
|
/* return the chunk to the buffer */
|
||||||
|
music_buffer_return(g_music_buffer, shifted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
audio_output_all_pause(void)
|
audio_output_all_pause(void)
|
||||||
{
|
{
|
||||||
@@ -278,12 +413,19 @@ audio_output_all_cancel(void)
|
|||||||
|
|
||||||
audio_output_all_update();
|
audio_output_all_update();
|
||||||
|
|
||||||
|
/* send the cancel() command to all audio outputs */
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i) {
|
for (i = 0; i < num_audio_outputs; ++i) {
|
||||||
if (audio_output_is_open(&audio_outputs[i]))
|
if (audio_output_is_open(&audio_outputs[i]))
|
||||||
audio_output_cancel(&audio_outputs[i]);
|
audio_output_cancel(&audio_outputs[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
audio_output_wait_all();
|
audio_output_wait_all();
|
||||||
|
|
||||||
|
/* clear the music pipe and return all chunks to the buffer */
|
||||||
|
|
||||||
|
if (g_mp != NULL)
|
||||||
|
music_pipe_clear(g_mp, g_music_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -293,16 +435,14 @@ audio_output_all_close(void)
|
|||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
for (i = 0; i < num_audio_outputs; ++i)
|
||||||
audio_output_close(&audio_outputs[i]);
|
audio_output_close(&audio_outputs[i]);
|
||||||
}
|
|
||||||
|
if (g_mp != NULL) {
|
||||||
void
|
assert(g_music_buffer != NULL);
|
||||||
audio_output_all_tag(const struct tag *tag)
|
|
||||||
{
|
music_pipe_clear(g_mp, g_music_buffer);
|
||||||
unsigned int i;
|
music_pipe_free(g_mp);
|
||||||
|
g_mp = NULL;
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
}
|
||||||
if (audio_output_is_open(&audio_outputs[i]))
|
|
||||||
audio_output_send_tag(&audio_outputs[i], tag);
|
g_music_buffer = NULL;
|
||||||
|
|
||||||
audio_output_wait_all();
|
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,8 @@
|
|||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
|
||||||
struct audio_format;
|
struct audio_format;
|
||||||
struct tag;
|
struct music_buffer;
|
||||||
|
struct music_chunk;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global initialization: load audio outputs from the configuration
|
* Global initialization: load audio outputs from the configuration
|
||||||
@@ -68,10 +69,13 @@ audio_output_find(const char *name);
|
|||||||
*
|
*
|
||||||
* @param audio_format the preferred audio format, or NULL to reuse
|
* @param audio_format the preferred audio format, or NULL to reuse
|
||||||
* the previous format
|
* the previous format
|
||||||
|
* @param buffer the #music_buffer where consumed #music_chunk objects
|
||||||
|
* should be returned
|
||||||
* @return true on success, false on failure
|
* @return true on success, false on failure
|
||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
audio_output_all_open(const struct audio_format *audio_format);
|
audio_output_all_open(const struct audio_format *audio_format,
|
||||||
|
struct music_buffer *buffer);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes all audio outputs.
|
* Closes all audio outputs.
|
||||||
@@ -80,19 +84,24 @@ void
|
|||||||
audio_output_all_close(void);
|
audio_output_all_close(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a chunk of audio data.
|
* Enqueue a #music_chunk object for playing, i.e. pushes it to a
|
||||||
|
* #music_pipe.
|
||||||
*
|
*
|
||||||
|
* @param chunk the #music_chunk object to be played
|
||||||
* @return true on success, false if no audio output was able to play
|
* @return true on success, false if no audio output was able to play
|
||||||
* (all closed then)
|
* (all closed then)
|
||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
audio_output_all_play(const char *data, size_t size);
|
audio_output_all_play(struct music_chunk *chunk);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send metadata for the next chunk.
|
* Checks if the output devices have drained their music pipe, and
|
||||||
|
* returns the consumed music chunks to the #music_buffer.
|
||||||
|
*
|
||||||
|
* @return the number of chunks to play left in the #music_pipe
|
||||||
*/
|
*/
|
||||||
void
|
unsigned
|
||||||
audio_output_all_tag(const struct tag *tag);
|
audio_output_all_check(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Puts all audio outputs into pause mode. Most implementations will
|
* Puts all audio outputs into pause mode. Most implementations will
|
||||||
|
@@ -57,8 +57,11 @@ static void ao_command_async(struct audio_output *ao,
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
audio_output_open(struct audio_output *ao,
|
audio_output_open(struct audio_output *ao,
|
||||||
const struct audio_format *audio_format)
|
const struct audio_format *audio_format,
|
||||||
|
const struct music_pipe *mp)
|
||||||
{
|
{
|
||||||
|
assert(mp != NULL);
|
||||||
|
|
||||||
if (ao->fail_timer != NULL) {
|
if (ao->fail_timer != NULL) {
|
||||||
g_timer_destroy(ao->fail_timer);
|
g_timer_destroy(ao->fail_timer);
|
||||||
ao->fail_timer = NULL;
|
ao->fail_timer = NULL;
|
||||||
@@ -66,10 +69,13 @@ audio_output_open(struct audio_output *ao,
|
|||||||
|
|
||||||
if (ao->open &&
|
if (ao->open &&
|
||||||
audio_format_equals(audio_format, &ao->in_audio_format)) {
|
audio_format_equals(audio_format, &ao->in_audio_format)) {
|
||||||
|
assert(ao->pipe == mp);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ao->in_audio_format = *audio_format;
|
ao->in_audio_format = *audio_format;
|
||||||
|
ao->chunk = NULL;
|
||||||
|
|
||||||
if (audio_format_defined(&ao->config_audio_format)) {
|
if (audio_format_defined(&ao->config_audio_format)) {
|
||||||
/* copy config_audio_format to out_audio_format only if the
|
/* copy config_audio_format to out_audio_format only if the
|
||||||
@@ -85,6 +91,8 @@ audio_output_open(struct audio_output *ao,
|
|||||||
audio_output_close(ao);
|
audio_output_close(ao);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ao->pipe = mp;
|
||||||
|
|
||||||
if (ao->thread == NULL)
|
if (ao->thread == NULL)
|
||||||
audio_output_thread_start(ao);
|
audio_output_thread_start(ao);
|
||||||
|
|
||||||
@@ -96,12 +104,15 @@ audio_output_open(struct audio_output *ao,
|
|||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_update(struct audio_output *ao,
|
audio_output_update(struct audio_output *ao,
|
||||||
const struct audio_format *audio_format)
|
const struct audio_format *audio_format,
|
||||||
|
const struct music_pipe *mp)
|
||||||
{
|
{
|
||||||
|
assert(mp != NULL);
|
||||||
|
|
||||||
if (ao->enabled) {
|
if (ao->enabled) {
|
||||||
if (ao->fail_timer == NULL ||
|
if (ao->fail_timer == NULL ||
|
||||||
g_timer_elapsed(ao->fail_timer, NULL) > REOPEN_AFTER)
|
g_timer_elapsed(ao->fail_timer, NULL) > REOPEN_AFTER)
|
||||||
return audio_output_open(ao, audio_format);
|
return audio_output_open(ao, audio_format, mp);
|
||||||
} else if (audio_output_is_open(ao))
|
} else if (audio_output_is_open(ao))
|
||||||
audio_output_close(ao);
|
audio_output_close(ao);
|
||||||
|
|
||||||
@@ -115,16 +126,12 @@ audio_output_signal(struct audio_output *ao)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
audio_output_play(struct audio_output *ao, const void *chunk, size_t size)
|
audio_output_play(struct audio_output *ao)
|
||||||
{
|
{
|
||||||
assert(size > 0);
|
|
||||||
|
|
||||||
if (!ao->open)
|
if (!ao->open)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ao->args.play.data = chunk;
|
notify_signal(&ao->notify);
|
||||||
ao->args.play.size = size;
|
|
||||||
ao_command_async(ao, AO_COMMAND_PLAY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void audio_output_pause(struct audio_output *ao)
|
void audio_output_pause(struct audio_output *ao)
|
||||||
@@ -163,14 +170,5 @@ void audio_output_finish(struct audio_output *ao)
|
|||||||
ao_plugin_finish(ao->plugin, ao->data);
|
ao_plugin_finish(ao->plugin, ao->data);
|
||||||
|
|
||||||
notify_deinit(&ao->notify);
|
notify_deinit(&ao->notify);
|
||||||
}
|
g_mutex_free(ao->mutex);
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_send_tag(struct audio_output *ao, const struct tag *tag)
|
|
||||||
{
|
|
||||||
if (ao->plugin->send_tag == NULL)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ao->args.tag = tag;
|
|
||||||
ao_command_async(ao, AO_COMMAND_SEND_TAG);
|
|
||||||
}
|
}
|
||||||
|
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
struct audio_output;
|
struct audio_output;
|
||||||
struct audio_format;
|
struct audio_format;
|
||||||
struct tag;
|
|
||||||
struct config_param;
|
struct config_param;
|
||||||
|
struct music_pipe;
|
||||||
|
|
||||||
static inline GQuark
|
static inline GQuark
|
||||||
audio_output_quark(void)
|
audio_output_quark(void)
|
||||||
@@ -46,7 +46,8 @@ audio_output_init(struct audio_output *ao, const struct config_param *param,
|
|||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
audio_output_update(struct audio_output *ao,
|
audio_output_update(struct audio_output *ao,
|
||||||
const struct audio_format *audio_format);
|
const struct audio_format *audio_format,
|
||||||
|
const struct music_pipe *mp);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wakes up the audio output thread. This is part of a workaround for
|
* Wakes up the audio output thread. This is part of a workaround for
|
||||||
@@ -57,14 +58,12 @@ void
|
|||||||
audio_output_signal(struct audio_output *ao);
|
audio_output_signal(struct audio_output *ao);
|
||||||
|
|
||||||
void
|
void
|
||||||
audio_output_play(struct audio_output *ao, const void *chunk, size_t size);
|
audio_output_play(struct audio_output *ao);
|
||||||
|
|
||||||
void audio_output_pause(struct audio_output *ao);
|
void audio_output_pause(struct audio_output *ao);
|
||||||
|
|
||||||
void audio_output_cancel(struct audio_output *ao);
|
void audio_output_cancel(struct audio_output *ao);
|
||||||
void audio_output_close(struct audio_output *ao);
|
void audio_output_close(struct audio_output *ao);
|
||||||
void audio_output_finish(struct audio_output *ao);
|
void audio_output_finish(struct audio_output *ao);
|
||||||
void
|
|
||||||
audio_output_send_tag(struct audio_output *ao, const struct tag *tag);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@@ -124,6 +124,7 @@ audio_output_init(struct audio_output *ao, const struct config_param *param,
|
|||||||
ao->thread = NULL;
|
ao->thread = NULL;
|
||||||
notify_init(&ao->notify);
|
notify_init(&ao->notify);
|
||||||
ao->command = AO_COMMAND_NONE;
|
ao->command = AO_COMMAND_NONE;
|
||||||
|
ao->mutex = g_mutex_new();
|
||||||
|
|
||||||
ao->data = ao_plugin_init(plugin,
|
ao->data = ao_plugin_init(plugin,
|
||||||
format ? &ao->config_audio_format : NULL,
|
format ? &ao->config_audio_format : NULL,
|
||||||
|
@@ -30,10 +30,8 @@ enum audio_output_command {
|
|||||||
AO_COMMAND_NONE = 0,
|
AO_COMMAND_NONE = 0,
|
||||||
AO_COMMAND_OPEN,
|
AO_COMMAND_OPEN,
|
||||||
AO_COMMAND_CLOSE,
|
AO_COMMAND_CLOSE,
|
||||||
AO_COMMAND_PLAY,
|
|
||||||
AO_COMMAND_PAUSE,
|
AO_COMMAND_PAUSE,
|
||||||
AO_COMMAND_CANCEL,
|
AO_COMMAND_CANCEL,
|
||||||
AO_COMMAND_SEND_TAG,
|
|
||||||
AO_COMMAND_KILL
|
AO_COMMAND_KILL
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,16 +108,27 @@ struct audio_output {
|
|||||||
enum audio_output_command command;
|
enum audio_output_command command;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Command arguments, depending on the command.
|
* The music pipe which provides music chunks to be played.
|
||||||
*/
|
*/
|
||||||
union {
|
const struct music_pipe *pipe;
|
||||||
struct {
|
|
||||||
const void *data;
|
|
||||||
size_t size;
|
|
||||||
} play;
|
|
||||||
|
|
||||||
const struct tag *tag;
|
/**
|
||||||
} args;
|
* This mutex protects #chunk and #chunk_finished.
|
||||||
|
*/
|
||||||
|
GMutex *mutex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The #music_chunk which is currently being played. All
|
||||||
|
* chunks before this one may be returned to the
|
||||||
|
* #music_buffer, because they are not going to be used by
|
||||||
|
* this output anymore.
|
||||||
|
*/
|
||||||
|
const struct music_chunk *chunk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has the output finished playing #chunk?
|
||||||
|
*/
|
||||||
|
bool chunk_finished;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -19,6 +19,8 @@
|
|||||||
#include "output_thread.h"
|
#include "output_thread.h"
|
||||||
#include "output_api.h"
|
#include "output_api.h"
|
||||||
#include "output_internal.h"
|
#include "output_internal.h"
|
||||||
|
#include "chunk.h"
|
||||||
|
#include "pipe.h"
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
|
||||||
@@ -41,6 +43,12 @@ ao_close(struct audio_output *ao)
|
|||||||
{
|
{
|
||||||
assert(ao->open);
|
assert(ao->open);
|
||||||
|
|
||||||
|
ao->pipe = NULL;
|
||||||
|
|
||||||
|
g_mutex_lock(ao->mutex);
|
||||||
|
ao->chunk = NULL;
|
||||||
|
g_mutex_unlock(ao->mutex);
|
||||||
|
|
||||||
ao_plugin_close(ao->plugin, ao->data);
|
ao_plugin_close(ao->plugin, ao->data);
|
||||||
pcm_convert_deinit(&ao->convert_state);
|
pcm_convert_deinit(&ao->convert_state);
|
||||||
ao->open = false;
|
ao->open = false;
|
||||||
@@ -48,16 +56,25 @@ ao_close(struct audio_output *ao)
|
|||||||
g_debug("closed plugin=%s name=\"%s\"", ao->plugin->name, ao->name);
|
g_debug("closed plugin=%s name=\"%s\"", ao->plugin->name, ao->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ao_play(struct audio_output *ao)
|
static bool
|
||||||
|
ao_play_chunk(struct audio_output *ao, const struct music_chunk *chunk)
|
||||||
{
|
{
|
||||||
const char *data = ao->args.play.data;
|
const char *data = chunk->data;
|
||||||
size_t size = ao->args.play.size;
|
size_t size = chunk->length;
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
|
|
||||||
assert(size > 0);
|
assert(!music_chunk_is_empty(chunk));
|
||||||
|
assert(music_chunk_check_format(chunk, &ao->in_audio_format));
|
||||||
assert(size % audio_format_frame_size(&ao->in_audio_format) == 0);
|
assert(size % audio_format_frame_size(&ao->in_audio_format) == 0);
|
||||||
|
|
||||||
if (!audio_format_equals(&ao->in_audio_format, &ao->out_audio_format)) {
|
if (chunk->tag != NULL)
|
||||||
|
ao_plugin_send_tag(ao->plugin, ao->data, chunk->tag);
|
||||||
|
|
||||||
|
if (size == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!audio_format_equals(&ao->in_audio_format,
|
||||||
|
&ao->out_audio_format)) {
|
||||||
data = pcm_convert(&ao->convert_state,
|
data = pcm_convert(&ao->convert_state,
|
||||||
&ao->in_audio_format, data, size,
|
&ao->in_audio_format, data, size,
|
||||||
&ao->out_audio_format, &size);
|
&ao->out_audio_format, &size);
|
||||||
@@ -67,7 +84,7 @@ static void ao_play(struct audio_output *ao)
|
|||||||
investigated further, but for now, do this check as
|
investigated further, but for now, do this check as
|
||||||
a workaround: */
|
a workaround: */
|
||||||
if (data == NULL)
|
if (data == NULL)
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (size > 0) {
|
while (size > 0) {
|
||||||
@@ -87,7 +104,7 @@ static void ao_play(struct audio_output *ao)
|
|||||||
/* don't automatically reopen this device for
|
/* don't automatically reopen this device for
|
||||||
10 seconds */
|
10 seconds */
|
||||||
ao->fail_timer = g_timer_new();
|
ao->fail_timer = g_timer_new();
|
||||||
break;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(nbytes <= size);
|
assert(nbytes <= size);
|
||||||
@@ -97,7 +114,46 @@ static void ao_play(struct audio_output *ao)
|
|||||||
size -= nbytes;
|
size -= nbytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
ao_command_finished(ao);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ao_play(struct audio_output *ao)
|
||||||
|
{
|
||||||
|
bool success;
|
||||||
|
const struct music_chunk *chunk;
|
||||||
|
|
||||||
|
assert(ao->pipe != NULL);
|
||||||
|
|
||||||
|
g_mutex_lock(ao->mutex);
|
||||||
|
chunk = ao->chunk;
|
||||||
|
if (chunk != NULL)
|
||||||
|
/* continue the previous play() call */
|
||||||
|
chunk = chunk->next;
|
||||||
|
else
|
||||||
|
chunk = music_pipe_peek(ao->pipe);
|
||||||
|
ao->chunk_finished = false;
|
||||||
|
|
||||||
|
while (chunk != NULL && ao->command == AO_COMMAND_NONE) {
|
||||||
|
assert(!ao->chunk_finished);
|
||||||
|
|
||||||
|
ao->chunk = chunk;
|
||||||
|
g_mutex_unlock(ao->mutex);
|
||||||
|
|
||||||
|
success = ao_play_chunk(ao, chunk);
|
||||||
|
|
||||||
|
g_mutex_lock(ao->mutex);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
assert(ao->chunk == NULL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(ao->chunk == chunk);
|
||||||
|
chunk = chunk->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
ao->chunk_finished = true;
|
||||||
|
g_mutex_unlock(ao->mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ao_pause(struct audio_output *ao)
|
static void ao_pause(struct audio_output *ao)
|
||||||
@@ -130,6 +186,8 @@ static gpointer audio_output_task(gpointer arg)
|
|||||||
case AO_COMMAND_OPEN:
|
case AO_COMMAND_OPEN:
|
||||||
assert(!ao->open);
|
assert(!ao->open);
|
||||||
assert(ao->fail_timer == NULL);
|
assert(ao->fail_timer == NULL);
|
||||||
|
assert(ao->pipe != NULL);
|
||||||
|
assert(ao->chunk == NULL);
|
||||||
|
|
||||||
error = NULL;
|
error = NULL;
|
||||||
ret = ao_plugin_open(ao->plugin, ao->data,
|
ret = ao_plugin_open(ao->plugin, ao->data,
|
||||||
@@ -170,35 +228,40 @@ static gpointer audio_output_task(gpointer arg)
|
|||||||
|
|
||||||
case AO_COMMAND_CLOSE:
|
case AO_COMMAND_CLOSE:
|
||||||
assert(ao->open);
|
assert(ao->open);
|
||||||
|
assert(ao->pipe != NULL);
|
||||||
|
|
||||||
|
ao->pipe = NULL;
|
||||||
|
ao->chunk = NULL;
|
||||||
|
|
||||||
ao_plugin_cancel(ao->plugin, ao->data);
|
ao_plugin_cancel(ao->plugin, ao->data);
|
||||||
ao_close(ao);
|
ao_close(ao);
|
||||||
ao_command_finished(ao);
|
ao_command_finished(ao);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AO_COMMAND_PLAY:
|
|
||||||
ao_play(ao);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AO_COMMAND_PAUSE:
|
case AO_COMMAND_PAUSE:
|
||||||
ao_pause(ao);
|
ao_pause(ao);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AO_COMMAND_CANCEL:
|
case AO_COMMAND_CANCEL:
|
||||||
|
ao->chunk = NULL;
|
||||||
ao_plugin_cancel(ao->plugin, ao->data);
|
ao_plugin_cancel(ao->plugin, ao->data);
|
||||||
ao_command_finished(ao);
|
ao_command_finished(ao);
|
||||||
break;
|
|
||||||
|
|
||||||
case AO_COMMAND_SEND_TAG:
|
/* the player thread will now clear our music
|
||||||
ao_plugin_send_tag(ao->plugin, ao->data, ao->args.tag);
|
pipe - wait for a notify, to give it some
|
||||||
ao_command_finished(ao);
|
time */
|
||||||
break;
|
notify_wait(&ao->notify);
|
||||||
|
continue;
|
||||||
|
|
||||||
case AO_COMMAND_KILL:
|
case AO_COMMAND_KILL:
|
||||||
|
ao->chunk = NULL;
|
||||||
ao_command_finished(ao);
|
ao_command_finished(ao);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ao->open)
|
||||||
|
ao_play(ao);
|
||||||
|
|
||||||
notify_wait(&ao->notify);
|
notify_wait(&ao->notify);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -177,10 +177,22 @@ player_check_decoder_startup(struct player *player)
|
|||||||
return false;
|
return false;
|
||||||
} else if (!decoder_is_starting()) {
|
} else if (!decoder_is_starting()) {
|
||||||
/* the decoder is ready and ok */
|
/* the decoder is ready and ok */
|
||||||
|
|
||||||
|
if (audio_format_defined(&player->play_audio_format) &&
|
||||||
|
audio_output_all_check() > 0) {
|
||||||
|
/* the output devices havn't finished playing
|
||||||
|
all chunks yet - wait for that */
|
||||||
|
|
||||||
|
/* XXX synchronize in a better way */
|
||||||
|
g_usleep(1000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
player->decoder_starting = false;
|
player->decoder_starting = false;
|
||||||
|
|
||||||
if (!player->paused &&
|
if (!player->paused &&
|
||||||
!audio_output_all_open(&dc.out_audio_format)) {
|
!audio_output_all_open(&dc.out_audio_format,
|
||||||
|
player_buffer)) {
|
||||||
char *uri = song_get_uri(dc.next_song);
|
char *uri = song_get_uri(dc.next_song);
|
||||||
g_warning("problems opening audio device "
|
g_warning("problems opening audio device "
|
||||||
"while playing \"%s\"", uri);
|
"while playing \"%s\"", uri);
|
||||||
@@ -268,7 +280,7 @@ static void player_process_command(struct player *player)
|
|||||||
audio_output_all_pause();
|
audio_output_all_pause();
|
||||||
pc.state = PLAYER_STATE_PAUSE;
|
pc.state = PLAYER_STATE_PAUSE;
|
||||||
} else {
|
} else {
|
||||||
if (audio_output_all_open(NULL)) {
|
if (audio_output_all_open(NULL, player_buffer)) {
|
||||||
pc.state = PLAYER_STATE_PLAY;
|
pc.state = PLAYER_STATE_PLAY;
|
||||||
} else {
|
} else {
|
||||||
assert(dc.next_song == NULL || dc.next_song->url != NULL);
|
assert(dc.next_song == NULL || dc.next_song->url != NULL);
|
||||||
@@ -328,8 +340,6 @@ play_chunk(struct song *song, struct music_chunk *chunk,
|
|||||||
pc.bit_rate = chunk->bit_rate;
|
pc.bit_rate = chunk->bit_rate;
|
||||||
|
|
||||||
if (chunk->tag != NULL) {
|
if (chunk->tag != NULL) {
|
||||||
audio_output_all_tag(chunk->tag);
|
|
||||||
|
|
||||||
if (!song_is_file(song)) {
|
if (!song_is_file(song)) {
|
||||||
/* always update the tag of remote streams */
|
/* always update the tag of remote streams */
|
||||||
struct tag *old_tag = song->tag;
|
struct tag *old_tag = song->tag;
|
||||||
@@ -349,9 +359,6 @@ play_chunk(struct song *song, struct music_chunk *chunk,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chunk->length == 0)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
success = pcm_volume(chunk->data, chunk->length,
|
success = pcm_volume(chunk->data, chunk->length,
|
||||||
format, pc.software_volume);
|
format, pc.software_volume);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -362,7 +369,7 @@ play_chunk(struct song *song, struct music_chunk *chunk,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio_output_all_play(chunk->data, chunk->length)) {
|
if (!audio_output_all_play(chunk)) {
|
||||||
pc.errored_song = dc.current_song;
|
pc.errored_song = dc.current_song;
|
||||||
pc.error = PLAYER_ERROR_AUDIO;
|
pc.error = PLAYER_ERROR_AUDIO;
|
||||||
return false;
|
return false;
|
||||||
@@ -385,6 +392,15 @@ play_next_chunk(struct player *player)
|
|||||||
unsigned cross_fade_position;
|
unsigned cross_fade_position;
|
||||||
bool success;
|
bool success;
|
||||||
|
|
||||||
|
if (audio_output_all_check() >= 64) {
|
||||||
|
/* the output pipe is still large
|
||||||
|
enough, don't send another chunk */
|
||||||
|
|
||||||
|
/* XXX synchronize in a better way */
|
||||||
|
g_usleep(1000);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (player->xfade == XFADE_ENABLED &&
|
if (player->xfade == XFADE_ENABLED &&
|
||||||
dc.pipe != NULL && dc.pipe != player->pipe &&
|
dc.pipe != NULL && dc.pipe != player->pipe &&
|
||||||
(cross_fade_position = music_pipe_size(player->pipe))
|
(cross_fade_position = music_pipe_size(player->pipe))
|
||||||
@@ -440,10 +456,11 @@ play_next_chunk(struct player *player)
|
|||||||
|
|
||||||
success = play_chunk(player->song, chunk, &player->play_audio_format,
|
success = play_chunk(player->song, chunk, &player->play_audio_format,
|
||||||
player->size_to_time);
|
player->size_to_time);
|
||||||
music_buffer_return(player_buffer, chunk);
|
|
||||||
|
|
||||||
if (!success)
|
if (!success) {
|
||||||
|
music_buffer_return(player_buffer, chunk);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/* this formula should prevent that the
|
/* this formula should prevent that the
|
||||||
decoder gets woken up with each chunk; it
|
decoder gets woken up with each chunk; it
|
||||||
@@ -472,7 +489,10 @@ player_song_border(struct player *player)
|
|||||||
music_pipe_free(player->pipe);
|
music_pipe_free(player->pipe);
|
||||||
player->pipe = dc.pipe;
|
player->pipe = dc.pipe;
|
||||||
|
|
||||||
return player_wait_for_decoder(player);
|
if (!player_wait_for_decoder(player))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void do_play(void)
|
static void do_play(void)
|
||||||
@@ -488,7 +508,6 @@ static void do_play(void)
|
|||||||
.cross_fade_chunks = 0,
|
.cross_fade_chunks = 0,
|
||||||
.size_to_time = 0.0,
|
.size_to_time = 0.0,
|
||||||
};
|
};
|
||||||
static const char silence[CHUNK_SIZE];
|
|
||||||
|
|
||||||
player.pipe = music_pipe_new();
|
player.pipe = music_pipe_new();
|
||||||
|
|
||||||
@@ -585,6 +604,13 @@ static void do_play(void)
|
|||||||
|
|
||||||
if (!play_next_chunk(&player))
|
if (!play_next_chunk(&player))
|
||||||
break;
|
break;
|
||||||
|
} else if (audio_output_all_check() > 0) {
|
||||||
|
/* not enough data from decoder, but the
|
||||||
|
output thread is still busy, so it's
|
||||||
|
okay */
|
||||||
|
|
||||||
|
/* XXX synchronize in a better way */
|
||||||
|
g_usleep(10000);
|
||||||
} else if (dc.pipe != NULL && dc.pipe != player.pipe) {
|
} else if (dc.pipe != NULL && dc.pipe != player.pipe) {
|
||||||
/* at the beginning of a new song */
|
/* at the beginning of a new song */
|
||||||
|
|
||||||
@@ -593,15 +619,32 @@ static void do_play(void)
|
|||||||
} else if (decoder_is_idle()) {
|
} else if (decoder_is_idle()) {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
|
/* the decoder is too busy and hasn't provided
|
||||||
|
new PCM data in time: send silence (if the
|
||||||
|
output pipe is empty) */
|
||||||
|
struct music_chunk *chunk;
|
||||||
size_t frame_size =
|
size_t frame_size =
|
||||||
audio_format_frame_size(&player.play_audio_format);
|
audio_format_frame_size(&player.play_audio_format);
|
||||||
/* this formula ensures that we don't send
|
/* this formula ensures that we don't send
|
||||||
partial frames */
|
partial frames */
|
||||||
unsigned num_frames = CHUNK_SIZE / frame_size;
|
unsigned num_frames = CHUNK_SIZE / frame_size;
|
||||||
|
|
||||||
|
chunk = music_buffer_allocate(player_buffer);
|
||||||
|
if (chunk == NULL)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
chunk->audio_format = player.play_audio_format;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
chunk->length = num_frames * frame_size;
|
||||||
|
memset(chunk->data, 0, chunk->length);
|
||||||
|
|
||||||
/*DEBUG("waiting for decoded audio, play silence\n");*/
|
/*DEBUG("waiting for decoded audio, play silence\n");*/
|
||||||
if (!audio_output_all_play(silence, num_frames * frame_size))
|
if (!audio_output_all_play(chunk)) {
|
||||||
|
music_buffer_return(player_buffer, chunk);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,6 +680,9 @@ static gpointer player_task(G_GNUC_UNUSED gpointer arg)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case PLAYER_COMMAND_STOP:
|
case PLAYER_COMMAND_STOP:
|
||||||
|
audio_output_all_cancel();
|
||||||
|
/* fall through */
|
||||||
|
|
||||||
case PLAYER_COMMAND_SEEK:
|
case PLAYER_COMMAND_SEEK:
|
||||||
case PLAYER_COMMAND_PAUSE:
|
case PLAYER_COMMAND_PAUSE:
|
||||||
pc.next_song = NULL;
|
pc.next_song = NULL;
|
||||||
|
Reference in New Issue
Block a user