Program Listing for File stdstream.cpp — sudio documentation

# Program Listing for File stdstream.cpp

↰ Return to documentation for file (`sudio\io\src\stdstream.cpp`)

    /*
     -- W.T.A
     -- SUDIO (https://github.com/MrZahaki/sudio)
     -- The Audio Processing Platform
     -- Mail: mrzahaki@gmail.com
     -- Software license: "Apache License 2.0".
     -- file stdstream.cpp
    */
    #include "stdstream.hpp"
    #include <stdexcept>
    #include <iostream>
    #include <thread>
    #include <chrono>

    namespace stdstream {

    AudioStream::AudioStream() : stream(nullptr), isBlockingMode(false), inputEnabled(false), outputEnabled(false) {
        PaError err = Pa_Initialize();
        if (err != paNoError) {
            throw std::runtime_error("PortAudio initialization failed: " + std::string(Pa_GetErrorText(err)));
        }
    }

    AudioStream::~AudioStream() {
        if (stream) {
            close();
        }
        Pa_Terminate();
    }

    void AudioStream::open(int inputDeviceIndex, int outputDeviceIndex,
                           double sampleRate, PaSampleFormat format,
                           int inputChannels, int outputChannels,
                           unsigned long framesPerBuffer, bool enableInput,
                           bool enableOutput, PaStreamFlags streamFlags,
                           InputCallback inputCallback,
                           OutputCallback outputCallback) {
        PaStreamParameters inputParameters, outputParameters;
        PaStreamParameters *inputParamsPtr = nullptr;
        PaStreamParameters *outputParamsPtr = nullptr;

        if (!enableInput && !enableOutput) {
            throw std::runtime_error("At least one of input or output must be enabled");
        }

        inputEnabled = enableInput;
        outputEnabled = enableOutput;

        if (enableInput) {
            if (inputDeviceIndex == -1) inputDeviceIndex = Pa_GetDefaultInputDevice();
            const PaDeviceInfo* inputInfo = Pa_GetDeviceInfo(inputDeviceIndex);
            inputParameters.device = inputDeviceIndex;
            inputParameters.channelCount = inputChannels > 0 ? inputChannels : inputInfo->maxInputChannels;
            inputParameters.sampleFormat = format;
            inputParameters.suggestedLatency = inputInfo->defaultLowInputLatency;
            inputParameters.hostApiSpecificStreamInfo = nullptr;
            inputParamsPtr = &inputParameters;
        }

        if (enableOutput) {
            if (outputDeviceIndex == -1) outputDeviceIndex = Pa_GetDefaultOutputDevice();
            const PaDeviceInfo* outputInfo = Pa_GetDeviceInfo(outputDeviceIndex);
            outputParameters.device = outputDeviceIndex;
            outputParameters.channelCount = outputChannels > 0 ? outputChannels : outputInfo->maxOutputChannels;
            outputParameters.sampleFormat = format;
            outputParameters.suggestedLatency = outputInfo->defaultHighOutputLatency;
            outputParameters.hostApiSpecificStreamInfo = nullptr;
            outputParamsPtr = &outputParameters;
        }

        if (sampleRate == 0) {
            sampleRate = enableInput ? Pa_GetDeviceInfo(inputDeviceIndex)->defaultSampleRate :
                                       Pa_GetDeviceInfo(outputDeviceIndex)->defaultSampleRate;
        }

        userInputCallback = inputCallback;
        userOutputCallback = outputCallback;
        this->outputChannels =  outputParameters.channelCount;
        this->inputChannels = inputParameters.channelCount;

        isBlockingMode = !(inputCallback || outputCallback);
        PaStreamCallback *callbackPtr = isBlockingMode ? nullptr : &AudioStream::paCallback;

        PaError err = Pa_OpenStream(&stream, inputParamsPtr, outputParamsPtr, sampleRate, framesPerBuffer,
                                    streamFlags, callbackPtr, this);
        if (err != paNoError) {
            throw std::runtime_error("Failed to open PortAudio stream: " + std::string(Pa_GetErrorText(err)));
        }

        if (!stream) {
            throw std::runtime_error("Failed to create a valid PortAudio stream");
        }

        streamFormat = format;
        continueStreaming.store(true);
    }

    void AudioStream::start() {
        if (!stream) {
            throw std::runtime_error("Stream is not open");
        }
        PaError err = Pa_StartStream(stream);
        if (err != paNoError) {
            throw std::runtime_error("Failed to start PortAudio stream: " + std::string(Pa_GetErrorText(err)));
        }
    }

    void AudioStream::stop() {
        if (stream) {
            PaError err = Pa_StopStream(stream);
            if (err != paNoError) {
                std::cerr << "Warning: Failed to stop PortAudio stream: " << Pa_GetErrorText(err) << std::endl;
            }
        }
    }

    void AudioStream::close() {
        if (stream) {
            stop();
            PaError err = Pa_CloseStream(stream);
            if (err != paNoError) {
                std::cerr << "Warning: Failed to close PortAudio stream: " << Pa_GetErrorText(err) << std::endl;
            }
            stream = nullptr;
        }
    }

    std::vector<AudioDeviceInfo> AudioStream::getInputDevices() {
        std::vector<AudioDeviceInfo> devices;
        int numDevices = Pa_GetDeviceCount();
        int defaultInputDevice = Pa_GetDefaultInputDevice();

        for (int i = 0; i < numDevices; i++) {
            const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
            if (deviceInfo->maxInputChannels > 0) {
                devices.push_back({
                    i,
                    deviceInfo->name,
                    deviceInfo->maxInputChannels,
                    deviceInfo->maxOutputChannels,
                    deviceInfo->defaultSampleRate,
                    (i == defaultInputDevice),
                    false
                });
            }
        }
        return devices;
    }

    std::vector<AudioDeviceInfo> AudioStream::getOutputDevices() {
        std::vector<AudioDeviceInfo> devices;
        int numDevices = Pa_GetDeviceCount();
        int defaultOutputDevice = Pa_GetDefaultOutputDevice();

        for (int i = 0; i < numDevices; i++) {
            const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(i);
            if (deviceInfo->maxOutputChannels > 0) {
                devices.push_back({
                    i,
                    deviceInfo->name,
                    deviceInfo->maxInputChannels,
                    deviceInfo->maxOutputChannels,
                    deviceInfo->defaultSampleRate,
                    false,
                    (i == defaultOutputDevice)
                });
            }
        }
        return devices;
    }

    AudioDeviceInfo AudioStream::getDefaultInputDevice() {
        int defaultInputDevice = Pa_GetDefaultInputDevice();
        const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(defaultInputDevice);
        return {
            defaultInputDevice,
            deviceInfo->name,
            deviceInfo->maxInputChannels,
            deviceInfo->maxOutputChannels,
            deviceInfo->defaultSampleRate,
            true,
            false
        };
    }

    AudioDeviceInfo AudioStream::getDefaultOutputDevice() {
        int defaultOutputDevice = Pa_GetDefaultOutputDevice();
        const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(defaultOutputDevice);
        return {
            defaultOutputDevice,
            deviceInfo->name,
            deviceInfo->maxInputChannels,
            deviceInfo->maxOutputChannels,
            deviceInfo->defaultSampleRate,
            false,
            true
        };
    }

    long AudioStream::readStream(uint8_t* buffer, unsigned long frames) {
        if (!stream) {
            throw std::runtime_error("Stream is not open");
        }
        if (!isBlockingMode) {
            throw std::runtime_error("Write operation is only available in blocking mode");
        }
        if (!outputEnabled) {
            throw std::runtime_error("Output is not enabled for this stream");
        }
        if (frames == 0) {
            return 0;  // No frames to write
        }
        if (!buffer) {
            throw std::runtime_error("Invalid buffer pointer");
        }

        PaError err = Pa_ReadStream(stream, buffer, frames);
        if (err != paNoError) {
            if (err == paOutputUnderflowed) {
                return 0;  // Indicate no frames were written
            } else {
                throw std::runtime_error("Error writing to stream: " + std::string(Pa_GetErrorText(err)));
            }
        }

        return frames;
    }

    long AudioStream::writeStream(const uint8_t* buffer, unsigned long frames) {
        if (!stream) {
            throw std::runtime_error("Stream is not open");
        }
        if (!isBlockingMode) {
            throw std::runtime_error("Write operation is only available in blocking mode");
        }
        if (!outputEnabled) {
            throw std::runtime_error("Output is not enabled for this stream");
        }
        if (frames == 0) {
            return 0;  // No frames to write
        }
        if (!buffer) {
            throw std::runtime_error("Invalid buffer pointer");
        }

        unsigned long framesWritten = 0;
        const uint8_t* currentBuffer = buffer;

        while (framesWritten < frames) {
            long availableFrames = Pa_GetStreamWriteAvailable(stream);

            if (availableFrames == 0) {
                // No space available, wait a bit
                Pa_Sleep(1);
                continue;
            }
            unsigned long framesToWrite = std::min(static_cast<unsigned long>(availableFrames), frames - framesWritten);

            PaError err = Pa_WriteStream(stream, currentBuffer, framesToWrite);
            if (err != paNoError) {
                if (err == paOutputUnderflowed) {
                    std::cerr << "Warning: Output underflowed" << std::endl;
                    // In case of underflow, we'll try to continue
                    Pa_Sleep(1);
                    continue;
                } else {
                    throw std::runtime_error("Error writing to stream: " + std::string(Pa_GetErrorText(err)));
                }
            }

            framesWritten += framesToWrite;
            currentBuffer += framesToWrite * outputChannels * Pa_GetSampleSize(streamFormat);
        }

        return framesWritten;
    }

    long AudioStream::getStreamReadAvailable() {
        return Pa_GetStreamReadAvailable(stream);
    }

    long AudioStream::getStreamWriteAvailable() {
        return Pa_GetStreamWriteAvailable(stream);
    }

    int AudioStream::paCallback(const void* inputBuffer, void* outputBuffer,
                                unsigned long framesPerBuffer,
                                const PaStreamCallbackTimeInfo* timeInfo,
                                PaStreamCallbackFlags statusFlags,
                                void* userData) {
        AudioStream* stream = static_cast<AudioStream*>(userData);
        return stream->handleCallback(inputBuffer, outputBuffer, framesPerBuffer, timeInfo, statusFlags);
    }

    int AudioStream::handleCallback(const void* inputBuffer, void* outputBuffer,
                                    unsigned long framesPerBuffer,
                                    const PaStreamCallbackTimeInfo* timeInfo,
                                    PaStreamCallbackFlags statusFlags) {
        bool shouldContinue = true;

        if (inputEnabled && userInputCallback) {
            shouldContinue = userInputCallback((const char*)(inputBuffer),
                                               framesPerBuffer, streamFormat);
        }

        if (shouldContinue && outputEnabled && userOutputCallback) {
            shouldContinue = userOutputCallback((char*)(outputBuffer),
                                                framesPerBuffer, streamFormat);
        }

        if (!shouldContinue) {
            continueStreaming.store(false);
        }

        return continueStreaming.load() ? paContinue : paComplete;
    }

    int AudioStream::getDeviceCount() {
        return Pa_GetDeviceCount();
    }

    AudioDeviceInfo AudioStream::getDeviceInfoByIndex(int index) {
        const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(index);
        if (!deviceInfo) {
            throw std::runtime_error("Invalid device index");
        }

        AudioDeviceInfo info;
        info.index = index;
        info.name = deviceInfo->name;
        info.maxInputChannels = deviceInfo->maxInputChannels;
        info.maxOutputChannels = deviceInfo->maxOutputChannels;
        info.defaultSampleRate = deviceInfo->defaultSampleRate;
        info.isDefaultInput = (index == Pa_GetDefaultInputDevice());
        info.isDefaultOutput = (index == Pa_GetDefaultOutputDevice());

        return info;
    }

    void writeToDefaultOutput(const std::vector<uint8_t>& data, PaSampleFormat sampleFormat,
                              int channels, double sampleRate) {
        PaError err = Pa_Initialize();
        if (err != paNoError) {
            throw std::runtime_error("PortAudio initialization failed: " + std::string(Pa_GetErrorText(err)));
        }

        int outputDeviceIndex = Pa_GetDefaultOutputDevice();
        if (outputDeviceIndex == paNoDevice) {
            Pa_Terminate();
            throw std::runtime_error("No default output device found");
        }

        const PaDeviceInfo* deviceInfo = Pa_GetDeviceInfo(outputDeviceIndex);
        if (!deviceInfo) {
            Pa_Terminate();
            throw std::runtime_error("Failed to get device info for default output device");
        }

        if (channels <= 0) channels = deviceInfo->maxOutputChannels;
        if (sampleRate <= 0) sampleRate = deviceInfo->defaultSampleRate;

        PaStreamParameters outputParameters;
        outputParameters.device = outputDeviceIndex;
        outputParameters.channelCount = channels;
        outputParameters.sampleFormat = sampleFormat;
        outputParameters.suggestedLatency = deviceInfo->defaultLowOutputLatency;
        outputParameters.hostApiSpecificStreamInfo = nullptr;

        PaStream* stream;
        err = Pa_OpenStream(&stream, nullptr, &outputParameters, sampleRate, paFramesPerBufferUnspecified,
                            paClipOff, nullptr, nullptr);
        if (err != paNoError) {
            Pa_Terminate();
            throw std::runtime_error("Failed to open PortAudio stream: " + std::string(Pa_GetErrorText(err)));
        }

        err = Pa_StartStream(stream);
        if (err != paNoError) {
            Pa_CloseStream(stream);
            Pa_Terminate();
            throw std::runtime_error("Failed to start PortAudio stream: " + std::string(Pa_GetErrorText(err)));
        }

        const uint8_t* buffer = data.data();
        unsigned long totalFrames = data.size() / (channels * Pa_GetSampleSize(sampleFormat));
        unsigned long framesWritten = 0;

        while (framesWritten < totalFrames) {
            long availableFrames = Pa_GetStreamWriteAvailable(stream);
            if (availableFrames < 0) {
                Pa_StopStream(stream);
                Pa_CloseStream(stream);
                Pa_Terminate();
                throw std::runtime_error("Error getting available write frames: " + std::string(Pa_GetErrorText(availableFrames)));
            }

            unsigned long framesToWrite = std::min(static_cast<unsigned long>(availableFrames), totalFrames - framesWritten);
            if (framesToWrite == 0) {
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
                continue;
            }

            err = Pa_WriteStream(stream, buffer + framesWritten * channels * Pa_GetSampleSize(sampleFormat), framesToWrite);
            if (err != paNoError) {
                Pa_StopStream(stream);
                Pa_CloseStream(stream);
                Pa_Terminate();
                throw std::runtime_error("Error writing to stream: " + std::string(Pa_GetErrorText(err)));
            }

            framesWritten += framesToWrite;
        }

        err = Pa_StopStream(stream);
        if (err != paNoError) {
            Pa_CloseStream(stream);
            Pa_Terminate();
            throw std::runtime_error("Error stopping stream: " + std::string(Pa_GetErrorText(err)));
        }

        err = Pa_CloseStream(stream);
        if (err != paNoError) {
            Pa_Terminate();
            throw std::runtime_error("Error closing stream: " + std::string(Pa_GetErrorText(err)));
        }

        Pa_Terminate();
    }

    } // namespace stdstream