// // libtgvoip is free and unencumbered public domain software. // For more information, see http://unlicense.org or the UNLICENSE file // you should have received with this source code distribution. // #include "../../logging.h" #include "../../VoIPController.h" #include "AudioInputPulse.h" #include "AudioPulse.h" #include "PulseFunctions.h" #include #include #if !defined(__GLIBC__) #include #endif #include #include #define BUFFER_SIZE 960 #define CHECK_ERROR(res, msg) \ if (res != 0) \ { \ LOGE(msg " failed: %s", pa_strerror(res)); \ m_failed = true; \ return; \ } using namespace tgvoip::audio; AudioInputPulse::AudioInputPulse(pa_context* context, pa_threaded_mainloop* mainloop, std::string devID) : m_mainloop(mainloop) , m_context(context) { pa_threaded_mainloop_lock(mainloop); m_stream = CreateAndInitStream(); pa_threaded_mainloop_unlock(mainloop); if (m_stream == nullptr) { return; } SetCurrentDevice(std::move(devID)); } AudioInputPulse::~AudioInputPulse() { if (m_stream != nullptr) { pa_stream_disconnect(m_stream); pa_stream_unref(m_stream); } } pa_stream* AudioInputPulse::CreateAndInitStream() { pa_sample_spec sampleSpec { .format = PA_SAMPLE_S16LE, .rate = 48000, .channels = 1 }; pa_proplist* proplist = pa_proplist_new(); pa_proplist_sets(proplist, PA_PROP_FILTER_APPLY, ""); // according to PA sources, this disables any possible filters pa_stream* stream = pa_stream_new_with_proplist(m_context, "libtgvoip capture", &sampleSpec, nullptr, proplist); pa_proplist_free(proplist); if (stream == nullptr) { LOGE("Error initializing PulseAudio (pa_stream_new)"); m_failed = true; return nullptr; } pa_stream_set_state_callback(stream, AudioInputPulse::StreamStateCallback, this); pa_stream_set_read_callback(stream, AudioInputPulse::StreamReadCallback, this); return stream; } void AudioInputPulse::Start() { if (m_failed || m_isRecording) return; pa_threaded_mainloop_lock(m_mainloop); m_isRecording = true; pa_operation_unref(pa_stream_cork(m_stream, 0, nullptr, nullptr)); pa_threaded_mainloop_unlock(m_mainloop); } void AudioInputPulse::Stop() { if (!m_isRecording) return; m_isRecording = false; pa_threaded_mainloop_lock(m_mainloop); pa_operation_unref(pa_stream_cork(m_stream, 1, nullptr, nullptr)); pa_threaded_mainloop_unlock(m_mainloop); } bool AudioInputPulse::IsRecording() { return m_isRecording; } void AudioInputPulse::SetCurrentDevice(std::string devID) { pa_threaded_mainloop_lock(m_mainloop); m_currentDevice = std::move(devID); if (m_isRecording && m_isConnected) { pa_stream_disconnect(m_stream); pa_stream_unref(m_stream); m_isConnected = false; m_stream = CreateAndInitStream(); } pa_buffer_attr bufferAttr = { .maxlength = std::numeric_limits::max(), .tlength = std::numeric_limits::max(), .prebuf = std::numeric_limits::max(), .minreq = std::numeric_limits::max(), .fragsize = 960 * 2 }; int streamFlags = PA_STREAM_START_CORKED | PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; int err = pa_stream_connect_record(m_stream, m_currentDevice == "default" ? nullptr : m_currentDevice.c_str(), &bufferAttr, (pa_stream_flags_t)streamFlags); if (err != 0) { pa_threaded_mainloop_unlock(m_mainloop); } CHECK_ERROR(err, "pa_stream_connect_record"); while (true) { pa_stream_state_t streamState = pa_stream_get_state(m_stream); if (!PA_STREAM_IS_GOOD(streamState)) { LOGE("Error connecting to audio device '%s'", m_currentDevice.c_str()); pa_threaded_mainloop_unlock(m_mainloop); m_failed = true; return; } if (streamState == PA_STREAM_READY) break; pa_threaded_mainloop_wait(m_mainloop); } m_isConnected = true; if (m_isRecording) { pa_operation_unref(pa_stream_cork(m_stream, 0, nullptr, nullptr)); } pa_threaded_mainloop_unlock(m_mainloop); } bool AudioInputPulse::EnumerateDevices(std::vector& devs) { return AudioPulse::DoOneOperation([&](pa_context* ctx) { return pa_context_get_source_info_list( ctx, [](pa_context* ctx, const pa_source_info* info, int eol, void* userdata) { if (eol > 0) return; std::vector* devs = reinterpret_cast*>(userdata); AudioInputDevice dev; dev.id = std::string(info->name); dev.displayName = std::string(info->description); devs->emplace_back(dev); }, &devs); }); } void AudioInputPulse::StreamStateCallback(pa_stream* s, void* arg) { AudioInputPulse* self = reinterpret_cast(arg); pa_threaded_mainloop_signal(self->m_mainloop, 0); } void AudioInputPulse::StreamReadCallback(pa_stream* stream, std::size_t requestedBytes, void* userdata) { (reinterpret_cast(userdata))->StreamReadCallback(stream, requestedBytes); } void AudioInputPulse::StreamReadCallback(pa_stream* stream, std::size_t requestedBytes) { std::size_t bytesRemaining = requestedBytes; std::uint8_t* buffer = nullptr; pa_usec_t latency; if (pa_stream_get_latency(stream, &latency, nullptr) == 0) { m_estimatedDelay = static_cast(latency / 100); } while (bytesRemaining > 0) { std::size_t bytesToFill = 102400; if (bytesToFill > bytesRemaining) bytesToFill = bytesRemaining; int err = pa_stream_peek(stream, reinterpret_cast(&buffer), &bytesToFill); CHECK_ERROR(err, "pa_stream_peek"); if (m_isRecording) { if (m_remainingDataSize + bytesToFill > sizeof(m_remainingData)) { LOGE("Capture buffer is too big (%d)", static_cast(bytesToFill)); } std::memcpy(m_remainingData + m_remainingDataSize, buffer, bytesToFill); m_remainingDataSize += bytesToFill; while (m_remainingDataSize >= 960 * 2) { InvokeCallback(m_remainingData, 960 * 2); std::memmove(m_remainingData, m_remainingData + 960 * 2, m_remainingDataSize - 960 * 2); m_remainingDataSize -= 960 * 2; } } err = pa_stream_drop(stream); CHECK_ERROR(err, "pa_stream_drop"); bytesRemaining -= bytesToFill; } }