217 lines
9.2 KiB
C++
217 lines
9.2 KiB
C++
#include "audiorecorder.h"
|
|
#include "media_async/attachment_send.h"
|
|
#include "miniaudio.h"
|
|
#include "sound_manager.h"
|
|
#include "../transport/audio_diagnostics.h"
|
|
|
|
extern "C" {
|
|
#include "../../lib/audio_compressor.h"
|
|
}
|
|
|
|
#include "../../lib/debug_config.h"
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <exception>
|
|
#include <system_error>
|
|
|
|
/* Аппаратный callback не выделяет память и не обрабатывает компрессор. */
|
|
void AudioRecorder::captureCallback(const void* input, unsigned frames) {
|
|
if (!input || !m_recording.load() || m_captureFailed.load()) return;
|
|
auto* pcm = static_cast<const int16_t*>(input);
|
|
for (unsigned offset = 0; offset < frames;) {
|
|
CaptureBlock block{}; block.frames = std::min(frames - offset, 960u);
|
|
memcpy(block.pcm, pcm + offset * m_channels, block.frames * m_channels * sizeof(int16_t));
|
|
if (!m_captureQueue.push(block)) { m_captureFailed = true; break; }
|
|
offset += block.frames;
|
|
}
|
|
m_wake.notify_one();
|
|
}
|
|
|
|
/* Только worker изменяет PCM и compressor; GUI получает атомарные метрики. */
|
|
void AudioRecorder::recordLoop() {
|
|
try {
|
|
while (m_workerRunning || m_captureQueue.size()) {
|
|
CaptureBlock block{};
|
|
if (!m_captureQueue.pop(block)) {
|
|
std::unique_lock lock(m_waitMutex);
|
|
m_wake.wait_for(lock, std::chrono::milliseconds(5));
|
|
continue;
|
|
}
|
|
if (m_captureFailed) continue;
|
|
size_t count = (size_t)block.frames * m_channels;
|
|
if (!m_meterOnly) {
|
|
if (m_compressor && m_captureCompressorEnabled) {
|
|
if (audio_compressor_push(m_compressor, block.pcm, count) != 0) { m_captureFailed = true; continue; }
|
|
} else m_pcmBuffer.insert(m_pcmBuffer.end(), block.pcm, block.pcm + count);
|
|
}
|
|
double sum = 0;
|
|
for (size_t i = 0; i < count; ++i) { double value = block.pcm[i] / 32768.0; sum += value * value; }
|
|
m_peakLevel = (float)std::sqrt(sum / count);
|
|
m_recordedFrames.fetch_add(block.frames, std::memory_order_relaxed);
|
|
}
|
|
} catch (const std::exception& error) {
|
|
m_captureFailed = true;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: worker failed reason=%s", error.what());
|
|
}
|
|
}
|
|
|
|
AudioRecorder::AudioRecorder(QObject* parent)
|
|
: QObject(parent) {
|
|
m_compressor = audio_compressor_create();
|
|
if (m_compressor) {
|
|
audio_compressor_config_t cfg = {0};
|
|
cfg.sample_rate = m_sampleRate;
|
|
cfg.channels = m_channels;
|
|
cfg.lookahead_ms = 100;
|
|
m_compressorConfig = cfg;
|
|
audio_compressor_configure(m_compressor, &cfg);
|
|
}
|
|
m_captureId = SoundManager::instance()->captureDevice();
|
|
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() {
|
|
QString id = SoundManager::instance()->captureDevice();
|
|
if (!m_customDevice && id != m_captureId) {
|
|
m_captureId = id; m_capture.close(); m_capture.retryNow(); recoverCapture();
|
|
}
|
|
});
|
|
m_recoveryTimer.setInterval(200);
|
|
connect(&m_recoveryTimer, &QTimer::timeout, this, &AudioRecorder::recoverCapture);
|
|
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, [this]() {
|
|
m_capture.close(); m_durationTimer.stop();
|
|
});
|
|
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
|
|
m_capture.retryNow(); recoverCapture();
|
|
});
|
|
m_durationTimer.setInterval(100);
|
|
connect(&m_durationTimer, &QTimer::timeout, this, [this]() {
|
|
if (m_recording) {
|
|
m_elapsedMs = (int)(m_recordedFrames.load(std::memory_order_relaxed) * 1000 / m_sampleRate);
|
|
emit durationChanged(m_elapsedMs);
|
|
}
|
|
});
|
|
}
|
|
|
|
AudioRecorder::~AudioRecorder() {
|
|
shutdown();
|
|
audio_compressor_destroy(m_compressor);
|
|
m_compressor = nullptr;
|
|
}
|
|
|
|
bool AudioRecorder::init() {
|
|
m_initialized = true;
|
|
return true;
|
|
}
|
|
|
|
void AudioRecorder::shutdown() {
|
|
stopRecording();
|
|
m_recoveryTimer.stop();
|
|
m_capture.close();
|
|
m_initialized = false;
|
|
}
|
|
|
|
void AudioRecorder::startRecording(bool meterOnly) {
|
|
if (m_recording) return;
|
|
if (!m_initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "recorder: not initialized"); return; }
|
|
if (!m_microphone.acquire()) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "recorder: microphone owned by another operation"); return; }
|
|
m_pcmBuffer.clear(); m_meterOnly = meterOnly;
|
|
m_captureQueue.reset(); m_captureFailed = false;
|
|
m_recordedFrames = 0; m_elapsedMs = 0; m_peakLevel = 0;
|
|
m_captureCompressorEnabled = !meterOnly && m_compressorEnabled;
|
|
if (m_compressor) {
|
|
if (audio_compressor_configure(m_compressor, &m_compressorConfig) != 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: cannot configure compressor"); m_microphone.release(); return;
|
|
}
|
|
audio_compressor_set_enabled(m_compressor, m_captureCompressorEnabled);
|
|
audio_compressor_reset(m_compressor);
|
|
}
|
|
m_workerRunning = true;
|
|
try { m_recordWorker = std::thread(&AudioRecorder::recordLoop, this); }
|
|
catch (const std::system_error& error) {
|
|
m_workerRunning = false;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: worker start failed reason=%s", error.what()); m_microphone.release(); return;
|
|
}
|
|
m_recording = true;
|
|
m_capture.retryNow();
|
|
recoverCapture();
|
|
m_recoveryTimer.start();
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: recording requested compressor=%d", m_captureCompressorEnabled);
|
|
emit recordingStarted();
|
|
}
|
|
|
|
void AudioRecorder::recoverCapture() {
|
|
if (!m_recording) return;
|
|
if (m_captureFailed) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: capture loss or processing failure; recording rejected");
|
|
stopRecording(); return;
|
|
}
|
|
if (m_capture.needsRecovery()) { m_capture.close(); m_durationTimer.stop(); m_capture.retryLater(); }
|
|
if (!m_capture.retryReady()) return;
|
|
ma_context* context = SoundManager::instance()->context();
|
|
if (!context) return;
|
|
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
|
config.capture.format = ma_format_s16;
|
|
config.capture.channels = m_channels;
|
|
config.sampleRate = m_sampleRate;
|
|
ma_device_id id;
|
|
bool selected = SoundManager::deviceId(m_captureId, &id);
|
|
if (selected) config.capture.pDeviceID = &id;
|
|
auto callback = [this](const void* input, void*, unsigned int frames) { captureCallback(input, frames); };
|
|
bool ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start();
|
|
if (!ready) {
|
|
if (!selected) return;
|
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "recorder: selected input unavailable, trying default");
|
|
config.capture.pDeviceID = nullptr;
|
|
ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start();
|
|
}
|
|
if (ready) m_durationTimer.start();
|
|
}
|
|
|
|
void AudioRecorder::stopRecording() {
|
|
if (!m_recording.exchange(false)) return;
|
|
m_recoveryTimer.stop(); m_durationTimer.stop();
|
|
m_capture.close();
|
|
m_workerRunning = false; m_wake.notify_one();
|
|
if (m_recordWorker.joinable()) m_recordWorker.join();
|
|
m_microphone.release();
|
|
m_elapsedMs = (int)(m_recordedFrames.load() * 1000 / m_sampleRate);
|
|
int duration = m_elapsedMs;
|
|
size_t samples = m_compressor && m_captureCompressorEnabled ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size();
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: stopped duration=%dms samples=%zu", duration, samples);
|
|
emit recordingStopped(duration);
|
|
emit durationChanged(0);
|
|
}
|
|
|
|
int AudioRecorder::durationMs() const {
|
|
return m_elapsedMs;
|
|
}
|
|
|
|
/* Worker получает владельца буфера; новая запись имеет отдельное состояние. */
|
|
bool AudioRecorder::takeRecording(struct attachment_send_req* request) {
|
|
if (!request || m_recording || m_meterOnly || m_captureFailed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: cannot detach active recording"); return false; }
|
|
if (m_compressor && m_captureCompressorEnabled) {
|
|
auto* replacement = audio_compressor_clone_config(m_compressor);
|
|
if (!replacement) return false;
|
|
request->compressor = m_compressor;
|
|
m_compressor = replacement;
|
|
} else {
|
|
auto* samples = new std::vector<int16_t>(std::move(m_pcmBuffer));
|
|
request->pcm = samples->data(); request->pcm_count = samples->size(); request->pcm_owner = samples;
|
|
request->pcm_release = [](void* owner) { delete static_cast<std::vector<int16_t>*>(owner); };
|
|
}
|
|
return true;
|
|
}
|
|
|
|
float AudioRecorder::peakLevel() const { return m_peakLevel.exchange(0.0f); }
|
|
|
|
void AudioRecorder::configureCompressor(const audio_compressor_config_t& config) {
|
|
m_compressorConfig = config; /* Применяется перед следующей записью, когда callback ещё не запущен. */
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor configuration saved for next recording");
|
|
}
|
|
|
|
void AudioRecorder::setCompressorEnabled(bool enabled) {
|
|
m_compressorEnabled = enabled;
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor=%d for next recording", enabled);
|
|
}
|