/** * @file * @brief Source file for audio recording classes * @author Jonathan Thomas * * @ref License */ // Copyright (c) 2008-2026 OpenShot Studios, LLC // // SPDX-License-Identifier: LGPL-3.0-or-later #include "AudioRecorder.h" #include #include #include #include "Exceptions.h" #include "FFmpegWriter.h" #include "Frame.h" using namespace openshot; AudioLevelData AudioRecorderLevelMeter::ProcessBlock(const AudioRecorderBlock& block) const { AudioLevelData result; result.timestamp = block.sample_rate > 0 ? static_cast(block.first_sample) / static_cast(block.sample_rate) : 0.0; const int channels = static_cast(block.channels.size()); result.peak.assign(channels, 0.0f); result.rms.assign(channels, 0.0f); for (int channel = 0; channel < channels; ++channel) { const auto& samples = block.channels[channel]; double squared_sum = 0.0; float peak = 0.0f; for (float sample : samples) { const float abs_sample = std::abs(sample); peak = std::max(peak, abs_sample); squared_sum += static_cast(sample) * static_cast(sample); if (abs_sample >= 1.0f) { result.clipped = true; } } result.peak[channel] = peak; if (!samples.empty()) { result.rms[channel] = static_cast(std::sqrt(squared_sum / static_cast(samples.size()))); } } return result; } AudioRecorderWaveformAccumulator::AudioRecorderWaveformAccumulator(int new_sample_rate, int new_samples_per_second) : sample_rate(new_sample_rate) , samples_per_second(new_samples_per_second) , sample_divisor(1) , pending_samples(0) , pending_max(0.0f) , pending_squared_sum(0.0) , emitted_visual_samples(0) { if (sample_rate <= 0 || samples_per_second <= 0) { throw InvalidOptions("Audio waveform settings require a valid sample rate and samples-per-second value."); } sample_divisor = std::max(1, sample_rate / samples_per_second); } std::vector AudioRecorderWaveformAccumulator::ProcessBlock(const AudioRecorderBlock& block) { std::vector chunks; if (block.channels.empty() || block.Samples() <= 0) { return chunks; } AudioWaveformChunk chunk; chunk.samples_per_second = samples_per_second; chunk.start_time = static_cast(emitted_visual_samples) / static_cast(samples_per_second); const int channels = static_cast(block.channels.size()); const int samples = block.Samples(); for (int sample_index = 0; sample_index < samples; ++sample_index) { for (int channel = 0; channel < channels; ++channel) { if (sample_index >= static_cast(block.channels[channel].size())) { continue; } const float sample = block.channels[channel][sample_index]; pending_max = std::max(pending_max, std::abs(sample)); pending_squared_sum += static_cast(sample) * static_cast(sample); } pending_samples++; if (pending_samples >= sample_divisor) { const double denominator = static_cast(pending_samples * channels); const float rms = denominator > 0.0 ? static_cast(std::sqrt(pending_squared_sum / denominator)) : 0.0f; max_samples.push_back(pending_max); rms_samples.push_back(rms); chunk.max_samples.push_back(pending_max); chunk.rms_samples.push_back(rms); emitted_visual_samples++; pending_samples = 0; pending_max = 0.0f; pending_squared_sum = 0.0; } } if (!chunk.max_samples.empty()) { chunk.duration = static_cast(chunk.max_samples.size()) / static_cast(samples_per_second); chunks.push_back(std::move(chunk)); } return chunks; } AudioWaveformData AudioRecorderWaveformAccumulator::Snapshot() const { AudioWaveformData result; result.max_samples = max_samples; result.rms_samples = rms_samples; return result; } void AudioRecorderWaveformAccumulator::Reset() { pending_samples = 0; pending_max = 0.0f; pending_squared_sum = 0.0; emitted_visual_samples = 0; max_samples.clear(); rms_samples.clear(); } std::shared_ptr AudioRecordingFrameFactory::CreateFrame( const AudioRecorderBlock& block, ChannelLayout channel_layout, int64_t frame_number) { const int channels = static_cast(block.channels.size()); const int samples = block.Samples(); auto frame = std::make_shared(frame_number, samples, channels); frame->SampleRate(block.sample_rate); frame->ChannelsLayout(channel_layout); for (int channel = 0; channel < channels; ++channel) { frame->AddAudio(true, channel, 0, block.channels[channel].data(), samples, 1.0f); } return frame; } AudioRecorder::AudioRecorder(const AudioRecorderSettings& new_settings) : settings(new_settings) , writer(nullptr) , waveform_accumulator(nullptr) , is_open(false) , is_recording(false) , is_monitoring(false) , writer_should_stop(false) , samples_recorded(0) , dropped_blocks(0) , next_frame_number(1) { ValidateSettings(); } AudioRecorder::~AudioRecorder() { Close(); } void AudioRecorder::ValidateSettings() const { if (settings.path.empty()) { throw InvalidOptions("Audio recorder requires an output path."); } if (settings.codec.empty()) { throw InvalidOptions("Audio recorder requires an audio codec."); } if (settings.sample_rate < 8000) { throw InvalidSampleRate("Audio recorder requires a sample rate of at least 8000 Hz.", settings.path); } if (settings.channels <= 0) { throw InvalidChannels("Audio recorder requires at least one input channel.", settings.path); } if (settings.buffer_size <= 0) { throw InvalidOptions("Audio recorder requires a positive audio buffer size.", settings.path); } if (settings.waveform_samples_per_second <= 0) { throw InvalidOptions("Audio recorder requires a positive waveform sample rate.", settings.path); } if (settings.max_queue_seconds <= 0) { throw InvalidOptions("Audio recorder requires a positive maximum queue duration.", settings.path); } } void AudioRecorder::Open() { if (is_open) { return; } if (!settings.device_type.empty()) { device_manager.setCurrentAudioDeviceType(settings.device_type, true); } juce::AudioDeviceManager::AudioDeviceSetup setup; setup.inputChannels.clear(); for (int channel = 0; channel < settings.channels; ++channel) { setup.inputChannels.setBit(channel); } setup.outputChannels.clear(); setup.sampleRate = settings.sample_rate; setup.bufferSize = settings.buffer_size; setup.inputDeviceName = settings.device_name; const juce::String error = device_manager.initialise( settings.channels, 0, nullptr, true, settings.device_name, &setup); if (error.isNotEmpty()) { throw InvalidOptions(error.toStdString(), settings.path); } if (auto* device = device_manager.getCurrentAudioDevice()) { const double actual_rate = device->getCurrentSampleRate(); if (actual_rate > 0.0) { settings.sample_rate = static_cast(std::llround(actual_rate)); } } waveform_accumulator = std::make_unique( settings.sample_rate, settings.waveform_samples_per_second); is_open = true; } void AudioRecorder::OpenWriter() { if (writer) { return; } writer = std::make_unique(settings.path); writer->SetAudioOptions(true, settings.codec, settings.sample_rate, settings.channels, settings.channel_layout, settings.bit_rate); writer->Open(); } void AudioRecorder::Start() { if (is_recording) { return; } PrepareRecording(); writer_should_stop = false; is_recording = true; device_manager.addAudioCallback(this); writer_thread = std::thread(&AudioRecorder::WriterLoop, this); } void AudioRecorder::PrepareRecording() { if (!is_open) { Open(); } StopMonitoring(); OpenWriter(); if (waveform_accumulator) { waveform_accumulator->Reset(); } samples_recorded = 0; dropped_blocks = 0; next_frame_number = 1; } void AudioRecorder::Stop() { if (!is_recording && !writer_thread.joinable()) { if (writer) { writer->Close(); writer.reset(); } return; } is_recording = false; device_manager.removeAudioCallback(this); writer_should_stop = true; queue_condition.notify_all(); if (writer_thread.joinable()) { writer_thread.join(); } if (writer) { writer->Close(); writer.reset(); } } void AudioRecorder::StartMonitoring() { if (is_recording || is_monitoring) { return; } if (!is_open) { Open(); } is_monitoring = true; device_manager.addAudioCallback(this); } void AudioRecorder::StopMonitoring() { if (!is_monitoring) { return; } is_monitoring = false; device_manager.removeAudioCallback(this); } void AudioRecorder::Close() { StopMonitoring(); Stop(); if (is_open) { device_manager.closeAudioDevice(); if (writer) { writer->Close(); writer.reset(); } is_open = false; } } bool AudioRecorder::IsOpen() const { return is_open; } bool AudioRecorder::IsRecording() const { return is_recording; } bool AudioRecorder::IsMonitoring() const { return is_monitoring; } AudioRecorderStats AudioRecorder::GetStats() const { AudioRecorderStats stats; stats.is_open = is_open; stats.is_recording = is_recording; stats.sample_rate = settings.sample_rate; stats.channels = settings.channels; stats.samples_recorded = samples_recorded; stats.dropped_blocks = dropped_blocks; stats.duration = settings.sample_rate > 0 ? static_cast(stats.samples_recorded) / static_cast(settings.sample_rate) : 0.0; std::lock_guard lock(queue_mutex); stats.queued_blocks = static_cast(queue.size()); return stats; } AudioWaveformData AudioRecorder::GetWaveformSnapshot() const { std::lock_guard lock(state_mutex); return waveform_accumulator ? waveform_accumulator->Snapshot() : AudioWaveformData(); } AudioLevelData AudioRecorder::GetLevelSnapshot() const { std::lock_guard lock(state_mutex); return last_level; } void AudioRecorder::SetLevelCallback(std::function callback) { std::lock_guard lock(state_mutex); level_callback = std::move(callback); } void AudioRecorder::SetWaveformCallback(std::function callback) { std::lock_guard lock(state_mutex); waveform_callback = std::move(callback); } void AudioRecorder::audioDeviceIOCallbackWithContext( const float* const* inputChannelData, int numInputChannels, float* const* outputChannelData, int numOutputChannels, int numSamples, const juce::AudioIODeviceCallbackContext&) { for (int channel = 0; channel < numOutputChannels; ++channel) { if (outputChannelData[channel]) { std::fill(outputChannelData[channel], outputChannelData[channel] + numSamples, 0.0f); } } if ((!is_recording && !is_monitoring) || numSamples <= 0) { return; } AudioRecorderBlock block; block.sample_rate = settings.sample_rate; block.first_sample = samples_recorded.load(); block.channels.resize(settings.channels); for (int channel = 0; channel < settings.channels; ++channel) { block.channels[channel].assign(numSamples, 0.0f); if (channel < numInputChannels && inputChannelData[channel]) { std::copy(inputChannelData[channel], inputChannelData[channel] + numSamples, block.channels[channel].begin()); } } AudioLevelData level = level_meter.ProcessBlock(block); std::function level_cb; { std::lock_guard lock(state_mutex); last_level = level; level_cb = level_callback; } if (level_cb) { level_cb(level); } if (!is_recording) { samples_recorded += numSamples; return; } const int64_t max_queue_blocks = static_cast( std::max(1, (settings.max_queue_seconds * settings.sample_rate) / std::max(1, numSamples))); { std::lock_guard lock(queue_mutex); if (static_cast(queue.size()) >= max_queue_blocks) { dropped_blocks++; } else { queue.push_back(std::move(block)); queue_condition.notify_one(); } } samples_recorded += numSamples; } void AudioRecorder::audioDeviceAboutToStart(juce::AudioIODevice* device) { (void) device; } void AudioRecorder::audioDeviceStopped() { } bool AudioRecorder::PopBlock(AudioRecorderBlock& block) { std::unique_lock lock(queue_mutex); queue_condition.wait(lock, [this]() { return writer_should_stop || !queue.empty(); }); if (queue.empty()) { return false; } block = std::move(queue.front()); queue.pop_front(); return true; } void AudioRecorder::WriterLoop() { while (true) { AudioRecorderBlock block; if (!PopBlock(block)) { if (writer_should_stop) { break; } continue; } std::vector waveform_chunks; std::function waveform_cb; { std::lock_guard lock(state_mutex); if (waveform_accumulator) { waveform_chunks = waveform_accumulator->ProcessBlock(block); } waveform_cb = waveform_callback; } if (waveform_cb) { for (const auto& chunk : waveform_chunks) { waveform_cb(chunk); } } if (writer) { writer->WriteFrame(AudioRecordingFrameFactory::CreateFrame( block, settings.channel_layout, next_frame_number++)); } } }