OutputAll: convert to class, move instance to class Partition
Another big chunk of code for multi-player support.
This commit is contained in:
parent
36bab6ef06
commit
f5a923b9d1
@ -961,7 +961,7 @@ OUTPUT_API_SRC = \
|
|||||||
src/output/OutputAPI.hxx \
|
src/output/OutputAPI.hxx \
|
||||||
src/output/OutputInternal.hxx \
|
src/output/OutputInternal.hxx \
|
||||||
src/output/OutputList.cxx src/output/OutputList.hxx \
|
src/output/OutputList.cxx src/output/OutputList.hxx \
|
||||||
src/output/OutputAll.cxx src/output/OutputAll.hxx \
|
src/output/MultipleOutputs.cxx src/output/MultipleOutputs.hxx \
|
||||||
src/output/OutputThread.cxx src/output/OutputThread.hxx \
|
src/output/OutputThread.cxx src/output/OutputThread.hxx \
|
||||||
src/output/OutputError.cxx src/output/OutputError.hxx \
|
src/output/OutputError.cxx src/output/OutputError.hxx \
|
||||||
src/output/OutputControl.cxx src/output/OutputControl.hxx \
|
src/output/OutputControl.cxx src/output/OutputControl.hxx \
|
||||||
@ -985,7 +985,7 @@ MIXER_API_SRC = \
|
|||||||
src/mixer/MixerList.hxx \
|
src/mixer/MixerList.hxx \
|
||||||
src/mixer/MixerControl.cxx src/mixer/MixerControl.hxx \
|
src/mixer/MixerControl.cxx src/mixer/MixerControl.hxx \
|
||||||
src/mixer/MixerType.cxx src/mixer/MixerType.hxx \
|
src/mixer/MixerType.cxx src/mixer/MixerType.hxx \
|
||||||
src/mixer/MixerAll.cxx src/mixer/MixerAll.hxx \
|
src/mixer/MixerAll.cxx \
|
||||||
src/mixer/MixerInternal.hxx
|
src/mixer/MixerInternal.hxx
|
||||||
|
|
||||||
libmixer_plugins_a_SOURCES = \
|
libmixer_plugins_a_SOURCES = \
|
||||||
|
@ -37,7 +37,6 @@
|
|||||||
#include "command/AllCommands.hxx"
|
#include "command/AllCommands.hxx"
|
||||||
#include "Partition.hxx"
|
#include "Partition.hxx"
|
||||||
#include "mixer/Volume.hxx"
|
#include "mixer/Volume.hxx"
|
||||||
#include "output/OutputAll.hxx"
|
|
||||||
#include "tag/TagConfig.hxx"
|
#include "tag/TagConfig.hxx"
|
||||||
#include "ReplayGainConfig.hxx"
|
#include "ReplayGainConfig.hxx"
|
||||||
#include "Idle.hxx"
|
#include "Idle.hxx"
|
||||||
@ -458,7 +457,7 @@ int mpd_main(int argc, char *argv[])
|
|||||||
initialize_decoder_and_player();
|
initialize_decoder_and_player();
|
||||||
volume_init();
|
volume_init();
|
||||||
initAudioConfig();
|
initAudioConfig();
|
||||||
audio_output_all_init(instance->partition->pc);
|
instance->partition->outputs.Configure(instance->partition->pc);
|
||||||
client_manager_init();
|
client_manager_init();
|
||||||
replay_gain_global_init();
|
replay_gain_global_init();
|
||||||
|
|
||||||
@ -500,7 +499,7 @@ int mpd_main(int argc, char *argv[])
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio_output_all_set_replay_gain_mode(replay_gain_get_real_mode(instance->partition->playlist.queue.random));
|
instance->partition->outputs.SetReplayGainMode(replay_gain_get_real_mode(instance->partition->playlist.queue.random));
|
||||||
|
|
||||||
if (config_get_bool(CONF_AUTO_UPDATE, false)) {
|
if (config_get_bool(CONF_AUTO_UPDATE, false)) {
|
||||||
#ifdef ENABLE_INOTIFY
|
#ifdef ENABLE_INOTIFY
|
||||||
@ -567,7 +566,6 @@ int mpd_main(int argc, char *argv[])
|
|||||||
|
|
||||||
playlist_list_global_finish();
|
playlist_list_global_finish();
|
||||||
input_stream_global_finish();
|
input_stream_global_finish();
|
||||||
audio_output_all_finish();
|
|
||||||
mapper_finish();
|
mapper_finish();
|
||||||
delete instance->partition;
|
delete instance->partition;
|
||||||
command_finish();
|
command_finish();
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "Partition.hxx"
|
#include "Partition.hxx"
|
||||||
#include "DetachedSong.hxx"
|
#include "DetachedSong.hxx"
|
||||||
|
#include "output/MultipleOutputs.hxx"
|
||||||
|
|
||||||
void
|
void
|
||||||
Partition::DatabaseModified()
|
Partition::DatabaseModified()
|
||||||
|
@ -21,9 +21,11 @@
|
|||||||
#define MPD_PARTITION_HXX
|
#define MPD_PARTITION_HXX
|
||||||
|
|
||||||
#include "Playlist.hxx"
|
#include "Playlist.hxx"
|
||||||
|
#include "output/MultipleOutputs.hxx"
|
||||||
#include "PlayerControl.hxx"
|
#include "PlayerControl.hxx"
|
||||||
|
|
||||||
struct Instance;
|
struct Instance;
|
||||||
|
class MultipleOutputs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A partition of the Music Player Daemon. It is a separate unit with
|
* A partition of the Music Player Daemon. It is a separate unit with
|
||||||
@ -34,6 +36,8 @@ struct Partition {
|
|||||||
|
|
||||||
struct playlist playlist;
|
struct playlist playlist;
|
||||||
|
|
||||||
|
MultipleOutputs outputs;
|
||||||
|
|
||||||
PlayerControl pc;
|
PlayerControl pc;
|
||||||
|
|
||||||
Partition(Instance &_instance,
|
Partition(Instance &_instance,
|
||||||
@ -41,8 +45,7 @@ struct Partition {
|
|||||||
unsigned buffer_chunks,
|
unsigned buffer_chunks,
|
||||||
unsigned buffered_before_play)
|
unsigned buffered_before_play)
|
||||||
:instance(_instance), playlist(max_length),
|
:instance(_instance), playlist(max_length),
|
||||||
pc(buffer_chunks, buffered_before_play) {
|
pc(outputs, buffer_chunks, buffered_before_play) {}
|
||||||
}
|
|
||||||
|
|
||||||
void ClearQueue() {
|
void ClearQueue() {
|
||||||
playlist.Clear(pc);
|
playlist.Clear(pc);
|
||||||
|
@ -26,9 +26,11 @@
|
|||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
|
|
||||||
PlayerControl::PlayerControl(unsigned _buffer_chunks,
|
PlayerControl::PlayerControl(MultipleOutputs &_outputs,
|
||||||
|
unsigned _buffer_chunks,
|
||||||
unsigned _buffered_before_play)
|
unsigned _buffered_before_play)
|
||||||
:buffer_chunks(_buffer_chunks),
|
:outputs(_outputs),
|
||||||
|
buffer_chunks(_buffer_chunks),
|
||||||
buffered_before_play(_buffered_before_play),
|
buffered_before_play(_buffered_before_play),
|
||||||
command(PlayerCommand::NONE),
|
command(PlayerCommand::NONE),
|
||||||
state(PlayerState::STOP),
|
state(PlayerState::STOP),
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
class MultipleOutputs;
|
||||||
class DetachedSong;
|
class DetachedSong;
|
||||||
|
|
||||||
enum class PlayerState : uint8_t {
|
enum class PlayerState : uint8_t {
|
||||||
@ -91,6 +92,8 @@ struct player_status {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct PlayerControl {
|
struct PlayerControl {
|
||||||
|
MultipleOutputs &outputs;
|
||||||
|
|
||||||
unsigned buffer_chunks;
|
unsigned buffer_chunks;
|
||||||
|
|
||||||
unsigned int buffered_before_play;
|
unsigned int buffered_before_play;
|
||||||
@ -170,7 +173,8 @@ struct PlayerControl {
|
|||||||
*/
|
*/
|
||||||
bool border_pause;
|
bool border_pause;
|
||||||
|
|
||||||
PlayerControl(unsigned buffer_chunks,
|
PlayerControl(MultipleOutputs &_outputs,
|
||||||
|
unsigned buffer_chunks,
|
||||||
unsigned buffered_before_play);
|
unsigned buffered_before_play);
|
||||||
~PlayerControl();
|
~PlayerControl();
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
#include "system/FatalError.hxx"
|
#include "system/FatalError.hxx"
|
||||||
#include "CrossFade.hxx"
|
#include "CrossFade.hxx"
|
||||||
#include "PlayerControl.hxx"
|
#include "PlayerControl.hxx"
|
||||||
#include "output/OutputAll.hxx"
|
#include "output/MultipleOutputs.hxx"
|
||||||
#include "tag/Tag.hxx"
|
#include "tag/Tag.hxx"
|
||||||
#include "Idle.hxx"
|
#include "Idle.hxx"
|
||||||
#include "GlobalEvents.hxx"
|
#include "GlobalEvents.hxx"
|
||||||
@ -125,7 +125,7 @@ class Player {
|
|||||||
/**
|
/**
|
||||||
* The time stamp of the chunk most recently sent to the
|
* The time stamp of the chunk most recently sent to the
|
||||||
* output thread. This attribute is only used if
|
* output thread. This attribute is only used if
|
||||||
* audio_output_all_get_elapsed_time() didn't return a usable
|
* MultipleOutputs::GetElapsedTime() didn't return a usable
|
||||||
* value; the output thread can estimate the elapsed time more
|
* value; the output thread can estimate the elapsed time more
|
||||||
* precisely.
|
* precisely.
|
||||||
*/
|
*/
|
||||||
@ -228,8 +228,8 @@ private:
|
|||||||
bool WaitForDecoder();
|
bool WaitForDecoder();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper for audio_output_all_open(). Upon failure, it pauses the
|
* Wrapper for MultipleOutputs::Open(). Upon failure, it
|
||||||
* player.
|
* pauses the player.
|
||||||
*
|
*
|
||||||
* @return true on success
|
* @return true on success
|
||||||
*/
|
*/
|
||||||
@ -393,7 +393,7 @@ Player::OpenOutput()
|
|||||||
pc.state == PlayerState::PAUSE);
|
pc.state == PlayerState::PAUSE);
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
if (audio_output_all_open(play_audio_format, buffer, error)) {
|
if (pc.outputs.Open(play_audio_format, buffer, error)) {
|
||||||
output_open = true;
|
output_open = true;
|
||||||
paused = false;
|
paused = false;
|
||||||
|
|
||||||
@ -444,7 +444,7 @@ Player::CheckDecoderStartup()
|
|||||||
pc.Unlock();
|
pc.Unlock();
|
||||||
|
|
||||||
if (output_open &&
|
if (output_open &&
|
||||||
!audio_output_all_wait(pc, 1))
|
!pc.outputs.Wait(pc, 1))
|
||||||
/* the output devices havn't finished playing
|
/* the output devices havn't finished playing
|
||||||
all chunks yet - wait for that */
|
all chunks yet - wait for that */
|
||||||
return true;
|
return true;
|
||||||
@ -504,7 +504,7 @@ Player::SendSilence()
|
|||||||
memset(chunk->data, 0, chunk->length);
|
memset(chunk->data, 0, chunk->length);
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
if (!audio_output_all_play(chunk, error)) {
|
if (!pc.outputs.Play(chunk, error)) {
|
||||||
LogError(error);
|
LogError(error);
|
||||||
buffer.Return(chunk);
|
buffer.Return(chunk);
|
||||||
return false;
|
return false;
|
||||||
@ -582,7 +582,7 @@ Player::SeekDecoder()
|
|||||||
/* re-fill the buffer after seeking */
|
/* re-fill the buffer after seeking */
|
||||||
buffering = true;
|
buffering = true;
|
||||||
|
|
||||||
audio_output_all_cancel();
|
pc.outputs.Cancel();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -599,7 +599,7 @@ Player::ProcessCommand()
|
|||||||
|
|
||||||
case PlayerCommand::UPDATE_AUDIO:
|
case PlayerCommand::UPDATE_AUDIO:
|
||||||
pc.Unlock();
|
pc.Unlock();
|
||||||
audio_output_all_enable_disable();
|
pc.outputs.EnableDisable();
|
||||||
pc.Lock();
|
pc.Lock();
|
||||||
pc.CommandFinished();
|
pc.CommandFinished();
|
||||||
break;
|
break;
|
||||||
@ -618,7 +618,7 @@ Player::ProcessCommand()
|
|||||||
|
|
||||||
paused = !paused;
|
paused = !paused;
|
||||||
if (paused) {
|
if (paused) {
|
||||||
audio_output_all_pause();
|
pc.outputs.Pause();
|
||||||
pc.Lock();
|
pc.Lock();
|
||||||
|
|
||||||
pc.state = PlayerState::PAUSE;
|
pc.state = PlayerState::PAUSE;
|
||||||
@ -669,11 +669,11 @@ Player::ProcessCommand()
|
|||||||
case PlayerCommand::REFRESH:
|
case PlayerCommand::REFRESH:
|
||||||
if (output_open && !paused) {
|
if (output_open && !paused) {
|
||||||
pc.Unlock();
|
pc.Unlock();
|
||||||
audio_output_all_check();
|
pc.outputs.Check();
|
||||||
pc.Lock();
|
pc.Lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
pc.elapsed_time = audio_output_all_get_elapsed_time();
|
pc.elapsed_time = pc.outputs.GetElapsedTime();
|
||||||
if (pc.elapsed_time < 0.0)
|
if (pc.elapsed_time < 0.0)
|
||||||
pc.elapsed_time = elapsed_time;
|
pc.elapsed_time = elapsed_time;
|
||||||
|
|
||||||
@ -733,7 +733,7 @@ play_chunk(PlayerControl &pc,
|
|||||||
|
|
||||||
/* send the chunk to the audio outputs */
|
/* send the chunk to the audio outputs */
|
||||||
|
|
||||||
if (!audio_output_all_play(chunk, error))
|
if (!pc.outputs.Play(chunk, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
pc.total_play_time += (double)chunk->length /
|
pc.total_play_time += (double)chunk->length /
|
||||||
@ -744,7 +744,7 @@ play_chunk(PlayerControl &pc,
|
|||||||
inline bool
|
inline bool
|
||||||
Player::PlayNextChunk()
|
Player::PlayNextChunk()
|
||||||
{
|
{
|
||||||
if (!audio_output_all_wait(pc, 64))
|
if (!pc.outputs.Wait(pc, 64))
|
||||||
/* the output pipe is still large enough, don't send
|
/* the output pipe is still large enough, don't send
|
||||||
another chunk */
|
another chunk */
|
||||||
return true;
|
return true;
|
||||||
@ -883,7 +883,7 @@ Player::SongBorder()
|
|||||||
|
|
||||||
ReplacePipe(dc.pipe);
|
ReplacePipe(dc.pipe);
|
||||||
|
|
||||||
audio_output_all_song_border();
|
pc.outputs.SongBorder();
|
||||||
|
|
||||||
if (!WaitForDecoder())
|
if (!WaitForDecoder())
|
||||||
return false;
|
return false;
|
||||||
@ -933,7 +933,7 @@ Player::Run()
|
|||||||
pc.command == PlayerCommand::EXIT ||
|
pc.command == PlayerCommand::EXIT ||
|
||||||
pc.command == PlayerCommand::CLOSE_AUDIO) {
|
pc.command == PlayerCommand::CLOSE_AUDIO) {
|
||||||
pc.Unlock();
|
pc.Unlock();
|
||||||
audio_output_all_cancel();
|
pc.outputs.Cancel();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -949,7 +949,7 @@ Player::Run()
|
|||||||
/* not enough decoded buffer space yet */
|
/* not enough decoded buffer space yet */
|
||||||
|
|
||||||
if (!paused && output_open &&
|
if (!paused && output_open &&
|
||||||
audio_output_all_check() < 4 &&
|
pc.outputs.Check() < 4 &&
|
||||||
!SendSilence())
|
!SendSilence())
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -1029,7 +1029,7 @@ Player::Run()
|
|||||||
to the audio output */
|
to the audio output */
|
||||||
|
|
||||||
PlayNextChunk();
|
PlayNextChunk();
|
||||||
} else if (audio_output_all_check() > 0) {
|
} else if (pc.outputs.Check() > 0) {
|
||||||
/* not enough data from decoder, but the
|
/* not enough data from decoder, but the
|
||||||
output thread is still busy, so it's
|
output thread is still busy, so it's
|
||||||
okay */
|
okay */
|
||||||
@ -1054,7 +1054,7 @@ Player::Run()
|
|||||||
if (pipe->IsEmpty()) {
|
if (pipe->IsEmpty()) {
|
||||||
/* wait for the hardware to finish
|
/* wait for the hardware to finish
|
||||||
playback */
|
playback */
|
||||||
audio_output_all_drain();
|
pc.outputs.Drain();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else if (output_open) {
|
} else if (output_open) {
|
||||||
@ -1130,7 +1130,7 @@ player_task(void *arg)
|
|||||||
|
|
||||||
case PlayerCommand::STOP:
|
case PlayerCommand::STOP:
|
||||||
pc.Unlock();
|
pc.Unlock();
|
||||||
audio_output_all_cancel();
|
pc.outputs.Cancel();
|
||||||
pc.Lock();
|
pc.Lock();
|
||||||
|
|
||||||
/* fall through */
|
/* fall through */
|
||||||
@ -1145,7 +1145,7 @@ player_task(void *arg)
|
|||||||
case PlayerCommand::CLOSE_AUDIO:
|
case PlayerCommand::CLOSE_AUDIO:
|
||||||
pc.Unlock();
|
pc.Unlock();
|
||||||
|
|
||||||
audio_output_all_release();
|
pc.outputs.Release();
|
||||||
|
|
||||||
pc.Lock();
|
pc.Lock();
|
||||||
pc.CommandFinished();
|
pc.CommandFinished();
|
||||||
@ -1156,7 +1156,7 @@ player_task(void *arg)
|
|||||||
|
|
||||||
case PlayerCommand::UPDATE_AUDIO:
|
case PlayerCommand::UPDATE_AUDIO:
|
||||||
pc.Unlock();
|
pc.Unlock();
|
||||||
audio_output_all_enable_disable();
|
pc.outputs.EnableDisable();
|
||||||
pc.Lock();
|
pc.Lock();
|
||||||
pc.CommandFinished();
|
pc.CommandFinished();
|
||||||
break;
|
break;
|
||||||
@ -1166,7 +1166,7 @@ player_task(void *arg)
|
|||||||
|
|
||||||
dc.Quit();
|
dc.Quit();
|
||||||
|
|
||||||
audio_output_all_close();
|
pc.outputs.Close();
|
||||||
|
|
||||||
player_command_finished(pc);
|
player_command_finished(pc);
|
||||||
return;
|
return;
|
||||||
|
@ -75,7 +75,7 @@ StateFile::Write()
|
|||||||
}
|
}
|
||||||
|
|
||||||
save_sw_volume_state(fp);
|
save_sw_volume_state(fp);
|
||||||
audio_output_state_save(fp);
|
audio_output_state_save(fp, partition.outputs);
|
||||||
playlist_state_save(fp, partition.playlist, partition.pc);
|
playlist_state_save(fp, partition.playlist, partition.pc);
|
||||||
|
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
@ -99,8 +99,8 @@ StateFile::Read()
|
|||||||
|
|
||||||
const char *line;
|
const char *line;
|
||||||
while ((line = file.ReadLine()) != NULL) {
|
while ((line = file.ReadLine()) != NULL) {
|
||||||
success = read_sw_volume_state(line) ||
|
success = read_sw_volume_state(line, partition.outputs) ||
|
||||||
audio_output_state_read(line) ||
|
audio_output_state_read(line, partition.outputs) ||
|
||||||
playlist_state_restore(line, file, partition.playlist,
|
playlist_state_restore(line, file, partition.playlist,
|
||||||
partition.pc);
|
partition.pc);
|
||||||
if (!success)
|
if (!success)
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
#include "db/PlaylistVector.hxx"
|
#include "db/PlaylistVector.hxx"
|
||||||
#include "client/ClientFile.hxx"
|
#include "client/ClientFile.hxx"
|
||||||
#include "client/Client.hxx"
|
#include "client/Client.hxx"
|
||||||
|
#include "Partition.hxx"
|
||||||
#include "Idle.hxx"
|
#include "Idle.hxx"
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
@ -254,7 +255,7 @@ handle_setvol(Client &client, gcc_unused int argc, char *argv[])
|
|||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
success = volume_level_change(level);
|
success = volume_level_change(client.partition.outputs, level);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
command_error(client, ACK_ERROR_SYSTEM,
|
command_error(client, ACK_ERROR_SYSTEM,
|
||||||
"problems setting volume");
|
"problems setting volume");
|
||||||
@ -276,7 +277,7 @@ handle_volume(Client &client, gcc_unused int argc, char *argv[])
|
|||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int old_volume = volume_level_get();
|
const int old_volume = volume_level_get(client.partition.outputs);
|
||||||
if (old_volume < 0) {
|
if (old_volume < 0) {
|
||||||
command_error(client, ACK_ERROR_SYSTEM, "No mixer");
|
command_error(client, ACK_ERROR_SYSTEM, "No mixer");
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
@ -288,7 +289,8 @@ handle_volume(Client &client, gcc_unused int argc, char *argv[])
|
|||||||
else if (new_volume > 100)
|
else if (new_volume > 100)
|
||||||
new_volume = 100;
|
new_volume = 100;
|
||||||
|
|
||||||
if (new_volume != old_volume && !volume_level_change(new_volume)) {
|
if (new_volume != old_volume &&
|
||||||
|
!volume_level_change(client.partition.outputs, new_volume)) {
|
||||||
command_error(client, ACK_ERROR_SYSTEM,
|
command_error(client, ACK_ERROR_SYSTEM,
|
||||||
"problems setting volume");
|
"problems setting volume");
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
|
@ -23,18 +23,17 @@
|
|||||||
#include "output/OutputCommand.hxx"
|
#include "output/OutputCommand.hxx"
|
||||||
#include "protocol/Result.hxx"
|
#include "protocol/Result.hxx"
|
||||||
#include "protocol/ArgParser.hxx"
|
#include "protocol/ArgParser.hxx"
|
||||||
|
#include "client/Client.hxx"
|
||||||
|
#include "Partition.hxx"
|
||||||
|
|
||||||
CommandResult
|
CommandResult
|
||||||
handle_enableoutput(Client &client, gcc_unused int argc, char *argv[])
|
handle_enableoutput(Client &client, gcc_unused int argc, char *argv[])
|
||||||
{
|
{
|
||||||
unsigned device;
|
unsigned device;
|
||||||
bool ret;
|
|
||||||
|
|
||||||
if (!check_unsigned(client, &device, argv[1]))
|
if (!check_unsigned(client, &device, argv[1]))
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
|
|
||||||
ret = audio_output_enable_index(device);
|
if (!audio_output_enable_index(client.partition.outputs, device)) {
|
||||||
if (!ret) {
|
|
||||||
command_error(client, ACK_ERROR_NO_EXIST,
|
command_error(client, ACK_ERROR_NO_EXIST,
|
||||||
"No such audio output");
|
"No such audio output");
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
@ -47,13 +46,10 @@ CommandResult
|
|||||||
handle_disableoutput(Client &client, gcc_unused int argc, char *argv[])
|
handle_disableoutput(Client &client, gcc_unused int argc, char *argv[])
|
||||||
{
|
{
|
||||||
unsigned device;
|
unsigned device;
|
||||||
bool ret;
|
|
||||||
|
|
||||||
if (!check_unsigned(client, &device, argv[1]))
|
if (!check_unsigned(client, &device, argv[1]))
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
|
|
||||||
ret = audio_output_disable_index(device);
|
if (!audio_output_disable_index(client.partition.outputs, device)) {
|
||||||
if (!ret) {
|
|
||||||
command_error(client, ACK_ERROR_NO_EXIST,
|
command_error(client, ACK_ERROR_NO_EXIST,
|
||||||
"No such audio output");
|
"No such audio output");
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
@ -69,7 +65,7 @@ handle_toggleoutput(Client &client, gcc_unused int argc, char *argv[])
|
|||||||
if (!check_unsigned(client, &device, argv[1]))
|
if (!check_unsigned(client, &device, argv[1]))
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
|
|
||||||
if (!audio_output_toggle_index(device)) {
|
if (!audio_output_toggle_index(client.partition.outputs, device)) {
|
||||||
command_error(client, ACK_ERROR_NO_EXIST,
|
command_error(client, ACK_ERROR_NO_EXIST,
|
||||||
"No such audio output");
|
"No such audio output");
|
||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
@ -82,7 +78,7 @@ CommandResult
|
|||||||
handle_devices(Client &client,
|
handle_devices(Client &client,
|
||||||
gcc_unused int argc, gcc_unused char *argv[])
|
gcc_unused int argc, gcc_unused char *argv[])
|
||||||
{
|
{
|
||||||
printAudioDevices(client);
|
printAudioDevices(client, client.partition.outputs);
|
||||||
|
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
#include "db/update/UpdateGlue.hxx"
|
#include "db/update/UpdateGlue.hxx"
|
||||||
#include "client/Client.hxx"
|
#include "client/Client.hxx"
|
||||||
#include "mixer/Volume.hxx"
|
#include "mixer/Volume.hxx"
|
||||||
#include "output/OutputAll.hxx"
|
|
||||||
#include "Partition.hxx"
|
#include "Partition.hxx"
|
||||||
#include "protocol/Result.hxx"
|
#include "protocol/Result.hxx"
|
||||||
#include "protocol/ArgParser.hxx"
|
#include "protocol/ArgParser.hxx"
|
||||||
@ -140,7 +139,7 @@ handle_status(Client &client,
|
|||||||
COMMAND_STATUS_PLAYLIST_LENGTH ": %i\n"
|
COMMAND_STATUS_PLAYLIST_LENGTH ": %i\n"
|
||||||
COMMAND_STATUS_MIXRAMPDB ": %f\n"
|
COMMAND_STATUS_MIXRAMPDB ": %f\n"
|
||||||
COMMAND_STATUS_STATE ": %s\n",
|
COMMAND_STATUS_STATE ": %s\n",
|
||||||
volume_level_get(),
|
volume_level_get(client.partition.outputs),
|
||||||
playlist.GetRepeat(),
|
playlist.GetRepeat(),
|
||||||
playlist.GetRandom(),
|
playlist.GetRandom(),
|
||||||
playlist.GetSingle(),
|
playlist.GetSingle(),
|
||||||
@ -277,7 +276,7 @@ handle_random(Client &client, gcc_unused int argc, char *argv[])
|
|||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
|
|
||||||
client.partition.SetRandom(status);
|
client.partition.SetRandom(status);
|
||||||
audio_output_all_set_replay_gain_mode(replay_gain_get_real_mode(client.partition.GetRandom()));
|
client.partition.outputs.SetReplayGainMode(replay_gain_get_real_mode(client.partition.GetRandom()));
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -379,8 +378,7 @@ handle_replay_gain_mode(Client &client,
|
|||||||
return CommandResult::ERROR;
|
return CommandResult::ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio_output_all_set_replay_gain_mode(replay_gain_get_real_mode(client.playlist.queue.random));
|
client.partition.outputs.SetReplayGainMode(replay_gain_get_real_mode(client.playlist.queue.random));
|
||||||
|
|
||||||
return CommandResult::OK;
|
return CommandResult::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,11 +18,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "MixerAll.hxx"
|
#include "output/MultipleOutputs.hxx"
|
||||||
#include "MixerControl.hxx"
|
#include "MixerControl.hxx"
|
||||||
#include "MixerInternal.hxx"
|
#include "MixerInternal.hxx"
|
||||||
#include "MixerList.hxx"
|
#include "MixerList.hxx"
|
||||||
#include "output/OutputAll.hxx"
|
|
||||||
#include "output/OutputInternal.hxx"
|
#include "output/OutputInternal.hxx"
|
||||||
#include "pcm/Volume.hxx"
|
#include "pcm/Volume.hxx"
|
||||||
#include "util/Error.hxx"
|
#include "util/Error.hxx"
|
||||||
@ -34,39 +33,33 @@
|
|||||||
static constexpr Domain mixer_domain("mixer");
|
static constexpr Domain mixer_domain("mixer");
|
||||||
|
|
||||||
static int
|
static int
|
||||||
output_mixer_get_volume(unsigned i)
|
output_mixer_get_volume(const audio_output &ao)
|
||||||
{
|
{
|
||||||
struct audio_output *output;
|
if (!ao.enabled)
|
||||||
int volume;
|
|
||||||
|
|
||||||
assert(i < audio_output_count());
|
|
||||||
|
|
||||||
output = audio_output_get(i);
|
|
||||||
if (!output->enabled)
|
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
Mixer *mixer = output->mixer;
|
Mixer *mixer = ao.mixer;
|
||||||
if (mixer == nullptr)
|
if (mixer == nullptr)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
volume = mixer_get_volume(mixer, error);
|
int volume = mixer_get_volume(mixer, error);
|
||||||
if (volume < 0 && error.IsDefined())
|
if (volume < 0 && error.IsDefined())
|
||||||
FormatError(error,
|
FormatError(error,
|
||||||
"Failed to read mixer for '%s'",
|
"Failed to read mixer for '%s'",
|
||||||
output->name);
|
ao.name);
|
||||||
|
|
||||||
return volume;
|
return volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
mixer_all_get_volume(void)
|
MultipleOutputs::GetVolume() const
|
||||||
{
|
{
|
||||||
unsigned count = audio_output_count(), ok = 0;
|
unsigned ok = 0;
|
||||||
int volume, total = 0;
|
int total = 0;
|
||||||
|
|
||||||
for (unsigned i = 0; i < count; i++) {
|
for (auto ao : outputs) {
|
||||||
volume = output_mixer_get_volume(i);
|
int volume = output_mixer_get_volume(*ao);
|
||||||
if (volume >= 0) {
|
if (volume >= 0) {
|
||||||
total += volume;
|
total += volume;
|
||||||
++ok;
|
++ok;
|
||||||
@ -80,59 +73,47 @@ mixer_all_get_volume(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
output_mixer_set_volume(unsigned i, unsigned volume)
|
output_mixer_set_volume(audio_output &ao, unsigned volume)
|
||||||
{
|
{
|
||||||
struct audio_output *output;
|
|
||||||
bool success;
|
|
||||||
|
|
||||||
assert(i < audio_output_count());
|
|
||||||
assert(volume <= 100);
|
assert(volume <= 100);
|
||||||
|
|
||||||
output = audio_output_get(i);
|
if (!ao.enabled)
|
||||||
if (!output->enabled)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Mixer *mixer = output->mixer;
|
Mixer *mixer = ao.mixer;
|
||||||
if (mixer == nullptr)
|
if (mixer == nullptr)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
success = mixer_set_volume(mixer, volume, error);
|
bool success = mixer_set_volume(mixer, volume, error);
|
||||||
if (!success && error.IsDefined())
|
if (!success && error.IsDefined())
|
||||||
FormatError(error,
|
FormatError(error,
|
||||||
"Failed to set mixer for '%s'",
|
"Failed to set mixer for '%s'",
|
||||||
output->name);
|
ao.name);
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
mixer_all_set_volume(unsigned volume)
|
MultipleOutputs::SetVolume(unsigned volume)
|
||||||
{
|
{
|
||||||
bool success = false;
|
|
||||||
unsigned count = audio_output_count();
|
|
||||||
|
|
||||||
assert(volume <= 100);
|
assert(volume <= 100);
|
||||||
|
|
||||||
for (unsigned i = 0; i < count; i++)
|
bool success = false;
|
||||||
success = output_mixer_set_volume(i, volume)
|
for (auto ao : outputs)
|
||||||
|
success = output_mixer_set_volume(*ao, volume)
|
||||||
|| success;
|
|| success;
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
output_mixer_get_software_volume(unsigned i)
|
output_mixer_get_software_volume(const audio_output &ao)
|
||||||
{
|
{
|
||||||
struct audio_output *output;
|
if (!ao.enabled)
|
||||||
|
|
||||||
assert(i < audio_output_count());
|
|
||||||
|
|
||||||
output = audio_output_get(i);
|
|
||||||
if (!output->enabled)
|
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
Mixer *mixer = output->mixer;
|
Mixer *mixer = ao.mixer;
|
||||||
if (mixer == nullptr || !mixer->IsPlugin(software_mixer_plugin))
|
if (mixer == nullptr || !mixer->IsPlugin(software_mixer_plugin))
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
@ -140,13 +121,13 @@ output_mixer_get_software_volume(unsigned i)
|
|||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
mixer_all_get_software_volume(void)
|
MultipleOutputs::GetSoftwareVolume() const
|
||||||
{
|
{
|
||||||
unsigned count = audio_output_count(), ok = 0;
|
unsigned ok = 0;
|
||||||
int volume, total = 0;
|
int total = 0;
|
||||||
|
|
||||||
for (unsigned i = 0; i < count; i++) {
|
for (auto ao : outputs) {
|
||||||
volume = output_mixer_get_software_volume(i);
|
int volume = output_mixer_get_software_volume(*ao);
|
||||||
if (volume >= 0) {
|
if (volume >= 0) {
|
||||||
total += volume;
|
total += volume;
|
||||||
++ok;
|
++ok;
|
||||||
@ -160,16 +141,15 @@ mixer_all_get_software_volume(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
mixer_all_set_software_volume(unsigned volume)
|
MultipleOutputs::SetSoftwareVolume(unsigned volume)
|
||||||
{
|
{
|
||||||
unsigned count = audio_output_count();
|
|
||||||
|
|
||||||
assert(volume <= PCM_VOLUME_1);
|
assert(volume <= PCM_VOLUME_1);
|
||||||
|
|
||||||
for (unsigned i = 0; i < count; i++) {
|
for (auto ao : outputs) {
|
||||||
struct audio_output *output = audio_output_get(i);
|
const auto mixer = ao->mixer;
|
||||||
if (output->mixer != nullptr &&
|
|
||||||
output->mixer->plugin == &software_mixer_plugin)
|
if (mixer != nullptr &&
|
||||||
mixer_set_volume(output->mixer, volume, IgnoreError());
|
mixer->plugin == &software_mixer_plugin)
|
||||||
|
mixer_set_volume(mixer, volume, IgnoreError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2003-2014 The Music Player Daemon Project
|
|
||||||
* http://www.musicpd.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.,
|
|
||||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** \file
|
|
||||||
*
|
|
||||||
* Functions which affect the mixers of all audio outputs.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef MPD_MIXER_ALL_HXX
|
|
||||||
#define MPD_MIXER_ALL_HXX
|
|
||||||
|
|
||||||
#include "Compiler.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the average volume of all available mixers (range 0..100).
|
|
||||||
* Returns -1 if no mixer can be queried.
|
|
||||||
*/
|
|
||||||
gcc_pure
|
|
||||||
int
|
|
||||||
mixer_all_get_volume(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the volume on all available mixers.
|
|
||||||
*
|
|
||||||
* @param volume the volume (range 0..100)
|
|
||||||
* @return true on success, false on failure
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
mixer_all_set_volume(unsigned volume);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Similar to mixer_all_get_volume(), but gets the volume only for
|
|
||||||
* software mixers. See #software_mixer_plugin. This function fails
|
|
||||||
* if no software mixer is configured.
|
|
||||||
*/
|
|
||||||
gcc_pure
|
|
||||||
int
|
|
||||||
mixer_all_get_software_volume(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Similar to mixer_all_set_volume(), but sets the volume only for
|
|
||||||
* software mixers. See #software_mixer_plugin. This function cannot
|
|
||||||
* fail, because the underlying software mixers cannot fail either.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
mixer_all_set_software_volume(unsigned volume);
|
|
||||||
|
|
||||||
#endif
|
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "Volume.hxx"
|
#include "Volume.hxx"
|
||||||
#include "MixerAll.hxx"
|
#include "output/MultipleOutputs.hxx"
|
||||||
#include "Idle.hxx"
|
#include "Idle.hxx"
|
||||||
#include "GlobalEvents.hxx"
|
#include "GlobalEvents.hxx"
|
||||||
#include "util/StringUtil.hxx"
|
#include "util/StringUtil.hxx"
|
||||||
@ -59,36 +59,40 @@ void volume_init(void)
|
|||||||
GlobalEvents::Register(GlobalEvents::MIXER, mixer_event_callback);
|
GlobalEvents::Register(GlobalEvents::MIXER, mixer_event_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
int volume_level_get(void)
|
int
|
||||||
|
volume_level_get(const MultipleOutputs &outputs)
|
||||||
{
|
{
|
||||||
if (last_hardware_volume >= 0 &&
|
if (last_hardware_volume >= 0 &&
|
||||||
!hardware_volume_clock.CheckUpdate(1000))
|
!hardware_volume_clock.CheckUpdate(1000))
|
||||||
/* throttle access to hardware mixers */
|
/* throttle access to hardware mixers */
|
||||||
return last_hardware_volume;
|
return last_hardware_volume;
|
||||||
|
|
||||||
last_hardware_volume = mixer_all_get_volume();
|
last_hardware_volume = outputs.GetVolume();
|
||||||
return last_hardware_volume;
|
return last_hardware_volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool software_volume_change(unsigned volume)
|
static bool
|
||||||
|
software_volume_change(MultipleOutputs &outputs, unsigned volume)
|
||||||
{
|
{
|
||||||
assert(volume <= 100);
|
assert(volume <= 100);
|
||||||
|
|
||||||
volume_software_set = volume;
|
volume_software_set = volume;
|
||||||
mixer_all_set_software_volume(volume);
|
outputs.SetSoftwareVolume(volume);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool hardware_volume_change(unsigned volume)
|
static bool
|
||||||
|
hardware_volume_change(MultipleOutputs &outputs, unsigned volume)
|
||||||
{
|
{
|
||||||
/* reset the cache */
|
/* reset the cache */
|
||||||
last_hardware_volume = -1;
|
last_hardware_volume = -1;
|
||||||
|
|
||||||
return mixer_all_set_volume(volume);
|
return outputs.SetVolume(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool volume_level_change(unsigned volume)
|
bool
|
||||||
|
volume_level_change(MultipleOutputs &outputs, unsigned volume)
|
||||||
{
|
{
|
||||||
assert(volume <= 100);
|
assert(volume <= 100);
|
||||||
|
|
||||||
@ -96,11 +100,11 @@ bool volume_level_change(unsigned volume)
|
|||||||
|
|
||||||
idle_add(IDLE_MIXER);
|
idle_add(IDLE_MIXER);
|
||||||
|
|
||||||
return hardware_volume_change(volume);
|
return hardware_volume_change(outputs, volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
read_sw_volume_state(const char *line)
|
read_sw_volume_state(const char *line, MultipleOutputs &outputs)
|
||||||
{
|
{
|
||||||
char *end = nullptr;
|
char *end = nullptr;
|
||||||
long int sv;
|
long int sv;
|
||||||
@ -111,7 +115,7 @@ read_sw_volume_state(const char *line)
|
|||||||
line += sizeof(SW_VOLUME_STATE) - 1;
|
line += sizeof(SW_VOLUME_STATE) - 1;
|
||||||
sv = strtol(line, &end, 10);
|
sv = strtol(line, &end, 10);
|
||||||
if (*end == 0 && sv >= 0 && sv <= 100)
|
if (*end == 0 && sv >= 0 && sv <= 100)
|
||||||
software_volume_change(sv);
|
software_volume_change(outputs, sv);
|
||||||
else
|
else
|
||||||
FormatWarning(volume_domain,
|
FormatWarning(volume_domain,
|
||||||
"Can't parse software volume: %s", line);
|
"Can't parse software volume: %s", line);
|
||||||
|
@ -24,15 +24,19 @@
|
|||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
class MultipleOutputs;
|
||||||
|
|
||||||
void volume_init(void);
|
void volume_init(void);
|
||||||
|
|
||||||
gcc_pure
|
gcc_pure
|
||||||
int volume_level_get(void);
|
int
|
||||||
|
volume_level_get(const MultipleOutputs &outputs);
|
||||||
bool volume_level_change(unsigned volume);
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
read_sw_volume_state(const char *line);
|
volume_level_change(MultipleOutputs &outputs, unsigned volume);
|
||||||
|
|
||||||
|
bool
|
||||||
|
read_sw_volume_state(const char *line, MultipleOutputs &outputs);
|
||||||
|
|
||||||
void save_sw_volume_state(FILE *fp);
|
void save_sw_volume_state(FILE *fp);
|
||||||
|
|
||||||
|
477
src/output/MultipleOutputs.cxx
Normal file
477
src/output/MultipleOutputs.cxx
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2003-2014 The Music Player Daemon Project
|
||||||
|
* http://www.musicpd.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.,
|
||||||
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "MultipleOutputs.hxx"
|
||||||
|
#include "PlayerControl.hxx"
|
||||||
|
#include "OutputInternal.hxx"
|
||||||
|
#include "OutputControl.hxx"
|
||||||
|
#include "OutputError.hxx"
|
||||||
|
#include "MusicBuffer.hxx"
|
||||||
|
#include "MusicPipe.hxx"
|
||||||
|
#include "MusicChunk.hxx"
|
||||||
|
#include "system/FatalError.hxx"
|
||||||
|
#include "util/Error.hxx"
|
||||||
|
#include "config/ConfigData.hxx"
|
||||||
|
#include "config/ConfigGlobal.hxx"
|
||||||
|
#include "config/ConfigOption.hxx"
|
||||||
|
#include "notify.hxx"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
MultipleOutputs::MultipleOutputs()
|
||||||
|
:buffer(nullptr), pipe(nullptr),
|
||||||
|
elapsed_time(-1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
MultipleOutputs::~MultipleOutputs()
|
||||||
|
{
|
||||||
|
for (auto i : outputs) {
|
||||||
|
audio_output_disable(i);
|
||||||
|
audio_output_finish(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static audio_output *
|
||||||
|
LoadOutput(PlayerControl &pc, const config_param ¶m)
|
||||||
|
{
|
||||||
|
Error error;
|
||||||
|
audio_output *output = audio_output_new(param, pc, error);
|
||||||
|
if (output == nullptr) {
|
||||||
|
if (param.line > 0)
|
||||||
|
FormatFatalError("line %i: %s",
|
||||||
|
param.line,
|
||||||
|
error.GetMessage());
|
||||||
|
else
|
||||||
|
FatalError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::Configure(PlayerControl &pc)
|
||||||
|
{
|
||||||
|
const config_param *param = nullptr;
|
||||||
|
while ((param = config_get_next_param(CONF_AUDIO_OUTPUT,
|
||||||
|
param)) != nullptr) {
|
||||||
|
auto output = LoadOutput(pc, *param);
|
||||||
|
if (FindByName(output->name) != nullptr)
|
||||||
|
FormatFatalError("output devices with identical "
|
||||||
|
"names: %s", output->name);
|
||||||
|
|
||||||
|
outputs.push_back(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputs.empty()) {
|
||||||
|
/* auto-detect device */
|
||||||
|
const config_param empty;
|
||||||
|
auto output = LoadOutput(pc, empty);
|
||||||
|
outputs.push_back(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_output *
|
||||||
|
MultipleOutputs::FindByName(const char *name) const
|
||||||
|
{
|
||||||
|
for (auto i : outputs)
|
||||||
|
if (strcmp(i->name, name) == 0)
|
||||||
|
return i;
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::EnableDisable()
|
||||||
|
{
|
||||||
|
for (auto ao : outputs) {
|
||||||
|
bool enabled;
|
||||||
|
|
||||||
|
ao->mutex.lock();
|
||||||
|
enabled = ao->really_enabled;
|
||||||
|
ao->mutex.unlock();
|
||||||
|
|
||||||
|
if (ao->enabled != enabled) {
|
||||||
|
if (ao->enabled)
|
||||||
|
audio_output_enable(ao);
|
||||||
|
else
|
||||||
|
audio_output_disable(ao);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
MultipleOutputs::AllFinished() const
|
||||||
|
{
|
||||||
|
for (auto ao : outputs) {
|
||||||
|
const ScopeLock protect(ao->mutex);
|
||||||
|
if (audio_output_is_open(ao) &&
|
||||||
|
!audio_output_command_is_finished(ao))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::WaitAll()
|
||||||
|
{
|
||||||
|
while (!AllFinished())
|
||||||
|
audio_output_client_notify.Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::AllowPlay()
|
||||||
|
{
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_allow_play(ao);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
audio_output_reset_reopen(struct audio_output *ao)
|
||||||
|
{
|
||||||
|
const ScopeLock protect(ao->mutex);
|
||||||
|
|
||||||
|
ao->fail_timer.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::ResetReopen()
|
||||||
|
{
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_reset_reopen(ao);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
MultipleOutputs::Update()
|
||||||
|
{
|
||||||
|
bool ret = false;
|
||||||
|
|
||||||
|
if (!input_audio_format.IsDefined())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (auto ao : outputs)
|
||||||
|
ret = audio_output_update(ao, input_audio_format, *pipe)
|
||||||
|
|| ret;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::SetReplayGainMode(ReplayGainMode mode)
|
||||||
|
{
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_set_replay_gain_mode(ao, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
MultipleOutputs::Play(music_chunk *chunk, Error &error)
|
||||||
|
{
|
||||||
|
assert(buffer != nullptr);
|
||||||
|
assert(pipe != nullptr);
|
||||||
|
assert(chunk != nullptr);
|
||||||
|
assert(chunk->CheckFormat(input_audio_format));
|
||||||
|
|
||||||
|
if (!Update()) {
|
||||||
|
/* TODO: obtain real error */
|
||||||
|
error.Set(output_domain, "Failed to open audio output");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pipe->Push(chunk);
|
||||||
|
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_play(ao);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
MultipleOutputs::Open(const AudioFormat audio_format,
|
||||||
|
MusicBuffer &_buffer,
|
||||||
|
Error &error)
|
||||||
|
{
|
||||||
|
bool ret = false, enabled = false;
|
||||||
|
|
||||||
|
assert(buffer == nullptr || buffer == &_buffer);
|
||||||
|
assert((pipe == nullptr) == (buffer == nullptr));
|
||||||
|
|
||||||
|
buffer = &_buffer;
|
||||||
|
|
||||||
|
/* the audio format must be the same as existing chunks in the
|
||||||
|
pipe */
|
||||||
|
assert(pipe == nullptr || pipe->CheckFormat(audio_format));
|
||||||
|
|
||||||
|
if (pipe == nullptr)
|
||||||
|
pipe = new MusicPipe();
|
||||||
|
else
|
||||||
|
/* if the pipe hasn't been cleared, the the audio
|
||||||
|
format must not have changed */
|
||||||
|
assert(pipe->IsEmpty() || audio_format == input_audio_format);
|
||||||
|
|
||||||
|
input_audio_format = audio_format;
|
||||||
|
|
||||||
|
ResetReopen();
|
||||||
|
EnableDisable();
|
||||||
|
Update();
|
||||||
|
|
||||||
|
for (auto ao : outputs) {
|
||||||
|
if (ao->enabled)
|
||||||
|
enabled = true;
|
||||||
|
|
||||||
|
if (ao->open)
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabled)
|
||||||
|
error.Set(output_domain, "All audio outputs are disabled");
|
||||||
|
else if (!ret)
|
||||||
|
/* TODO: obtain real error */
|
||||||
|
error.Set(output_domain, "Failed to open audio output");
|
||||||
|
|
||||||
|
if (!ret)
|
||||||
|
/* close all devices if there was an error */
|
||||||
|
Close();
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has the specified audio output already consumed this chunk?
|
||||||
|
*/
|
||||||
|
gcc_pure
|
||||||
|
static bool
|
||||||
|
chunk_is_consumed_in(const struct audio_output *ao,
|
||||||
|
gcc_unused const MusicPipe *pipe,
|
||||||
|
const struct music_chunk *chunk)
|
||||||
|
{
|
||||||
|
if (!ao->open)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (ao->chunk == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
assert(chunk == ao->chunk || pipe->Contains(ao->chunk));
|
||||||
|
|
||||||
|
if (chunk != ao->chunk) {
|
||||||
|
assert(chunk->next != nullptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ao->chunk_finished && chunk->next == nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
MultipleOutputs::IsChunkConsumed(const music_chunk *chunk) const
|
||||||
|
{
|
||||||
|
for (auto ao : outputs) {
|
||||||
|
const ScopeLock protect(ao->mutex);
|
||||||
|
if (!chunk_is_consumed_in(ao, pipe, chunk))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
MultipleOutputs::ClearTailChunk(gcc_unused const struct music_chunk *chunk,
|
||||||
|
bool *locked)
|
||||||
|
{
|
||||||
|
assert(chunk->next == nullptr);
|
||||||
|
assert(pipe->Contains(chunk));
|
||||||
|
|
||||||
|
for (unsigned i = 0, n = outputs.size(); i != n; ++i) {
|
||||||
|
audio_output *ao = outputs[i];
|
||||||
|
|
||||||
|
/* this mutex will be unlocked by the caller when it's
|
||||||
|
ready */
|
||||||
|
ao->mutex.lock();
|
||||||
|
locked[i] = ao->open;
|
||||||
|
|
||||||
|
if (!locked[i]) {
|
||||||
|
ao->mutex.unlock();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(ao->chunk == chunk);
|
||||||
|
assert(ao->chunk_finished);
|
||||||
|
ao->chunk = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned
|
||||||
|
MultipleOutputs::Check()
|
||||||
|
{
|
||||||
|
const struct music_chunk *chunk;
|
||||||
|
bool is_tail;
|
||||||
|
struct music_chunk *shifted;
|
||||||
|
bool locked[outputs.size()];
|
||||||
|
|
||||||
|
assert(buffer != nullptr);
|
||||||
|
assert(pipe != nullptr);
|
||||||
|
|
||||||
|
while ((chunk = pipe->Peek()) != nullptr) {
|
||||||
|
assert(!pipe->IsEmpty());
|
||||||
|
|
||||||
|
if (!IsChunkConsumed(chunk))
|
||||||
|
/* at least one output is not finished playing
|
||||||
|
this chunk */
|
||||||
|
return pipe->GetSize();
|
||||||
|
|
||||||
|
if (chunk->length > 0 && chunk->times >= 0.0)
|
||||||
|
/* only update elapsed_time if the chunk
|
||||||
|
provides a defined value */
|
||||||
|
elapsed_time = chunk->times;
|
||||||
|
|
||||||
|
is_tail = chunk->next == nullptr;
|
||||||
|
if (is_tail)
|
||||||
|
/* this is the tail of the pipe - clear the
|
||||||
|
chunk reference in all outputs */
|
||||||
|
ClearTailChunk(chunk, locked);
|
||||||
|
|
||||||
|
/* remove the chunk from the pipe */
|
||||||
|
shifted = pipe->Shift();
|
||||||
|
assert(shifted == chunk);
|
||||||
|
|
||||||
|
if (is_tail)
|
||||||
|
/* unlock all audio outputs which were locked
|
||||||
|
by clear_tail_chunk() */
|
||||||
|
for (unsigned i = 0, n = outputs.size(); i != n; ++i)
|
||||||
|
if (locked[i])
|
||||||
|
outputs[i]->mutex.unlock();
|
||||||
|
|
||||||
|
/* return the chunk to the buffer */
|
||||||
|
buffer->Return(shifted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
MultipleOutputs::Wait(PlayerControl &pc, unsigned threshold)
|
||||||
|
{
|
||||||
|
pc.Lock();
|
||||||
|
|
||||||
|
if (Check() < threshold) {
|
||||||
|
pc.Unlock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.Wait();
|
||||||
|
pc.Unlock();
|
||||||
|
|
||||||
|
return Check() < threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::Pause()
|
||||||
|
{
|
||||||
|
Update();
|
||||||
|
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_pause(ao);
|
||||||
|
|
||||||
|
WaitAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::Drain()
|
||||||
|
{
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_drain_async(ao);
|
||||||
|
|
||||||
|
WaitAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::Cancel()
|
||||||
|
{
|
||||||
|
/* send the cancel() command to all audio outputs */
|
||||||
|
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_cancel(ao);
|
||||||
|
|
||||||
|
WaitAll();
|
||||||
|
|
||||||
|
/* clear the music pipe and return all chunks to the buffer */
|
||||||
|
|
||||||
|
if (pipe != nullptr)
|
||||||
|
pipe->Clear(*buffer);
|
||||||
|
|
||||||
|
/* the audio outputs are now waiting for a signal, to
|
||||||
|
synchronize the cleared music pipe */
|
||||||
|
|
||||||
|
AllowPlay();
|
||||||
|
|
||||||
|
/* invalidate elapsed_time */
|
||||||
|
|
||||||
|
elapsed_time = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::Close()
|
||||||
|
{
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_close(ao);
|
||||||
|
|
||||||
|
if (pipe != nullptr) {
|
||||||
|
assert(buffer != nullptr);
|
||||||
|
|
||||||
|
pipe->Clear(*buffer);
|
||||||
|
delete pipe;
|
||||||
|
pipe = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = nullptr;
|
||||||
|
|
||||||
|
input_audio_format.Clear();
|
||||||
|
|
||||||
|
elapsed_time = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::Release()
|
||||||
|
{
|
||||||
|
for (auto ao : outputs)
|
||||||
|
audio_output_release(ao);
|
||||||
|
|
||||||
|
if (pipe != nullptr) {
|
||||||
|
assert(buffer != nullptr);
|
||||||
|
|
||||||
|
pipe->Clear(*buffer);
|
||||||
|
delete pipe;
|
||||||
|
pipe = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = nullptr;
|
||||||
|
|
||||||
|
input_audio_format.Clear();
|
||||||
|
|
||||||
|
elapsed_time = -1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
MultipleOutputs::SongBorder()
|
||||||
|
{
|
||||||
|
/* clear the elapsed_time pointer at the beginning of a new
|
||||||
|
song */
|
||||||
|
elapsed_time = 0.0;
|
||||||
|
}
|
271
src/output/MultipleOutputs.hxx
Normal file
271
src/output/MultipleOutputs.hxx
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2003-2014 The Music Player Daemon Project
|
||||||
|
* http://www.musicpd.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.,
|
||||||
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Functions for dealing with all configured (enabled) audion outputs
|
||||||
|
* at once.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef OUTPUT_ALL_H
|
||||||
|
#define OUTPUT_ALL_H
|
||||||
|
|
||||||
|
#include "AudioFormat.hxx"
|
||||||
|
#include "ReplayGainInfo.hxx"
|
||||||
|
#include "Compiler.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
struct AudioFormat;
|
||||||
|
class MusicBuffer;
|
||||||
|
class MusicPipe;
|
||||||
|
struct music_chunk;
|
||||||
|
struct PlayerControl;
|
||||||
|
struct audio_output;
|
||||||
|
class Error;
|
||||||
|
|
||||||
|
class MultipleOutputs {
|
||||||
|
std::vector<audio_output *> outputs;
|
||||||
|
|
||||||
|
AudioFormat input_audio_format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The #MusicBuffer object where consumed chunks are returned.
|
||||||
|
*/
|
||||||
|
MusicBuffer *buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The #MusicPipe object which feeds all audio outputs. It is
|
||||||
|
* filled by audio_output_all_play().
|
||||||
|
*/
|
||||||
|
MusicPipe *pipe;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "elapsed_time" stamp of the most recently finished
|
||||||
|
* chunk.
|
||||||
|
*/
|
||||||
|
float elapsed_time;
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Load audio outputs from the configuration file and
|
||||||
|
* initialize them.
|
||||||
|
*/
|
||||||
|
MultipleOutputs();
|
||||||
|
~MultipleOutputs();
|
||||||
|
|
||||||
|
void Configure(PlayerControl &pc);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total number of audio output devices, including
|
||||||
|
* those which are disabled right now.
|
||||||
|
*/
|
||||||
|
gcc_pure
|
||||||
|
unsigned Size() const {
|
||||||
|
return outputs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the "i"th audio output device.
|
||||||
|
*/
|
||||||
|
const audio_output &Get(unsigned i) const {
|
||||||
|
assert(i < Size());
|
||||||
|
|
||||||
|
return *outputs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_output &Get(unsigned i) {
|
||||||
|
assert(i < Size());
|
||||||
|
|
||||||
|
return *outputs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the audio output device with the specified name.
|
||||||
|
* Returns nullptr if the name does not exist.
|
||||||
|
*/
|
||||||
|
gcc_pure
|
||||||
|
audio_output *FindByName(const char *name) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the "enabled" flag of all audio outputs, and if one has
|
||||||
|
* changed, commit the change.
|
||||||
|
*/
|
||||||
|
void EnableDisable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens all audio outputs which are not disabled.
|
||||||
|
*
|
||||||
|
* @param audio_format the preferred audio format
|
||||||
|
* @param buffer the #music_buffer where consumed #music_chunk objects
|
||||||
|
* should be returned
|
||||||
|
* @return true on success, false on failure
|
||||||
|
*/
|
||||||
|
bool Open(const AudioFormat audio_format, MusicBuffer &_buffer,
|
||||||
|
Error &error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all audio outputs.
|
||||||
|
*/
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all audio outputs. Outputs with the "always_on"
|
||||||
|
* flag are put into pause mode.
|
||||||
|
*/
|
||||||
|
void Release();
|
||||||
|
|
||||||
|
void SetReplayGainMode(ReplayGainMode mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a #music_chunk object for playing, i.e. pushes it to a
|
||||||
|
* #MusicPipe.
|
||||||
|
*
|
||||||
|
* @param chunk the #music_chunk object to be played
|
||||||
|
* @return true on success, false if no audio output was able to play
|
||||||
|
* (all closed then)
|
||||||
|
*/
|
||||||
|
bool Play(music_chunk *chunk, Error &error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 #MusicPipe
|
||||||
|
*/
|
||||||
|
unsigned Check();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the size of the #MusicPipe is below the #threshold. If
|
||||||
|
* not, it attempts to synchronize with all output threads, and waits
|
||||||
|
* until another #music_chunk is finished.
|
||||||
|
*
|
||||||
|
* @param threshold the maximum number of chunks in the pipe
|
||||||
|
* @return true if there are less than #threshold chunks in the pipe
|
||||||
|
*/
|
||||||
|
bool Wait(PlayerControl &pc, unsigned threshold);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts all audio outputs into pause mode. Most implementations will
|
||||||
|
* simply close it then.
|
||||||
|
*/
|
||||||
|
void Pause();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drain all audio outputs.
|
||||||
|
*/
|
||||||
|
void Drain();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to cancel data which may still be in the device's buffers.
|
||||||
|
*/
|
||||||
|
void Cancel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicate that a new song will begin now.
|
||||||
|
*/
|
||||||
|
void SongBorder();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the "elapsed_time" stamp of the most recently finished
|
||||||
|
* chunk. A negative value is returned when no chunk has been
|
||||||
|
* finished yet.
|
||||||
|
*/
|
||||||
|
gcc_pure
|
||||||
|
float GetElapsedTime() const {
|
||||||
|
return elapsed_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the average volume of all available mixers (range
|
||||||
|
* 0..100). Returns -1 if no mixer can be queried.
|
||||||
|
*/
|
||||||
|
gcc_pure
|
||||||
|
int GetVolume() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the volume on all available mixers.
|
||||||
|
*
|
||||||
|
* @param volume the volume (range 0..100)
|
||||||
|
* @return true on success, false on failure
|
||||||
|
*/
|
||||||
|
bool SetVolume(unsigned volume);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to GetVolume(), but gets the volume only for
|
||||||
|
* software mixers. See #software_mixer_plugin. This
|
||||||
|
* function fails if no software mixer is configured.
|
||||||
|
*/
|
||||||
|
gcc_pure
|
||||||
|
int GetSoftwareVolume() const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to SetVolume(), but sets the volume only for
|
||||||
|
* software mixers. See #software_mixer_plugin. This
|
||||||
|
* function cannot fail, because the underlying software
|
||||||
|
* mixers cannot fail either.
|
||||||
|
*/
|
||||||
|
void SetSoftwareVolume(unsigned volume);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Determine if all (active) outputs have finished the current
|
||||||
|
* command.
|
||||||
|
*/
|
||||||
|
gcc_pure
|
||||||
|
bool AllFinished() const;
|
||||||
|
|
||||||
|
void WaitAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals all audio outputs which are open.
|
||||||
|
*/
|
||||||
|
void AllowPlay();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the "reopen" flag on all audio devices. MPD should
|
||||||
|
* immediately retry to open the device instead of waiting for
|
||||||
|
* the timeout when the user wants to start playback.
|
||||||
|
*/
|
||||||
|
void ResetReopen();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens all output devices which are enabled, but closed.
|
||||||
|
*
|
||||||
|
* @return true if there is at least open output device which
|
||||||
|
* is open
|
||||||
|
*/
|
||||||
|
bool Update();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Has this chunk been consumed by all audio outputs?
|
||||||
|
*/
|
||||||
|
bool IsChunkConsumed(const music_chunk *chunk) const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There's only one chunk left in the pipe (#pipe), and all
|
||||||
|
* audio outputs have consumed it already. Clear the
|
||||||
|
* reference.
|
||||||
|
*/
|
||||||
|
void ClearTailChunk(const struct music_chunk *chunk, bool *locked);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
@ -1,589 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2003-2014 The Music Player Daemon Project
|
|
||||||
* http://www.musicpd.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.,
|
|
||||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
#include "OutputAll.hxx"
|
|
||||||
#include "PlayerControl.hxx"
|
|
||||||
#include "OutputInternal.hxx"
|
|
||||||
#include "OutputControl.hxx"
|
|
||||||
#include "OutputError.hxx"
|
|
||||||
#include "MusicBuffer.hxx"
|
|
||||||
#include "MusicPipe.hxx"
|
|
||||||
#include "MusicChunk.hxx"
|
|
||||||
#include "system/FatalError.hxx"
|
|
||||||
#include "util/Error.hxx"
|
|
||||||
#include "config/ConfigData.hxx"
|
|
||||||
#include "config/ConfigGlobal.hxx"
|
|
||||||
#include "config/ConfigOption.hxx"
|
|
||||||
#include "notify.hxx"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
static AudioFormat input_audio_format;
|
|
||||||
|
|
||||||
static struct audio_output **audio_outputs;
|
|
||||||
static unsigned int num_audio_outputs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The #MusicBuffer object where consumed chunks are returned.
|
|
||||||
*/
|
|
||||||
static MusicBuffer *g_music_buffer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The #MusicPipe object which feeds all audio outputs. It is filled
|
|
||||||
* by audio_output_all_play().
|
|
||||||
*/
|
|
||||||
static MusicPipe *g_mp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The "elapsed_time" stamp of the most recently finished chunk.
|
|
||||||
*/
|
|
||||||
static float audio_output_all_elapsed_time = -1.0;
|
|
||||||
|
|
||||||
unsigned int audio_output_count(void)
|
|
||||||
{
|
|
||||||
return num_audio_outputs;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct audio_output *
|
|
||||||
audio_output_get(unsigned i)
|
|
||||||
{
|
|
||||||
assert(i < num_audio_outputs);
|
|
||||||
|
|
||||||
assert(audio_outputs[i] != nullptr);
|
|
||||||
|
|
||||||
return audio_outputs[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
struct audio_output *
|
|
||||||
audio_output_find(const char *name)
|
|
||||||
{
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; ++i) {
|
|
||||||
struct audio_output *ao = audio_output_get(i);
|
|
||||||
|
|
||||||
if (strcmp(ao->name, name) == 0)
|
|
||||||
return ao;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* name not found */
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
gcc_const
|
|
||||||
static unsigned
|
|
||||||
audio_output_config_count(void)
|
|
||||||
{
|
|
||||||
unsigned int nr = 0;
|
|
||||||
const struct config_param *param = nullptr;
|
|
||||||
|
|
||||||
while ((param = config_get_next_param(CONF_AUDIO_OUTPUT, param)))
|
|
||||||
nr++;
|
|
||||||
if (!nr)
|
|
||||||
nr = 1; /* we'll always have at least one device */
|
|
||||||
return nr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_init(PlayerControl &pc)
|
|
||||||
{
|
|
||||||
const struct config_param *param = nullptr;
|
|
||||||
unsigned int i;
|
|
||||||
Error error;
|
|
||||||
|
|
||||||
num_audio_outputs = audio_output_config_count();
|
|
||||||
audio_outputs = new audio_output *[num_audio_outputs];
|
|
||||||
|
|
||||||
const config_param empty;
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; i++)
|
|
||||||
{
|
|
||||||
unsigned int j;
|
|
||||||
|
|
||||||
param = config_get_next_param(CONF_AUDIO_OUTPUT, param);
|
|
||||||
if (param == nullptr) {
|
|
||||||
/* only allow param to be nullptr if there
|
|
||||||
just one audio output */
|
|
||||||
assert(i == 0);
|
|
||||||
assert(num_audio_outputs == 1);
|
|
||||||
|
|
||||||
param = ∅
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_output *output = audio_output_new(*param, pc, error);
|
|
||||||
if (output == nullptr) {
|
|
||||||
if (param != nullptr)
|
|
||||||
FormatFatalError("line %i: %s",
|
|
||||||
param->line,
|
|
||||||
error.GetMessage());
|
|
||||||
else
|
|
||||||
FatalError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
audio_outputs[i] = output;
|
|
||||||
|
|
||||||
/* require output names to be unique: */
|
|
||||||
for (j = 0; j < i; j++) {
|
|
||||||
if (!strcmp(output->name, audio_outputs[j]->name)) {
|
|
||||||
FormatFatalError("output devices with identical "
|
|
||||||
"names: %s", output->name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_finish(void)
|
|
||||||
{
|
|
||||||
unsigned int i;
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; i++) {
|
|
||||||
audio_output_disable(audio_outputs[i]);
|
|
||||||
audio_output_finish(audio_outputs[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete[] audio_outputs;
|
|
||||||
audio_outputs = nullptr;
|
|
||||||
num_audio_outputs = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_enable_disable(void)
|
|
||||||
{
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; i++) {
|
|
||||||
struct audio_output *ao = audio_outputs[i];
|
|
||||||
bool enabled;
|
|
||||||
|
|
||||||
ao->mutex.lock();
|
|
||||||
enabled = ao->really_enabled;
|
|
||||||
ao->mutex.unlock();
|
|
||||||
|
|
||||||
if (ao->enabled != enabled) {
|
|
||||||
if (ao->enabled)
|
|
||||||
audio_output_enable(ao);
|
|
||||||
else
|
|
||||||
audio_output_disable(ao);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if all (active) outputs have finished the current
|
|
||||||
* command.
|
|
||||||
*/
|
|
||||||
static bool
|
|
||||||
audio_output_all_finished(void)
|
|
||||||
{
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; ++i) {
|
|
||||||
struct audio_output *ao = audio_outputs[i];
|
|
||||||
|
|
||||||
const ScopeLock protect(ao->mutex);
|
|
||||||
if (audio_output_is_open(ao) &&
|
|
||||||
!audio_output_command_is_finished(ao))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void audio_output_wait_all(void)
|
|
||||||
{
|
|
||||||
while (!audio_output_all_finished())
|
|
||||||
audio_output_client_notify.Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signals all audio outputs which are open.
|
|
||||||
*/
|
|
||||||
static void
|
|
||||||
audio_output_allow_play_all(void)
|
|
||||||
{
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_allow_play(audio_outputs[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
audio_output_reset_reopen(struct audio_output *ao)
|
|
||||||
{
|
|
||||||
const ScopeLock protect(ao->mutex);
|
|
||||||
|
|
||||||
ao->fail_timer.Reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the "reopen" flag on all audio devices. MPD should
|
|
||||||
* immediately retry to open the device instead of waiting for the
|
|
||||||
* timeout when the user wants to start playback.
|
|
||||||
*/
|
|
||||||
static void
|
|
||||||
audio_output_all_reset_reopen(void)
|
|
||||||
{
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; ++i) {
|
|
||||||
struct audio_output *ao = audio_outputs[i];
|
|
||||||
|
|
||||||
audio_output_reset_reopen(ao);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens all output devices which are enabled, but closed.
|
|
||||||
*
|
|
||||||
* @return true if there is at least open output device which is open
|
|
||||||
*/
|
|
||||||
static bool
|
|
||||||
audio_output_all_update(void)
|
|
||||||
{
|
|
||||||
unsigned int i;
|
|
||||||
bool ret = false;
|
|
||||||
|
|
||||||
if (!input_audio_format.IsDefined())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
|
||||||
ret = audio_output_update(audio_outputs[i],
|
|
||||||
input_audio_format, *g_mp) || ret;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_set_replay_gain_mode(ReplayGainMode mode)
|
|
||||||
{
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_set_replay_gain_mode(audio_outputs[i], mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
audio_output_all_play(struct music_chunk *chunk, Error &error)
|
|
||||||
{
|
|
||||||
bool ret;
|
|
||||||
unsigned int i;
|
|
||||||
|
|
||||||
assert(g_music_buffer != nullptr);
|
|
||||||
assert(g_mp != nullptr);
|
|
||||||
assert(chunk != nullptr);
|
|
||||||
assert(chunk->CheckFormat(input_audio_format));
|
|
||||||
|
|
||||||
ret = audio_output_all_update();
|
|
||||||
if (!ret) {
|
|
||||||
/* TODO: obtain real error */
|
|
||||||
error.Set(output_domain, "Failed to open audio output");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_mp->Push(chunk);
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_play(audio_outputs[i]);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
audio_output_all_open(const AudioFormat audio_format,
|
|
||||||
MusicBuffer &buffer,
|
|
||||||
Error &error)
|
|
||||||
{
|
|
||||||
bool ret = false, enabled = false;
|
|
||||||
unsigned int i;
|
|
||||||
|
|
||||||
assert(g_music_buffer == nullptr || g_music_buffer == &buffer);
|
|
||||||
assert((g_mp == nullptr) == (g_music_buffer == nullptr));
|
|
||||||
|
|
||||||
g_music_buffer = &buffer;
|
|
||||||
|
|
||||||
/* the audio format must be the same as existing chunks in the
|
|
||||||
pipe */
|
|
||||||
assert(g_mp == nullptr || g_mp->CheckFormat(audio_format));
|
|
||||||
|
|
||||||
if (g_mp == nullptr)
|
|
||||||
g_mp = new MusicPipe();
|
|
||||||
else
|
|
||||||
/* if the pipe hasn't been cleared, the the audio
|
|
||||||
format must not have changed */
|
|
||||||
assert(g_mp->IsEmpty() || audio_format == input_audio_format);
|
|
||||||
|
|
||||||
input_audio_format = audio_format;
|
|
||||||
|
|
||||||
audio_output_all_reset_reopen();
|
|
||||||
audio_output_all_enable_disable();
|
|
||||||
audio_output_all_update();
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i) {
|
|
||||||
if (audio_outputs[i]->enabled)
|
|
||||||
enabled = true;
|
|
||||||
|
|
||||||
if (audio_outputs[i]->open)
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enabled)
|
|
||||||
error.Set(output_domain, "All audio outputs are disabled");
|
|
||||||
else if (!ret)
|
|
||||||
/* TODO: obtain real error */
|
|
||||||
error.Set(output_domain, "Failed to open audio output");
|
|
||||||
|
|
||||||
if (!ret)
|
|
||||||
/* close all devices if there was an error */
|
|
||||||
audio_output_all_close();
|
|
||||||
|
|
||||||
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->open)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (ao->chunk == nullptr)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
assert(chunk == ao->chunk || g_mp->Contains(ao->chunk));
|
|
||||||
|
|
||||||
if (chunk != ao->chunk) {
|
|
||||||
assert(chunk->next != nullptr);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ao->chunk_finished && chunk->next == nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
|
||||||
struct audio_output *ao = audio_outputs[i];
|
|
||||||
|
|
||||||
const ScopeLock protect(ao->mutex);
|
|
||||||
if (!chunk_is_consumed_in(ao, chunk))
|
|
||||||
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(gcc_unused const struct music_chunk *chunk, bool *locked)
|
|
||||||
{
|
|
||||||
assert(chunk->next == nullptr);
|
|
||||||
assert(g_mp->Contains(chunk));
|
|
||||||
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; ++i) {
|
|
||||||
struct audio_output *ao = audio_outputs[i];
|
|
||||||
|
|
||||||
/* this mutex will be unlocked by the caller when it's
|
|
||||||
ready */
|
|
||||||
ao->mutex.lock();
|
|
||||||
locked[i] = ao->open;
|
|
||||||
|
|
||||||
if (!locked[i]) {
|
|
||||||
ao->mutex.unlock();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(ao->chunk == chunk);
|
|
||||||
assert(ao->chunk_finished);
|
|
||||||
ao->chunk = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 != nullptr);
|
|
||||||
assert(g_mp != nullptr);
|
|
||||||
|
|
||||||
while ((chunk = g_mp->Peek()) != nullptr) {
|
|
||||||
assert(!g_mp->IsEmpty());
|
|
||||||
|
|
||||||
if (!chunk_is_consumed(chunk))
|
|
||||||
/* at least one output is not finished playing
|
|
||||||
this chunk */
|
|
||||||
return g_mp->GetSize();
|
|
||||||
|
|
||||||
if (chunk->length > 0 && chunk->times >= 0.0)
|
|
||||||
/* only update elapsed_time if the chunk
|
|
||||||
provides a defined value */
|
|
||||||
audio_output_all_elapsed_time = chunk->times;
|
|
||||||
|
|
||||||
is_tail = chunk->next == nullptr;
|
|
||||||
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 = g_mp->Shift();
|
|
||||||
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])
|
|
||||||
audio_outputs[i]->mutex.unlock();
|
|
||||||
|
|
||||||
/* return the chunk to the buffer */
|
|
||||||
g_music_buffer->Return(shifted);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
audio_output_all_wait(PlayerControl &pc, unsigned threshold)
|
|
||||||
{
|
|
||||||
pc.Lock();
|
|
||||||
|
|
||||||
if (audio_output_all_check() < threshold) {
|
|
||||||
pc.Unlock();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pc.Wait();
|
|
||||||
pc.Unlock();
|
|
||||||
|
|
||||||
return audio_output_all_check() < threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_pause(void)
|
|
||||||
{
|
|
||||||
unsigned int i;
|
|
||||||
|
|
||||||
audio_output_all_update();
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_pause(audio_outputs[i]);
|
|
||||||
|
|
||||||
audio_output_wait_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_drain(void)
|
|
||||||
{
|
|
||||||
for (unsigned i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_drain_async(audio_outputs[i]);
|
|
||||||
|
|
||||||
audio_output_wait_all();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_cancel(void)
|
|
||||||
{
|
|
||||||
unsigned int i;
|
|
||||||
|
|
||||||
/* send the cancel() command to all audio outputs */
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_cancel(audio_outputs[i]);
|
|
||||||
|
|
||||||
audio_output_wait_all();
|
|
||||||
|
|
||||||
/* clear the music pipe and return all chunks to the buffer */
|
|
||||||
|
|
||||||
if (g_mp != nullptr)
|
|
||||||
g_mp->Clear(*g_music_buffer);
|
|
||||||
|
|
||||||
/* the audio outputs are now waiting for a signal, to
|
|
||||||
synchronize the cleared music pipe */
|
|
||||||
|
|
||||||
audio_output_allow_play_all();
|
|
||||||
|
|
||||||
/* invalidate elapsed_time */
|
|
||||||
|
|
||||||
audio_output_all_elapsed_time = -1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_close(void)
|
|
||||||
{
|
|
||||||
unsigned int i;
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_close(audio_outputs[i]);
|
|
||||||
|
|
||||||
if (g_mp != nullptr) {
|
|
||||||
assert(g_music_buffer != nullptr);
|
|
||||||
|
|
||||||
g_mp->Clear(*g_music_buffer);
|
|
||||||
delete g_mp;
|
|
||||||
g_mp = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_music_buffer = nullptr;
|
|
||||||
|
|
||||||
input_audio_format.Clear();
|
|
||||||
|
|
||||||
audio_output_all_elapsed_time = -1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_release(void)
|
|
||||||
{
|
|
||||||
unsigned int i;
|
|
||||||
|
|
||||||
for (i = 0; i < num_audio_outputs; ++i)
|
|
||||||
audio_output_release(audio_outputs[i]);
|
|
||||||
|
|
||||||
if (g_mp != nullptr) {
|
|
||||||
assert(g_music_buffer != nullptr);
|
|
||||||
|
|
||||||
g_mp->Clear(*g_music_buffer);
|
|
||||||
delete g_mp;
|
|
||||||
g_mp = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_music_buffer = nullptr;
|
|
||||||
|
|
||||||
input_audio_format.Clear();
|
|
||||||
|
|
||||||
audio_output_all_elapsed_time = -1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_song_border(void)
|
|
||||||
{
|
|
||||||
/* clear the elapsed_time pointer at the beginning of a new
|
|
||||||
song */
|
|
||||||
audio_output_all_elapsed_time = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
float
|
|
||||||
audio_output_all_get_elapsed_time(void)
|
|
||||||
{
|
|
||||||
return audio_output_all_elapsed_time;
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2003-2014 The Music Player Daemon Project
|
|
||||||
* http://www.musicpd.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.,
|
|
||||||
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Functions for dealing with all configured (enabled) audion outputs
|
|
||||||
* at once.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef OUTPUT_ALL_H
|
|
||||||
#define OUTPUT_ALL_H
|
|
||||||
|
|
||||||
#include "ReplayGainInfo.hxx"
|
|
||||||
#include "Compiler.h"
|
|
||||||
|
|
||||||
struct AudioFormat;
|
|
||||||
class MusicBuffer;
|
|
||||||
struct music_chunk;
|
|
||||||
struct PlayerControl;
|
|
||||||
class Error;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global initialization: load audio outputs from the configuration
|
|
||||||
* file and initialize them.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_init(PlayerControl &pc);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global finalization: free memory occupied by audio outputs. All
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_finish(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the total number of audio output devices, including those
|
|
||||||
* who are disabled right now.
|
|
||||||
*/
|
|
||||||
gcc_const
|
|
||||||
unsigned int audio_output_count(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the "i"th audio output device.
|
|
||||||
*/
|
|
||||||
gcc_const
|
|
||||||
struct audio_output *
|
|
||||||
audio_output_get(unsigned i);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the audio output device with the specified name. Returns
|
|
||||||
* NULL if the name does not exist.
|
|
||||||
*/
|
|
||||||
gcc_pure
|
|
||||||
struct audio_output *
|
|
||||||
audio_output_find(const char *name);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the "enabled" flag of all audio outputs, and if one has
|
|
||||||
* changed, commit the change.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_enable_disable(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens all audio outputs which are not disabled.
|
|
||||||
*
|
|
||||||
* @param audio_format the preferred audio format
|
|
||||||
* @param buffer the #music_buffer where consumed #music_chunk objects
|
|
||||||
* should be returned
|
|
||||||
* @return true on success, false on failure
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
audio_output_all_open(AudioFormat audio_format,
|
|
||||||
MusicBuffer &buffer,
|
|
||||||
Error &error);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes all audio outputs.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_close(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes all audio outputs. Outputs with the "always_on" flag are
|
|
||||||
* put into pause mode.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_release(void);
|
|
||||||
|
|
||||||
void
|
|
||||||
audio_output_all_set_replay_gain_mode(ReplayGainMode mode);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueue a #music_chunk object for playing, i.e. pushes it to a
|
|
||||||
* #MusicPipe.
|
|
||||||
*
|
|
||||||
* @param chunk the #music_chunk object to be played
|
|
||||||
* @return true on success, false if no audio output was able to play
|
|
||||||
* (all closed then)
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
audio_output_all_play(music_chunk *chunk, Error &error);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 #MusicPipe
|
|
||||||
*/
|
|
||||||
unsigned
|
|
||||||
audio_output_all_check(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the size of the #MusicPipe is below the #threshold. If
|
|
||||||
* not, it attempts to synchronize with all output threads, and waits
|
|
||||||
* until another #music_chunk is finished.
|
|
||||||
*
|
|
||||||
* @param threshold the maximum number of chunks in the pipe
|
|
||||||
* @return true if there are less than #threshold chunks in the pipe
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
audio_output_all_wait(PlayerControl &pc, unsigned threshold);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts all audio outputs into pause mode. Most implementations will
|
|
||||||
* simply close it then.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_pause(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drain all audio outputs.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_drain(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to cancel data which may still be in the device's buffers.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_cancel(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicate that a new song will begin now.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
audio_output_all_song_border(void);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the "elapsed_time" stamp of the most recently finished
|
|
||||||
* chunk. A negative value is returned when no chunk has been
|
|
||||||
* finished yet.
|
|
||||||
*/
|
|
||||||
gcc_pure
|
|
||||||
float
|
|
||||||
audio_output_all_get_elapsed_time(void);
|
|
||||||
|
|
||||||
#endif
|
|
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "OutputCommand.hxx"
|
#include "OutputCommand.hxx"
|
||||||
#include "OutputAll.hxx"
|
#include "MultipleOutputs.hxx"
|
||||||
#include "OutputInternal.hxx"
|
#include "OutputInternal.hxx"
|
||||||
#include "PlayerControl.hxx"
|
#include "PlayerControl.hxx"
|
||||||
#include "mixer/MixerControl.hxx"
|
#include "mixer/MixerControl.hxx"
|
||||||
@ -35,21 +35,19 @@
|
|||||||
extern unsigned audio_output_state_version;
|
extern unsigned audio_output_state_version;
|
||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_enable_index(unsigned idx)
|
audio_output_enable_index(MultipleOutputs &outputs, unsigned idx)
|
||||||
{
|
{
|
||||||
struct audio_output *ao;
|
if (idx >= outputs.Size())
|
||||||
|
|
||||||
if (idx >= audio_output_count())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ao = audio_output_get(idx);
|
audio_output &ao = outputs.Get(idx);
|
||||||
if (ao->enabled)
|
if (ao.enabled)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
ao->enabled = true;
|
ao.enabled = true;
|
||||||
idle_add(IDLE_OUTPUT);
|
idle_add(IDLE_OUTPUT);
|
||||||
|
|
||||||
ao->player_control->UpdateAudio();
|
ao.player_control->UpdateAudio();
|
||||||
|
|
||||||
++audio_output_state_version;
|
++audio_output_state_version;
|
||||||
|
|
||||||
@ -57,27 +55,25 @@ audio_output_enable_index(unsigned idx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_disable_index(unsigned idx)
|
audio_output_disable_index(MultipleOutputs &outputs, unsigned idx)
|
||||||
{
|
{
|
||||||
struct audio_output *ao;
|
if (idx >= outputs.Size())
|
||||||
|
|
||||||
if (idx >= audio_output_count())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ao = audio_output_get(idx);
|
audio_output &ao = outputs.Get(idx);
|
||||||
if (!ao->enabled)
|
if (!ao.enabled)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
ao->enabled = false;
|
ao.enabled = false;
|
||||||
idle_add(IDLE_OUTPUT);
|
idle_add(IDLE_OUTPUT);
|
||||||
|
|
||||||
Mixer *mixer = ao->mixer;
|
Mixer *mixer = ao.mixer;
|
||||||
if (mixer != nullptr) {
|
if (mixer != nullptr) {
|
||||||
mixer_close(mixer);
|
mixer_close(mixer);
|
||||||
idle_add(IDLE_MIXER);
|
idle_add(IDLE_MIXER);
|
||||||
}
|
}
|
||||||
|
|
||||||
ao->player_control->UpdateAudio();
|
ao.player_control->UpdateAudio();
|
||||||
|
|
||||||
++audio_output_state_version;
|
++audio_output_state_version;
|
||||||
|
|
||||||
@ -85,26 +81,24 @@ audio_output_disable_index(unsigned idx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_toggle_index(unsigned idx)
|
audio_output_toggle_index(MultipleOutputs &outputs, unsigned idx)
|
||||||
{
|
{
|
||||||
struct audio_output *ao;
|
if (idx >= outputs.Size())
|
||||||
|
|
||||||
if (idx >= audio_output_count())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ao = audio_output_get(idx);
|
audio_output &ao = outputs.Get(idx);
|
||||||
const bool enabled = ao->enabled = !ao->enabled;
|
const bool enabled = ao.enabled = !ao.enabled;
|
||||||
idle_add(IDLE_OUTPUT);
|
idle_add(IDLE_OUTPUT);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
Mixer *mixer = ao->mixer;
|
Mixer *mixer = ao.mixer;
|
||||||
if (mixer != nullptr) {
|
if (mixer != nullptr) {
|
||||||
mixer_close(mixer);
|
mixer_close(mixer);
|
||||||
idle_add(IDLE_MIXER);
|
idle_add(IDLE_MIXER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ao->player_control->UpdateAudio();
|
ao.player_control->UpdateAudio();
|
||||||
|
|
||||||
++audio_output_state_version;
|
++audio_output_state_version;
|
||||||
|
|
||||||
|
@ -27,25 +27,27 @@
|
|||||||
#ifndef MPD_OUTPUT_COMMAND_HXX
|
#ifndef MPD_OUTPUT_COMMAND_HXX
|
||||||
#define MPD_OUTPUT_COMMAND_HXX
|
#define MPD_OUTPUT_COMMAND_HXX
|
||||||
|
|
||||||
|
class MultipleOutputs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables an audio output. Returns false if the specified output
|
* Enables an audio output. Returns false if the specified output
|
||||||
* does not exist.
|
* does not exist.
|
||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
audio_output_enable_index(unsigned idx);
|
audio_output_enable_index(MultipleOutputs &outputs, unsigned idx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables an audio output. Returns false if the specified output
|
* Disables an audio output. Returns false if the specified output
|
||||||
* does not exist.
|
* does not exist.
|
||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
audio_output_disable_index(unsigned idx);
|
audio_output_disable_index(MultipleOutputs &outputs, unsigned idx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles an audio output. Returns false if the specified output
|
* Toggles an audio output. Returns false if the specified output
|
||||||
* does not exist.
|
* does not exist.
|
||||||
*/
|
*/
|
||||||
bool
|
bool
|
||||||
audio_output_toggle_index(unsigned idx);
|
audio_output_toggle_index(MultipleOutputs &outputs, unsigned idx);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -24,22 +24,20 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "OutputPrint.hxx"
|
#include "OutputPrint.hxx"
|
||||||
#include "OutputAll.hxx"
|
#include "MultipleOutputs.hxx"
|
||||||
#include "OutputInternal.hxx"
|
#include "OutputInternal.hxx"
|
||||||
#include "client/Client.hxx"
|
#include "client/Client.hxx"
|
||||||
|
|
||||||
void
|
void
|
||||||
printAudioDevices(Client &client)
|
printAudioDevices(Client &client, const MultipleOutputs &outputs)
|
||||||
{
|
{
|
||||||
const unsigned n = audio_output_count();
|
for (unsigned i = 0, n = outputs.Size(); i != n; ++i) {
|
||||||
|
const audio_output &ao = outputs.Get(i);
|
||||||
for (unsigned i = 0; i < n; ++i) {
|
|
||||||
const struct audio_output *ao = audio_output_get(i);
|
|
||||||
|
|
||||||
client_printf(client,
|
client_printf(client,
|
||||||
"outputid: %i\n"
|
"outputid: %i\n"
|
||||||
"outputname: %s\n"
|
"outputname: %s\n"
|
||||||
"outputenabled: %i\n",
|
"outputenabled: %i\n",
|
||||||
i, ao->name, ao->enabled);
|
i, ao.name, ao.enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2003-2014 The Music Player Daemon Project
|
* Copyright (C) 2003-2014 The Music Player Daemon Project
|
||||||
* http://www.musicpd.org
|
* http://www.musicpd.org
|
||||||
@ -27,8 +26,9 @@
|
|||||||
#define MPD_OUTPUT_PRINT_HXX
|
#define MPD_OUTPUT_PRINT_HXX
|
||||||
|
|
||||||
class Client;
|
class Client;
|
||||||
|
class MultipleOutputs;
|
||||||
|
|
||||||
void
|
void
|
||||||
printAudioDevices(Client &client);
|
printAudioDevices(Client &client, const MultipleOutputs &outputs);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "OutputState.hxx"
|
#include "OutputState.hxx"
|
||||||
#include "OutputAll.hxx"
|
#include "MultipleOutputs.hxx"
|
||||||
#include "OutputInternal.hxx"
|
#include "OutputInternal.hxx"
|
||||||
#include "OutputError.hxx"
|
#include "OutputError.hxx"
|
||||||
#include "Log.hxx"
|
#include "Log.hxx"
|
||||||
@ -38,27 +38,22 @@
|
|||||||
unsigned audio_output_state_version;
|
unsigned audio_output_state_version;
|
||||||
|
|
||||||
void
|
void
|
||||||
audio_output_state_save(FILE *fp)
|
audio_output_state_save(FILE *fp, const MultipleOutputs &outputs)
|
||||||
{
|
{
|
||||||
unsigned n = audio_output_count();
|
for (unsigned i = 0, n = outputs.Size(); i != n; ++i) {
|
||||||
|
const audio_output &ao = outputs.Get(i);
|
||||||
assert(n > 0);
|
|
||||||
|
|
||||||
for (unsigned i = 0; i < n; ++i) {
|
|
||||||
const struct audio_output *ao = audio_output_get(i);
|
|
||||||
|
|
||||||
fprintf(fp, AUDIO_DEVICE_STATE "%d:%s\n",
|
fprintf(fp, AUDIO_DEVICE_STATE "%d:%s\n",
|
||||||
ao->enabled, ao->name);
|
ao.enabled, ao.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_state_read(const char *line)
|
audio_output_state_read(const char *line, MultipleOutputs &outputs)
|
||||||
{
|
{
|
||||||
long value;
|
long value;
|
||||||
char *endptr;
|
char *endptr;
|
||||||
const char *name;
|
const char *name;
|
||||||
struct audio_output *ao;
|
|
||||||
|
|
||||||
if (!StringStartsWith(line, AUDIO_DEVICE_STATE))
|
if (!StringStartsWith(line, AUDIO_DEVICE_STATE))
|
||||||
return false;
|
return false;
|
||||||
@ -74,7 +69,7 @@ audio_output_state_read(const char *line)
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
name = endptr + 1;
|
name = endptr + 1;
|
||||||
ao = audio_output_find(name);
|
audio_output *ao = outputs.FindByName(name);
|
||||||
if (ao == NULL) {
|
if (ao == NULL) {
|
||||||
FormatDebug(output_domain,
|
FormatDebug(output_domain,
|
||||||
"Ignoring device state for '%s'", name);
|
"Ignoring device state for '%s'", name);
|
||||||
|
@ -27,11 +27,13 @@
|
|||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
class MultipleOutputs;
|
||||||
|
|
||||||
bool
|
bool
|
||||||
audio_output_state_read(const char *line);
|
audio_output_state_read(const char *line, MultipleOutputs &outputs);
|
||||||
|
|
||||||
void
|
void
|
||||||
audio_output_state_save(FILE *fp);
|
audio_output_state_save(FILE *fp, const MultipleOutputs &outputs);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a version number for the current state of the audio
|
* Generates a version number for the current state of the audio
|
||||||
|
@ -74,8 +74,10 @@ find_named_config_block(ConfigOption option, const char *name)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerControl::PlayerControl(gcc_unused unsigned _buffer_chunks,
|
PlayerControl::PlayerControl(gcc_unused MultipleOutputs &_outputs,
|
||||||
gcc_unused unsigned _buffered_before_play) {}
|
gcc_unused unsigned _buffer_chunks,
|
||||||
|
gcc_unused unsigned _buffered_before_play)
|
||||||
|
:outputs(_outputs) {}
|
||||||
PlayerControl::~PlayerControl() {}
|
PlayerControl::~PlayerControl() {}
|
||||||
|
|
||||||
static struct audio_output *
|
static struct audio_output *
|
||||||
@ -89,7 +91,8 @@ load_audio_output(const char *name)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct PlayerControl dummy_player_control(32, 4);
|
static struct PlayerControl dummy_player_control(*(MultipleOutputs *)nullptr,
|
||||||
|
32, 4);
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
struct audio_output *ao =
|
struct audio_output *ao =
|
||||||
|
Loading…
Reference in New Issue
Block a user