31#define LOC QString("PulseAudio: ")
40 value = PA_VOLUME_MUTED;
70 QString fn_log_tag =
"OpenDevice, ";
77 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"Failed to get new threaded mainloop");
110 pa_operation *op = pa_context_get_sink_info_by_index(
m_pcontext, 0,
115 pa_operation_unref(op);
120 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Failed to determine default sink samplerate");
161 QString fn_log_tag =
"OpenDevice, ";
164 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"audio channel limit %1, but %2 requested")
182 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"unsupported sample format %1")
189 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"invalid sample spec");
192 std::string spec(PA_SAMPLE_SPEC_SNPRINT_MAX,
'\0');
193 pa_sample_spec_snprint(spec.data(), spec.size(), &
m_sampleSpec);
194 LOG(VB_AUDIO, LOG_INFO,
LOC + fn_log_tag +
"using sample spec " + spec.data());
198 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"failed to init channel map");
205 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"failed to get new threaded mainloop");
275 while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)
279 pa_operation_unref(op);
309 QString fn_log_tag =
"WriteAudio, ";
310 pa_stream_state_t sstate = pa_stream_get_state(
m_pstream);
312 LOG(VB_AUDIO | VB_TIMESTAMP, LOG_INFO,
LOC + fn_log_tag + QString(
"writing %1 bytes").arg(size));
318 if (sstate == PA_STREAM_CREATING || sstate == PA_STREAM_READY)
320 int write_status = PA_ERR_INVALID;
321 size_t to_write = size;
322 unsigned char *buf_ptr = aubuf;
328 size_t writable = pa_stream_writable_size(
m_pstream);
331 size_t write = std::min(to_write, writable);
333 nullptr, 0, PA_SEEK_RELATIVE);
335 if (0 != write_status)
350 if (write_status != 0)
352 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"stream write failed: %1")
353 .arg(write_status == PA_ERR_BADSTATE
355 :
"PA_ERR_INVALID"));
358 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"short write, %1 of %2")
359 .arg(size - to_write).arg(size));
364 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"stream state not good: %1")
371 pa_usec_t latency = 0;
380 const pa_buffer_attr *buf_attr = pa_stream_get_buffer_attr(
m_pstream);
381 size_t bfree = pa_stream_writable_size(
m_pstream);
382 buffered = buf_attr->tlength - bfree;
386 while (pa_stream_get_latency(
m_pstream, &latency,
nullptr) < 0)
388 if (pa_context_errno(
m_pcontext) != PA_ERR_NODATA)
405 (float)PA_VOLUME_NORM * 100.0F;
410 QString fn_log_tag =
"SetVolumeChannel, ";
414 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"bad volume params, channel %1, volume %2")
415 .arg(channel).arg(volume));
420 (float)volume / 100.0F * (
float)PA_VOLUME_NORM;
428 uint32_t stream_index = pa_stream_get_index(
m_pstream);
431 pa_context_set_sink_input_volume(
m_pcontext, stream_index,
436 pa_operation_unref(op);
439 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
440 QString(
"set stream volume operation failed, stream %1, "
443 .arg(pa_strerror(pa_context_errno(
m_pcontext))));
448 uint32_t sink_index = pa_stream_get_device_index(
m_pstream);
451 pa_context_set_sink_volume_by_index(
m_pcontext, sink_index,
456 pa_operation_unref(op);
459 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
460 QString(
"set sink volume operation failed, sink %1, "
463 .arg(pa_strerror(pa_context_errno(
m_pcontext))));
472 pa_operation *op = pa_stream_drain(
m_pstream,
nullptr,
this);
476 pa_operation_unref(op);
478 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Drain, stream drain failed");
483 QString fn_log_tag =
"ContextConnect, ";
486 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"context appears to exist, but shouldn't (yet)");
494 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"failed to create new proplist"));
497 pa_proplist_sets(
m_ctproplist, PA_PROP_APPLICATION_NAME,
"MythTV");
498 pa_proplist_sets(
m_ctproplist, PA_PROP_APPLICATION_ICON_NAME,
"mythtv");
499 pa_proplist_sets(
m_ctproplist, PA_PROP_MEDIA_ROLE,
"video");
501 pa_context_new_with_proplist(pa_threaded_mainloop_get_api(
m_mainloop),
505 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"failed to acquire new context");
514 !pulse_host.isEmpty() ? qPrintable(pulse_host) :
nullptr,
515 (pa_context_flags_t)0,
nullptr);
519 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"context connect failed: %1")
520 .arg(pa_strerror(pa_context_errno(
m_pcontext))));
525 bool connected =
false;
526 pa_context_state_t state = pa_context_get_state(
m_pcontext);
527 for (; !connected; state = pa_context_get_state(
m_pcontext))
531 case PA_CONTEXT_READY:
532 LOG(VB_AUDIO, LOG_INFO,
LOC + fn_log_tag +
"context connection ready");
536 case PA_CONTEXT_FAILED:
537 case PA_CONTEXT_TERMINATED:
538 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
539 QString(
"context connection failed or terminated: %1")
540 .arg(pa_strerror(pa_context_errno(
m_pcontext))));
546 LOG(VB_AUDIO, LOG_INFO,
LOC + fn_log_tag +
"waiting for context connection ready");
556 pa_operation_unref(op);
558 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"failed to get PulseAudio server info");
565 QString fn_log_tag =
"ChooseHost, ";
567 QString host = parts.size() > 1 ? parts[1] : QString();
570 if (host !=
"default")
573 if (pulse_host.isEmpty() && host !=
"default")
575 QString env_pulse_host = qEnvironmentVariable(
"PULSE_SERVER");
576 if (!env_pulse_host.isEmpty())
577 pulse_host = env_pulse_host;
580 LOG(VB_AUDIO, LOG_INFO,
LOC + fn_log_tag + QString(
"chosen PulseAudio server: %1")
581 .arg((pulse_host !=
nullptr) ? pulse_host :
"default"));
588 QString fn_log_tag =
"ConnectPlaybackStream, ";
592 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"failed to create new proplist"));
595 pa_proplist_sets(
m_stproplist, PA_PROP_MEDIA_ROLE,
"video");
601 LOG(VB_GENERAL, LOG_ERR,
LOC +
"failed to create new playback stream");
623 (
float)volume * (
float)PA_VOLUME_NORM / 100.0F);
638 int flags = PA_STREAM_INTERPOLATE_TIMING
639 | PA_STREAM_ADJUST_LATENCY
640 | PA_STREAM_AUTO_TIMING_UPDATE
641 | PA_STREAM_NO_REMIX_CHANNELS;
644 (pa_stream_flags_t)flags,
nullptr,
nullptr);
646 pa_stream_state_t sstate = PA_STREAM_UNCONNECTED;
647 bool connected =
false;
650 while (!(connected || failed))
652 pa_context_state_t cstate = pa_context_get_state(
m_pcontext);
655 case PA_CONTEXT_FAILED:
656 case PA_CONTEXT_TERMINATED:
657 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"context is stuffed, %1")
658 .arg(pa_strerror(pa_context_errno(
m_pcontext))));
664 switch (sstate = pa_stream_get_state(
m_pstream))
666 case PA_STREAM_READY:
669 case PA_STREAM_FAILED:
670 case PA_STREAM_TERMINATED:
671 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"stream failed or was terminated, "
672 "context state %1, stream state %2")
673 .arg(cstate).arg(sstate));
685 const pa_buffer_attr *buf_attr = pa_stream_get_buffer_attr(
m_pstream);
689 LOG(VB_AUDIO, LOG_INFO,
LOC + QString(
"fragment size %1, soundcard buffer size %2")
692 return (connected && !failed);
697 QString fn_log_tag = QString(
"FlushStream (%1), ").arg(caller);
699 pa_operation *op = pa_stream_flush(
m_pstream,
nullptr,
this);
702 pa_operation_unref(op);
704 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag +
"stream flush operation failed ");
709 auto *mloop = (pa_threaded_mainloop *)arg;
710 pa_threaded_mainloop_signal(mloop, 0);
716 switch (pa_context_get_state(c))
718 case PA_CONTEXT_READY:
719 case PA_CONTEXT_TERMINATED:
720 case PA_CONTEXT_FAILED:
721 pa_threaded_mainloop_signal(audoutP->m_mainloop, 0);
723 case PA_CONTEXT_CONNECTING:
724 case PA_CONTEXT_UNCONNECTED:
725 case PA_CONTEXT_AUTHORIZING:
726 case PA_CONTEXT_SETTING_NAME:
734 switch (pa_stream_get_state(s))
736 case PA_STREAM_READY:
737 case PA_STREAM_TERMINATED:
738 case PA_STREAM_FAILED:
739 pa_threaded_mainloop_signal(audoutP->m_mainloop, 0);
741 case PA_STREAM_UNCONNECTED:
742 case PA_STREAM_CREATING:
750 pa_threaded_mainloop_signal(audoutP->m_mainloop, 0);
755 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"stream buffer %1 flow").arg((
char*)tag));
759 pa_context *c,
int ok,
void *arg)
761 QString fn_log_tag =
"OpCompletionCallback, ";
765 LOG(VB_GENERAL, LOG_ERR,
LOC + fn_log_tag + QString(
"bummer, an operation failed: %1")
766 .arg(pa_strerror(pa_context_errno(c))));
768 pa_threaded_mainloop_signal(audoutP->m_mainloop, 0);
772 pa_context *,
const pa_server_info *inf,
void *)
774 QString fn_log_tag =
"ServerInfoCallback, ";
776 LOG(VB_AUDIO, LOG_INFO,
LOC + fn_log_tag +
777 QString(
"PulseAudio server info - host name: %1, server version: "
778 "%2, server name: %3, default sink: %4")
779 .arg(inf->host_name, inf->server_version,
780 inf->server_name, inf->default_sink_name));
784 pa_context *,
const pa_sink_info *
info,
int ,
void *arg)
790 pa_threaded_mainloop_signal(audoutP->m_mainloop, 0);
794 audoutP->m_aoSettings->AddSupportedRate(
info->sample_spec.rate);
796 for (
uint i = 2; i <=
info->sample_spec.channels; i++)
797 audoutP->m_aoSettings->AddSupportedChannels(i);
799 pa_threaded_mainloop_signal(audoutP->m_mainloop, 0);
static constexpr int8_t PULSE_MAX_CHANNELS
void KillAudio(void)
Kill the output thread and cleanup.
void Reconfigure(const AudioSettings &settings) override
(Re)Configure AudioOutputBase
AudioFormat m_outputFormat
int m_outputBytesPerFrame
long m_soundcardBufferSize
void InitSettings(const AudioSettings &settings)
void Drain(void) override
Block until all available frames have been written to the device.
int GetBufferedOnSoundcard(void) const override
Return the size in bytes of frames currently in the audio buffer adjusted with the audio playback lat...
void SetVolumeChannel(int channel, int volume) override
void WriteAudio(unsigned char *aubuf, int size) override
pa_proplist * m_ctproplist
pa_channel_map m_channelMap
pa_sample_spec m_sampleSpec
static void BufferFlowCallback(pa_stream *s, void *tag)
bool ConnectPlaybackStream(void)
void Drain(void) override
Block until all available frames have been written to the device.
static void SinkInfoCallback(pa_context *c, const pa_sink_info *info, int eol, void *arg)
static void ServerInfoCallback(pa_context *context, const pa_server_info *inf, void *arg)
static void ContextDrainCallback(pa_context *c, void *arg)
pa_cvolume m_volumeControl
void FlushStream(const char *caller)
AudioOutputPulseAudio(const AudioSettings &settings)
pa_proplist * m_stproplist
AudioOutputSettings * m_aoSettings
static void WriteCallback(pa_stream *s, size_t size, void *arg)
static void OpCompletionCallback(pa_context *c, int ok, void *arg)
static void StreamStateCallback(pa_stream *s, void *arg)
AudioOutputSettings * GetOutputSettings(bool digital) override
pa_threaded_mainloop * m_mainloop
int GetVolumeChannel(int channel) const override
static void ContextStateCallback(pa_context *c, void *arg)
bool OpenDevice(void) override
pa_buffer_attr m_bufferSettings
~AudioOutputPulseAudio() override
bool ContextConnect(void)
void CloseDevice(void) override
AudioFormat GetNextFormat()
void AddSupportedFormat(AudioFormat format)
bool m_init
If set to false, AudioOutput instance will not try to initially open the audio device.
QString GetSetting(const QString &key, const QString &defaultval="")
int GetNumSetting(const QString &key, int defaultval=0)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
def write(text, progress=True)