Commit 9dfd232
Changed files (4)
src/audioProcessor.cpp
@@ -0,0 +1,160 @@
+#include "audioProcessor.hpp"
+
+#include <iomanip>
+/* Reading and writing audio files using ffmpeg */
+void AudioProcessor::readAudioWithFFmpeg() {
+ size_t readSamples = 0;
+ const int sample = 48 * 1000;
+ const size_t totalSamples = audioData.duration * sample;
+ audioData.samples.resize(totalSamples);
+ audioData.header.sampleRate = sample;
+
+ const std::string ffmpegInputCmd =
+ "ffmpeg -i " + inputFile + " -ar " + std::to_string(sample) + " -f s16le -ac 1 -";
+ if (FILE *pipein = popen(ffmpegInputCmd.c_str(), "r")) {
+ readSamples = fread(audioData.samples.data(), sizeof(int16_t), totalSamples, pipein);
+ pclose(pipein);
+
+ if (readSamples == 0) {
+ throw std::runtime_error("Failed to read input audio");
+ }
+ } else {
+ throw std::runtime_error("Failed to open input audio");
+ }
+}
+
+void AudioProcessor::writeAudioWithFFmpeg() {
+ const int sample = 48 * 1000;
+ const size_t readSamples = audioData.samples.size();
+ const std::vector<int16_t> &outputBuffer = audioData.samples;
+ const std::string ffmpegOutputCmd =
+ "ffmpeg -y -f s16le -ar " + std::to_string(sample) + " -ac 1 -i - " + outputFile;
+
+ if (FILE *pipeout = popen(ffmpegOutputCmd.c_str(), "w")) {
+ fwrite(outputBuffer.data(), sizeof(int16_t), readSamples, pipeout);
+ pclose(pipeout);
+ } else {
+ throw std::runtime_error("Failed to open output pipe");
+ }
+}
+
+void AudioProcessor::resample(uint32_t expectedSampleRate) {
+ if (audioData.header.sampleRate == expectedSampleRate) {
+ return;
+ }
+ uint32_t oldSampleRate = audioData.header.sampleRate;
+ float ratio = static_cast<float>(expectedSampleRate) / oldSampleRate;
+ size_t newLength = static_cast<size_t>(std::ceil(audioData.samples.size() * ratio));
+ std::vector<int16_t> output(newLength);
+
+ // Linear interpolation
+ for (size_t i = 0; i < newLength; i++) {
+ float pos = i / ratio;
+ size_t idx = static_cast<size_t>(pos);
+ float frac = pos - idx;
+ output[i] = (idx + 1 < audioData.samples.size())
+ ? static_cast<int16_t>(
+ audioData.samples[idx] +
+ frac * (audioData.samples[idx + 1] - audioData.samples[idx]))
+ : audioData.samples[idx];
+ }
+ audioData.samples = output;
+ audioData.header.sampleRate = expectedSampleRate;
+ audioData.header.dataSize = output.size() * sizeof(int16_t);
+ audioData.header.chunkSize = audioData.header.dataSize + 36;
+ audioData.header.byteRate =
+ audioData.header.sampleRate * audioData.header.numChannels * sizeof(int16_t);
+ audioData.duration = calculateDuration(audioData.header);
+ /* we don't need to change this coz we are not changing the number of channels
+ newHeader.blockAlign = newHeader.numChannels * sizeof(int16_t); */
+
+ std::cout << "Resampled audio from " << oldSampleRate << " to " << expectedSampleRate << "Hz"
+ << std::endl;
+}
+
+void AudioProcessor::readRawAudioFile(bool headerOnly) {
+ std::ifstream file(inputFile, std::ios::binary);
+ if (!file) throw std::runtime_error("File not found!");
+ if (!file.is_open()) throw std::runtime_error("Failed to open file!");
+
+ file.read(reinterpret_cast<char *>(&audioData.header), sizeof(WAVHeader));
+ if (std::string(audioData.header.chunkID, 4) != "RIFF" ||
+ std::string(audioData.header.format, 4) != "WAVE")
+ throw std::runtime_error("Not a valid WAV file!");
+
+ while (std::string(audioData.header.dataID, 4) != "data") {
+ file.seekg(audioData.header.dataSize, std::ios::cur);
+ file.read(audioData.header.dataID, 4);
+ file.read(reinterpret_cast<char *>(&audioData.header.dataSize),
+ sizeof(audioData.header.dataSize));
+ if (file.eof()) {
+ std::cerr << "No 'data' chunk found in the WAV file." << std::endl;
+ throw std::runtime_error("Invalid WAV file");
+ }
+ }
+
+ audioData.duration = calculateDuration(audioData.header);
+ audioData.samples.resize(audioData.header.dataSize / sizeof(int16_t));
+ if (!headerOnly)
+ file.read(reinterpret_cast<char *>(audioData.samples.data()), audioData.header.dataSize);
+ file.close();
+}
+
+void AudioProcessor::writeRawAudioFile() {
+ std::ofstream file(outputFile, std::ios::binary);
+ file.write(reinterpret_cast<char *>(&audioData.header), sizeof(WAVHeader));
+ file.write(reinterpret_cast<char *>(audioData.samples.data()), audioData.header.dataSize);
+ file.close();
+}
+
+void AudioProcessor::processFrame(const int16_t *input, int16_t *output, int frameSize) {
+ std::vector<int16_t> frame(input, input + frameSize);
+ size_t expectedFrameSize = audioData.header.sampleRate / 1000 * MAX_PROCESSABLE_MS;
+
+ if (frameSize < (int)expectedFrameSize) {
+ frame.resize(expectedFrameSize, 0);
+ }
+ agcManager->process(frame);
+ std::copy(frame.begin(), frame.begin() + frameSize, output);
+}
+
+void AudioProcessor::performAGC() {
+ size_t frameCount = 0;
+ size_t readSamples = audioData.samples.size();
+ size_t frameSize = audioData.header.sampleRate / 1000 * MAX_PROCESSABLE_MS;
+ std::vector<int16_t> outputBuffer(readSamples);
+
+ if (readSamples == 0) {
+ throw std::runtime_error("No audio samples found");
+ }
+ bool supportedSampleRate = false;
+ for (uint32_t i : SUPPORTED_SAMPLE_RATES) {
+ if (audioData.header.sampleRate == i) {
+ supportedSampleRate = true;
+ break;
+ }
+ }
+ if (!supportedSampleRate) {
+ throw std::runtime_error("Unsupported sample rate");
+ }
+
+ agcManager = std::make_unique<AGC>(audioData.header.sampleRate);
+ auto processStart = std::chrono::steady_clock::now();
+
+ std::cout << "Processing audio..." << std::endl;
+ double percentage = 0.0;
+ for (size_t i = 0; i < readSamples; i += frameSize) {
+ size_t remainingSamples = std::min(frameSize, readSamples - i);
+ processFrame(audioData.samples.data() + i, outputBuffer.data() + i, remainingSamples);
+ percentage = (static_cast<double>(i) / readSamples) * 100;
+ std::cout << "Processed " << std::fixed << std::setprecision(2) << percentage << "%\r";
+ frameCount++;
+ }
+ audioData.samples = std::move(outputBuffer);
+ auto processEnd = std::chrono::steady_clock::now();
+
+ std::cout
+ << "Time taken to process " << frameCount << " frames: "
+ << std::chrono::duration_cast<std::chrono::milliseconds>(processEnd - processStart).count()
+ << "ms" << std::endl;
+}
\ No newline at end of file
src/audioProcessor.hpp
@@ -0,0 +1,137 @@
+#include <chrono>
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <vector>
+
+#include "audio_buffer.h"
+#include "gain_controller2.h"
+
+#define MAX_PROCESSABLE_MS 10
+#define SUPPORTED_SAMPLE_RATES {8000, 16000, 32000, 48000}
+
+struct WAVHeader {
+ char chunkID[4]; // "RIFF"
+ uint32_t chunkSize;
+ char format[4]; // "WAVE"
+ char subchunkID[4]; // "fmt "
+ uint32_t subchunkSize;
+ uint16_t audioFormat;
+ uint16_t numChannels;
+ uint32_t sampleRate;
+ uint32_t byteRate;
+ uint16_t blockAlign;
+ uint16_t bitsPerSample;
+ char dataID[4]; // "data"
+ uint32_t dataSize;
+
+ void print() {
+ std::cout << "chunkID: " << std::string(chunkID, 4) << std::endl;
+ std::cout << "chunkSize: " << chunkSize << std::endl;
+ std::cout << "format: " << std::string(format, 4) << std::endl;
+ std::cout << "subchunkID: " << std::string(subchunkID, 4) << std::endl;
+ std::cout << "subchunkSize: " << subchunkSize << std::endl;
+ std::cout << "audioFormat: " << audioFormat << std::endl;
+ std::cout << "numChannels: " << numChannels << std::endl;
+ std::cout << "sampleRate: " << sampleRate << std::endl;
+ std::cout << "byteRate: " << byteRate << std::endl;
+ std::cout << "blockAlign: " << blockAlign << std::endl;
+ std::cout << "bitsPerSample: " << bitsPerSample << std::endl;
+ std::cout << "dataID: " << std::string(dataID, 4) << std::endl;
+ std::cout << "dataSize: " << dataSize << std::endl;
+ }
+};
+
+struct AudioData {
+ WAVHeader header;
+ std::vector<int16_t> samples;
+ double duration;
+};
+
+class AGC {
+ private:
+ std::unique_ptr<webrtc::GainController2> gainController_m; // Use smart pointer
+ std::unique_ptr<webrtc::AudioBuffer> audioBuffer_m; // Use smart pointer
+ webrtc::StreamConfig streamConfig_m;
+ int sampleRate;
+
+ public:
+ AGC(int sampleRate) : streamConfig_m(sampleRate, 1), sampleRate(sampleRate) {
+ audioBuffer_m =
+ std::make_unique<webrtc::AudioBuffer>(sampleRate, 1, sampleRate, 1, sampleRate, 1);
+ initialize();
+ }
+ ~AGC() = default;
+
+ void initialize() {
+ webrtc::AudioProcessing::Config::GainController2 config;
+ webrtc::InputVolumeController::Config inputVolumeControllerConfig;
+ config.enabled = true;
+
+ // config.input_volume_controller.enabled = true; enable only if direct mic input is used
+
+ config.adaptive_digital.enabled = true;
+ config.adaptive_digital.headroom_db = 5.0f;
+ config.adaptive_digital.max_gain_db = 30.0f;
+ config.adaptive_digital.initial_gain_db = 10.0f;
+ config.adaptive_digital.max_gain_change_db_per_second = 5.0f;
+ config.adaptive_digital.max_output_noise_level_dbfs = -50.f;
+
+ config.fixed_digital.gain_db = 2.0f;
+
+ gainController_m = std::make_unique<webrtc::GainController2>(
+ config, inputVolumeControllerConfig, sampleRate, 1, false);
+ }
+
+ void process(std::vector<int16_t> &frame) {
+ size_t expectedSize = (size_t)(sampleRate * MAX_PROCESSABLE_MS / 1000);
+ if (frame.empty() || frame.size() != expectedSize) {
+ std::cerr << "Invalid frame size" << std::endl;
+ return;
+ }
+ audioBuffer_m->CopyFrom(frame.data(), streamConfig_m);
+ gainController_m->Process(std::nullopt, false, audioBuffer_m.get());
+ audioBuffer_m->CopyTo(streamConfig_m, frame.data());
+ }
+};
+
+class AudioProcessor {
+ private:
+ AudioData audioData;
+ std::string inputFile;
+ std::string outputFile;
+ std::unique_ptr<AGC> agcManager;
+
+ void processFrame(const int16_t *input, int16_t *output, int frameSize);
+ void readAudioWithFFmpeg();
+ void writeAudioWithFFmpeg();
+ void readRawAudioFile(bool headerOnly = false);
+ void writeRawAudioFile();
+ void performAGC();
+ double calculateDuration(const WAVHeader &header) {
+ return static_cast<double>(header.dataSize) / (header.byteRate);
+ }
+
+ public:
+ AudioProcessor(std::string inputFile, std::string outputFile)
+ : inputFile(inputFile), outputFile(outputFile) {
+ std::cout << "Input file: " << inputFile << std::endl;
+ std::cout << "Output file: " << outputFile << std::endl;
+ }
+ ~AudioProcessor() = default;
+ void resample(uint32_t expectedSampleRate);
+
+ void processWithFFmpeg() {
+ readRawAudioFile(true);
+ readAudioWithFFmpeg();
+ performAGC();
+ writeAudioWithFFmpeg();
+ }
+
+ void processWithCustomResampler(uint32_t expectedSampleRate = 48000) {
+ readRawAudioFile();
+ resample(expectedSampleRate);
+ performAGC();
+ writeRawAudioFile();
+ }
+};
src/main.cpp
@@ -0,0 +1,20 @@
+#include <fstream>
+
+#include "audioProcessor.hpp"
+
+int main(int argc, char* argv[]) {
+ if (argc < 3) {
+ std::cerr << "Usage: " << argv[0] << " <input_file> <output_file>" << std::endl;
+ return 1;
+ }
+
+ std::string inputFile = argv[1];
+ std::string outputFile = argv[2];
+
+ AudioProcessor audioProcessor(inputFile, outputFile);
+ audioProcessor.processWithCustomResampler();
+ // audioProcessor.processWithCustomResampler(16000);
+ // audioProcessor.processWithFFmpeg();
+
+ return 0;
+}
\ No newline at end of file
CMakeLists.txt
@@ -0,0 +1,23 @@
+cmake_minimum_required(VERSION 3.30)
+project(webrtcAGC
+ VERSION 1.0.0
+ DESCRIPTION "WebRTC AudioGainControl"
+ LANGUAGES CXX
+)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED True)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/bin)
+
+if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+ add_compile_options(-Wall)
+elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
+ add_compile_options(/W4)
+endif()
+
+add_executable(webrtcAGC src/main.cpp src/audioProcessor.cpp)
+add_definitions(-DWEBRTC_APM_DEBUG_DUMP=0)
+
+target_include_directories(webrtcAGC PRIVATE src/ thirdparty/agc/include thirdparty/agc/include/webrtc thirdparty/agc/include/webrtc/modules/audio_processing)
+target_link_directories(webrtcAGC PRIVATE thirdparty/agc/lib)
+target_link_libraries(webrtcAGC PRIVATE webrtc-audio-processing-2)