Commit 9dfd232

Ansari <ansari_official@yahoo.com>
2025-01-17 07:27:34
basic implementation
1 parent b4a3c8b
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)