#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 #include #include #include #include #include #include /* Аппаратный callback не выделяет память и не обрабатывает компрессор. */ void AudioRecorder::captureCallback(const void* input, unsigned frames) { if (!input || !m_recording.load() || m_captureFailed.load()) return; auto* pcm = static_cast(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; } 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"); 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()); 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_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(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*>(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); }