You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
7.0 KiB
178 lines
7.0 KiB
#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> |
|
|
|
void AudioRecorder::captureCallback(const void* pInput, unsigned int frameCount) { |
|
if (!pInput || !frameCount || !m_recording.load()) return; |
|
const int16_t* src = (const int16_t*)pInput; |
|
size_t add = (size_t)frameCount * (size_t)m_channels; |
|
|
|
if (m_compressor && m_captureCompressorEnabled) { |
|
audio_compressor_push(m_compressor, src, add); |
|
} else { |
|
size_t cur = m_pcmBuffer.size(); |
|
m_pcmBuffer.resize(cur + add); |
|
std::memcpy(m_pcmBuffer.data() + cur, src, add * sizeof(int16_t)); |
|
} |
|
|
|
float sumSq = 0.0f; |
|
for (unsigned int i = 0; i < frameCount * (unsigned int)m_channels; i++) { |
|
float v = (float)src[i] / 32768.0f; |
|
sumSq += v * v; |
|
} |
|
float rms = sqrtf(sumSq / (float)(frameCount * m_channels)); |
|
m_peakLevel = rms; |
|
|
|
m_recordedFrames.fetch_add(frameCount, std::memory_order_relaxed); |
|
} |
|
|
|
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() { |
|
if (m_recording) return; |
|
if (!m_initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "recorder: not initialized"); return; } |
|
m_pcmBuffer.clear(); |
|
m_recordedFrames = 0; m_elapsedMs = 0; m_peakLevel = 0; |
|
m_captureCompressorEnabled = m_compressorEnabled; |
|
if (m_compressor) { |
|
audio_compressor_configure(m_compressor, &m_compressorConfig); |
|
audio_compressor_set_enabled(m_compressor, m_captureCompressorEnabled); |
|
audio_compressor_reset(m_compressor); |
|
} |
|
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_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_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) { 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); |
|
}
|
|
|