void AudioOutputPulseAudio::SetVolumeChannel(int channel, int volume) { QString fn_log_tag = "SetVolumeChannel, "; if (channel < 0 || channel > PULSE_MAX_CHANNELS || volume < 0) { VBERROR(fn_log_tag + QString("bad volume params, channel %1, volume %2") .arg(channel).arg(volume)); return; } volume_control.values[channel] = (float)volume / 100.0f * (float)PA_VOLUME_NORM; volume = min(100, volume); volume = max(0, volume); uint32_t sink_index = pa_stream_get_device_index(pstream); pa_threaded_mainloop_lock(mainloop); pa_operation *op = pa_context_set_sink_volume_by_index(pcontext, sink_index, &volume_control, OpCompletionCallback, this); pa_threaded_mainloop_unlock(mainloop); if (op) pa_operation_unref(op); else VBERROR(fn_log_tag + QString("set sink volume operation failed, sink %1, error %2 ") .arg(sink_index).arg(pa_strerror(pa_context_errno(pcontext)))); }
void AudioOutputPulseAudio::WriteAudio(uchar *aubuf, int size) { QString fn_log_tag = "WriteAudio, "; pa_stream_state_t sstate = pa_stream_get_state(pstream); VBAUDIOTS(fn_log_tag + QString("writing %1 bytes").arg(size)); /* NB This "if" check can be replaced with PA_STREAM_IS_GOOD() in PulseAudio API from 0.9.11. As 0.9.10 is still widely used we use the more verbose version for now */ if (sstate == PA_STREAM_CREATING || sstate == PA_STREAM_READY) { int write_status = PA_ERR_INVALID; size_t to_write = size; unsigned char *buf_ptr = aubuf; pa_threaded_mainloop_lock(mainloop); while (to_write > 0) { write_status = 0; size_t writable = pa_stream_writable_size(pstream); if (writable > 0) { size_t write = min(to_write, writable); write_status = pa_stream_write(pstream, buf_ptr, write, NULL, 0, PA_SEEK_RELATIVE); if (0 != write_status) break; buf_ptr += write; to_write -= write; } else { pa_threaded_mainloop_wait(mainloop); } } pa_threaded_mainloop_unlock(mainloop); if (to_write > 0) { if (write_status != 0) VBERROR(fn_log_tag + QString("stream write failed: %1") .arg(write_status == PA_ERR_BADSTATE ? "PA_ERR_BADSTATE" : "PA_ERR_INVALID")); VBERROR(fn_log_tag + QString("short write, %1 of %2") .arg(size - to_write).arg(size)); } } else VBERROR(fn_log_tag + QString("stream state not good: %1") .arg(sstate,0,16)); }
char *AudioOutputPulseAudio::ChooseHost(void) { QString fn_log_tag = "ChooseHost, "; char *pulse_host = NULL; char *device = strdup(main_device.toAscii().constData()); const char *host; for (host=device; host && *host != ':' && *host != 0; host++); if (host && *host != 0) host++; if ( !(!host || *host == 0 || strcmp(host,"default") == 0)) { if ((pulse_host = new char[strlen(host)])) strcpy(pulse_host, host); else VBERROR(fn_log_tag + QString("allocation of pulse host '%1' char[%2] failed") .arg(host).arg(strlen(host) + 1)); } if (!pulse_host && strcmp(host,"default") != 0) { char *env_pulse_host = getenv("PULSE_SERVER"); if (env_pulse_host && (*env_pulse_host != '\0')) { int host_len = strlen(env_pulse_host) + 1; if ((pulse_host = new char[host_len])) strcpy(pulse_host, env_pulse_host); else { VBERROR(fn_log_tag + QString("allocation of pulse host '%1' char[%2] failed") .arg(env_pulse_host).arg(host_len)); } } } VBAUDIO(fn_log_tag + QString("chosen PulseAudio server: %1") .arg((pulse_host != NULL) ? pulse_host : "default")); free(device); return pulse_host; }
void AudioOutputPulseAudio::FlushStream(const char *caller) { QString fn_log_tag = QString("FlushStream (%1), ").arg(caller); pa_threaded_mainloop_lock(mainloop); pa_operation *op = pa_stream_flush(pstream, NULL, this); pa_threaded_mainloop_unlock(mainloop); if (op) pa_operation_unref(op); else VBERROR(fn_log_tag + "stream flush operation failed "); }
void AudioOutputPulseAudio::OpCompletionCallback( pa_context *c, int ok, void *arg) { QString fn_log_tag = "OpCompletionCallback, "; AudioOutputPulseAudio *audoutP = static_cast<AudioOutputPulseAudio*>(arg); if (!ok) { VBERROR(fn_log_tag + QString("bummer, an operation failed: %1") .arg(pa_strerror(pa_context_errno(c)))); } pa_threaded_mainloop_signal(audoutP->mainloop, 0); }
void AudioOutputPulseAudio::Drain(void) { AudioOutputBase::Drain(); pa_threaded_mainloop_lock(mainloop); pa_operation *op = pa_stream_drain(pstream, NULL, this); pa_threaded_mainloop_unlock(mainloop); if (op) pa_operation_unref(op); else VBERROR("Drain, stream drain failed"); }
void AudioOutputPulseAudio::BufferFlowCallback(pa_stream *s, void *tag) { VBERROR(QString("stream buffer %1 flow").arg((char*)tag)); }
AudioOutputSettings* AudioOutputPulseAudio::GetOutputSettings() { AudioFormat fmt; m_aosettings = new AudioOutputSettings(); QString fn_log_tag = "OpenDevice, "; /* Start the mainloop and connect a context so we can retrieve the parameters of the default sink */ mainloop = pa_threaded_mainloop_new(); if (!mainloop) { VBERROR(fn_log_tag + "Failed to get new threaded mainloop"); delete m_aosettings; return NULL; } pa_threaded_mainloop_start(mainloop); pa_threaded_mainloop_lock(mainloop); if (!ContextConnect()) { pa_threaded_mainloop_unlock(mainloop); pa_threaded_mainloop_stop(mainloop); delete m_aosettings; return NULL; } /* Get the samplerate and channel count of the default sink, supported rate and channels are added in SinkInfoCallback */ /* We should in theory be able to feed pulse any samplerate but allowing it to resample results in weird behaviour (odd channel maps, static) post pause / reset */ pa_operation *op = pa_context_get_sink_info_by_index(pcontext, 0, SinkInfoCallback, this); if (op) { pa_operation_unref(op); pa_threaded_mainloop_wait(mainloop); } else VBERROR("Failed to determine default sink samplerate"); pa_threaded_mainloop_unlock(mainloop); // All formats except S24 (pulse wants S24LSB) while ((fmt = m_aosettings->GetNextFormat())) { if (fmt == FORMAT_S24 // define from PA 0.9.15 only #ifndef PA_MAJOR || fmt == FORMAT_S24LSB #endif ) continue; m_aosettings->AddSupportedFormat(fmt); } pa_context_disconnect(pcontext); pa_context_unref(pcontext); pcontext = NULL; pa_threaded_mainloop_stop(mainloop); mainloop = NULL; return m_aosettings; }
bool AudioOutputPulseAudio::ConnectPlaybackStream(void) { QString fn_log_tag = "ConnectPlaybackStream, "; pstream = pa_stream_new(pcontext, "MythTV playback", &sample_spec, &channel_map); if (!pstream) { VBERROR(fn_log_tag + QString("failed to create new playback stream")); return false; } pa_stream_set_state_callback(pstream, StreamStateCallback, this); pa_stream_set_write_callback(pstream, WriteCallback, this); pa_stream_set_overflow_callback(pstream, BufferFlowCallback, (char*)"over"); pa_stream_set_underflow_callback(pstream, BufferFlowCallback, (char*)"under"); if (set_initial_vol) { int volume = gCoreContext->GetNumSetting("MasterMixerVolume", 80); pa_cvolume_set(&volume_control, channels, (float)volume * (float)PA_VOLUME_NORM / 100.0f); } else pa_cvolume_reset(&volume_control, channels); fragment_size = (samplerate * 25 * output_bytes_per_frame) / 1000; buffer_settings.maxlength = (uint32_t)-1; buffer_settings.tlength = fragment_size * 4; buffer_settings.prebuf = (uint32_t)-1; buffer_settings.minreq = (uint32_t)-1; int flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_NO_REMIX_CHANNELS; pa_stream_connect_playback(pstream, NULL, &buffer_settings, (pa_stream_flags_t)flags, &volume_control, NULL); pa_context_state_t cstate; pa_stream_state_t sstate; bool connected = false, failed = false; while (!(connected || failed)) { switch (cstate = pa_context_get_state(pcontext)) { case PA_CONTEXT_FAILED: case PA_CONTEXT_TERMINATED: VERBOSE(VB_IMPORTANT, LOC_ERR + fn_log_tag + QString("context is stuffed, %1") .arg(pa_strerror(pa_context_errno( pcontext)))); failed = true; break; default: switch (sstate = pa_stream_get_state(pstream)) { case PA_STREAM_READY: connected = true; break; case PA_STREAM_FAILED: case PA_STREAM_TERMINATED: VBERROR(fn_log_tag + QString("stream failed or was terminated, " "context state %1, stream state %2") .arg(cstate).arg(sstate)); failed = true; break; default: pa_threaded_mainloop_wait(mainloop); break; } } } const pa_buffer_attr *buf_attr = pa_stream_get_buffer_attr(pstream); fragment_size = buf_attr->tlength >> 2; soundcard_buffer_size = buf_attr->maxlength; VBAUDIO(fn_log_tag + QString("fragment size %1, soundcard buffer size %2") .arg(fragment_size).arg(soundcard_buffer_size)); return (connected && !failed); }
bool AudioOutputPulseAudio::ContextConnect(void) { QString fn_log_tag = "ContextConnect, "; if (pcontext) { VBERROR(fn_log_tag + "context appears to exist, but shouldn't (yet)"); pa_context_unref(pcontext); pcontext = NULL; return false; } pcontext = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "MythTV"); if (!pcontext) { VBERROR(fn_log_tag + "failed to acquire new context"); return false; } pa_context_set_state_callback(pcontext, ContextStateCallback, this); char *pulse_host = ChooseHost(); int chk = pa_context_connect( pcontext, pulse_host, (pa_context_flags_t)0, NULL); delete(pulse_host); if (chk < 0) { VBERROR(fn_log_tag + QString("context connect failed: %1") .arg(pa_strerror(pa_context_errno(pcontext)))); return false; } bool connected = false; pa_context_state_t state = pa_context_get_state(pcontext); for (; !connected; state = pa_context_get_state(pcontext)) { switch(state) { case PA_CONTEXT_READY: VBAUDIO(fn_log_tag +"context connection ready"); connected = true; continue; case PA_CONTEXT_FAILED: case PA_CONTEXT_TERMINATED: VBERROR(fn_log_tag + QString("context connection failed or terminated: %1") .arg(pa_strerror(pa_context_errno(pcontext)))); return false; default: VBAUDIO(fn_log_tag + "waiting for context connection ready"); pa_threaded_mainloop_wait(mainloop); break; } } pa_operation *op = pa_context_get_server_info(pcontext, ServerInfoCallback, this); if (op) pa_operation_unref(op); else VBERROR(fn_log_tag + "failed to get PulseAudio server info"); return true; }
bool AudioOutputPulseAudio::OpenDevice() { QString fn_log_tag = "OpenDevice, "; if (channels > PULSE_MAX_CHANNELS ) { VBERROR(fn_log_tag + QString("audio channel limit %1, but %2 requested") .arg(PULSE_MAX_CHANNELS).arg(channels)); return false; } sample_spec.rate = samplerate; sample_spec.channels = volume_control.channels = channels; switch (output_format) { case FORMAT_U8: sample_spec.format = PA_SAMPLE_U8; break; case FORMAT_S16: sample_spec.format = PA_SAMPLE_S16NE; break; // define from PA 0.9.15 only #ifdef PA_MAJOR case FORMAT_S24LSB: sample_spec.format = PA_SAMPLE_S24_32NE; break; #endif case FORMAT_S32: sample_spec.format = PA_SAMPLE_S32NE; break; case FORMAT_FLT: sample_spec.format = PA_SAMPLE_FLOAT32NE; break; break; default: VBERROR(fn_log_tag + QString("unsupported sample format %1") .arg(output_format)); return false; } if (!pa_sample_spec_valid(&sample_spec)) { VBERROR(fn_log_tag + "invalid sample spec"); return false; } else { char spec[PA_SAMPLE_SPEC_SNPRINT_MAX]; pa_sample_spec_snprint(spec, sizeof(spec), &sample_spec); VBAUDIO(fn_log_tag + QString("using sample spec %1").arg(spec)); } pa_channel_map *pmap = NULL; if(!(pmap = pa_channel_map_init_auto(&channel_map, channels, PA_CHANNEL_MAP_WAVEEX)) < 0) { VBERROR(fn_log_tag + "failed to init channel map"); return false; } channel_map = *pmap; mainloop = pa_threaded_mainloop_new(); if (!mainloop) { VBERROR(fn_log_tag + "failed to get new threaded mainloop"); return false; } pa_threaded_mainloop_start(mainloop); pa_threaded_mainloop_lock(mainloop); if (!ContextConnect()) { pa_threaded_mainloop_unlock(mainloop); pa_threaded_mainloop_stop(mainloop); return false; } if (!ConnectPlaybackStream()) { pa_threaded_mainloop_unlock(mainloop); pa_threaded_mainloop_stop(mainloop); return false; } pa_threaded_mainloop_unlock(mainloop); return true; }
void AudioOutputPulseAudio::WriteAudio(uchar *aubuf, int size) { QString fn_log_tag = "WriteAudio, "; pa_stream_state_t sstate = pa_stream_get_state(pstream); // Do not write anything to pulse server if we are in pause mode if (IsPaused()) return; VBAUDIOTS(fn_log_tag + QString("writing %1 bytes").arg(size)); /* NB This "if" check can be replaced with PA_STREAM_IS_GOOD() in PulseAudio API from 0.9.11. As 0.9.10 is still widely used we use the more verbose version for now */ if (sstate == PA_STREAM_CREATING || sstate == PA_STREAM_READY) { int write_status = PA_ERR_INVALID; size_t write; size_t writable; size_t to_write = size; unsigned char *buf_ptr = aubuf; pa_context_state_t cstate; pa_threaded_mainloop_lock(mainloop); while (to_write > 0) { write_status = 0; writable = pa_stream_writable_size(pstream); if (writable > 0) { write = min(to_write, writable); write_status = pa_stream_write(pstream, buf_ptr, write, NULL, 0, PA_SEEK_RELATIVE); if (!write_status) { buf_ptr += write; to_write -= write; } else break; } else if (writable < 0) break; else // writable == 0 pa_threaded_mainloop_wait(mainloop); } pa_threaded_mainloop_unlock(mainloop); if (to_write > 0) { if (writable < 0) { cstate = pa_context_get_state(pcontext); sstate = pa_stream_get_state(pstream); VBERROR(fn_log_tag + QString("stream unfit for writing (writable < 0), " "context state: %1, stream state: %2") .arg(cstate,0,16).arg(sstate,0,16)); } if (write_status != 0) VBERROR(fn_log_tag + QString("stream write failed: %1") .arg(write_status == PA_ERR_BADSTATE ? "PA_ERR_BADSTATE" : "PA_ERR_INVALID")); VBERROR(fn_log_tag + QString("short write, %1 of %2") .arg(size - to_write).arg(size)); } } else VBERROR(fn_log_tag + QString("stream state not good: %1") .arg(sstate,0,16)); }