#include "system_audio_capture.h"

#if defined(__APPLE__)

#import <Foundation/Foundation.h>
#import <CoreAudio/CoreAudio.h>
#import <CoreAudio/AudioHardwareTapping.h>
#import <CoreAudio/CATapDescription.h>

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <deque>
#include <limits>
#include <mutex>
#include <string>
#include <vector>

namespace {

constexpr size_t kMaxQueuedChunks = 256;
constexpr size_t kDefaultDrainChunkLimit = 64;
constexpr AudioObjectID kUnknownObject = kAudioObjectUnknown;

struct OutputDeviceInfo {
    std::string uid;
    std::string label;
    double sampleRate;
    UInt32 channelCount;
    bool isDefault;
};

struct CapturedChunk {
    std::vector<float> left;
    std::vector<float> right;
    UInt32 channelCount = 2;
    double capturedAtMilliseconds = 0.0;
    uint64_t sequence = 0;
};

double monotonicMilliseconds() {
    const auto now = std::chrono::steady_clock::now().time_since_epoch();
    return std::chrono::duration<double, std::milli>(now).count();
}

std::string cfStringToStdString(CFStringRef value) {
    if (value == nullptr) {
        return {};
    }

    const CFIndex length = CFStringGetLength(value);
    const CFIndex maxBytes =
        CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
    std::vector<char> buffer(static_cast<size_t>(std::max<CFIndex>(1, maxBytes)), '\0');

    if (!CFStringGetCString(value, buffer.data(), maxBytes, kCFStringEncodingUTF8)) {
        return {};
    }

    return std::string(buffer.data());
}

NSString* toNSString(const std::string& value) {
    return [[NSString alloc] initWithUTF8String:value.c_str()];
}

template <typename T>
bool getPropertyData(AudioObjectID objectId,
                     AudioObjectPropertySelector selector,
                     AudioObjectPropertyScope scope,
                     AudioObjectPropertyElement element,
                     T* outValue) {
    if (outValue == nullptr) {
        return false;
    }

    AudioObjectPropertyAddress address{
        selector,
        scope,
        element,
    };

    UInt32 size = sizeof(T);
    return AudioObjectGetPropertyData(objectId, &address, 0, nullptr, &size, outValue) == noErr;
}

bool getDeviceStringProperty(AudioDeviceID deviceId,
                             AudioObjectPropertySelector selector,
                             std::string* outValue) {
    if (outValue == nullptr) {
        return false;
    }

    AudioObjectPropertyAddress address{
        selector,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMain,
    };

    CFStringRef stringValue = nullptr;
    UInt32 size = sizeof(stringValue);
    const OSStatus status =
        AudioObjectGetPropertyData(deviceId, &address, 0, nullptr, &size, &stringValue);
    if (status != noErr || stringValue == nullptr) {
        return false;
    }

    *outValue = cfStringToStdString(stringValue);
    CFRelease(stringValue);
    return !outValue->empty();
}

AudioDeviceID getDefaultOutputDeviceId() {
    AudioDeviceID deviceId = kUnknownObject;
    if (!getPropertyData(kAudioObjectSystemObject,
                         kAudioHardwarePropertyDefaultOutputDevice,
                         kAudioObjectPropertyScopeGlobal,
                         kAudioObjectPropertyElementMain,
                         &deviceId)) {
        return kUnknownObject;
    }
    return deviceId;
}

bool getDeviceNominalSampleRate(AudioDeviceID deviceId, double* outSampleRate) {
    if (outSampleRate == nullptr) {
        return false;
    }

    Float64 sampleRate = 0.0;
    if (!getPropertyData(deviceId,
                         kAudioDevicePropertyNominalSampleRate,
                         kAudioObjectPropertyScopeGlobal,
                         kAudioObjectPropertyElementMain,
                         &sampleRate)) {
        return false;
    }

    if (sampleRate <= 0.0) {
        return false;
    }

    *outSampleRate = static_cast<double>(sampleRate);
    return true;
}

UInt32 getOutputChannelCount(AudioDeviceID deviceId) {
    AudioObjectPropertyAddress address{
        kAudioDevicePropertyStreamConfiguration,
        kAudioDevicePropertyScopeOutput,
        kAudioObjectPropertyElementMain,
    };

    UInt32 size = 0;
    if (AudioObjectGetPropertyDataSize(deviceId, &address, 0, nullptr, &size) != noErr ||
        size == 0) {
        return 0;
    }

    std::vector<uint8_t> storage(size);
    auto* bufferList = reinterpret_cast<AudioBufferList*>(storage.data());
    if (AudioObjectGetPropertyData(deviceId, &address, 0, nullptr, &size, bufferList) !=
        noErr) {
        return 0;
    }

    UInt32 channelCount = 0;
    for (UInt32 index = 0; index < bufferList->mNumberBuffers; ++index) {
        channelCount += bufferList->mBuffers[index].mNumberChannels;
    }

    return channelCount;
}

std::vector<OutputDeviceInfo> enumerateOutputDevices() {
    AudioObjectPropertyAddress address{
        kAudioHardwarePropertyDevices,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMain,
    };

    UInt32 size = 0;
    if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &address, 0, nullptr, &size) !=
            noErr ||
        size == 0) {
        return {};
    }

    const UInt32 deviceCount = size / sizeof(AudioDeviceID);
    std::vector<AudioDeviceID> deviceIds(deviceCount, kUnknownObject);
    if (AudioObjectGetPropertyData(
            kAudioObjectSystemObject, &address, 0, nullptr, &size, deviceIds.data()) != noErr) {
        return {};
    }

    const AudioDeviceID defaultDeviceId = getDefaultOutputDeviceId();
    std::vector<OutputDeviceInfo> devices;
    devices.reserve(deviceCount);

    for (AudioDeviceID deviceId : deviceIds) {
        const UInt32 channelCount = getOutputChannelCount(deviceId);
        if (channelCount == 0) {
            continue;
        }

        std::string uid;
        if (!getDeviceStringProperty(deviceId, kAudioDevicePropertyDeviceUID, &uid)) {
            continue;
        }

        std::string label;
        if (!getDeviceStringProperty(deviceId, kAudioObjectPropertyName, &label)) {
            label = uid;
        }

        double sampleRate = 0.0;
        if (!getDeviceNominalSampleRate(deviceId, &sampleRate)) {
            sampleRate = 48000.0;
        }

        devices.push_back(OutputDeviceInfo{
            uid,
            label,
            sampleRate,
            channelCount,
            deviceId == defaultDeviceId,
        });
    }

    return devices;
}

bool getTapFormat(AudioObjectID tapId, AudioStreamBasicDescription* outFormat) {
    if (outFormat == nullptr) {
        return false;
    }

    AudioObjectPropertyAddress address{
        kAudioTapPropertyFormat,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMain,
    };

    UInt32 size = sizeof(AudioStreamBasicDescription);
    return AudioObjectGetPropertyData(tapId, &address, 0, nullptr, &size, outFormat) == noErr;
}

float decodeSignedIntegerSample(const uint8_t* data, UInt32 bytesPerSample, bool isBigEndian) {
    if (data == nullptr || bytesPerSample == 0 || bytesPerSample > 4) {
        return 0.0f;
    }

    int32_t rawValue = 0;
    if (isBigEndian) {
        for (UInt32 byteIndex = 0; byteIndex < bytesPerSample; ++byteIndex) {
            rawValue = (rawValue << 8) | data[byteIndex];
        }
    } else {
        for (UInt32 byteIndex = 0; byteIndex < bytesPerSample; ++byteIndex) {
            rawValue |= static_cast<int32_t>(data[byteIndex]) << (byteIndex * 8);
        }
    }

    const UInt32 totalBits = bytesPerSample * 8;
    const int32_t signMask = 1 << (totalBits - 1);
    if ((rawValue & signMask) != 0) {
        rawValue |= ~((1 << totalBits) - 1);
    }

    const double maxMagnitude = static_cast<double>((1u << (totalBits - 1)) - 1u);
    if (maxMagnitude <= 0.0) {
        return 0.0f;
    }

    return static_cast<float>(static_cast<double>(rawValue) / maxMagnitude);
}

float readSampleFromFormat(const uint8_t* data,
                           const AudioStreamBasicDescription& format,
                           UInt32 sampleIndex) {
    if (data == nullptr) {
        return 0.0f;
    }

    const UInt32 bytesPerChannel = format.mBitsPerChannel / 8;
    if (bytesPerChannel == 0) {
        return 0.0f;
    }

    const uint8_t* samplePtr = data + static_cast<size_t>(sampleIndex) * bytesPerChannel;
    const bool isFloat = (format.mFormatFlags & kAudioFormatFlagIsFloat) != 0;
    const bool isBigEndian = (format.mFormatFlags & kAudioFormatFlagIsBigEndian) != 0;

    if (isFloat && format.mBitsPerChannel == 32) {
        Float32 value = 0.0f;
        std::memcpy(&value, samplePtr, sizeof(Float32));
        return value;
    }

    if (isFloat && format.mBitsPerChannel == 64) {
        Float64 value = 0.0;
        std::memcpy(&value, samplePtr, sizeof(Float64));
        return static_cast<float>(value);
    }

    if ((format.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0) {
        return decodeSignedIntegerSample(samplePtr, bytesPerChannel, isBigEndian);
    }

    return 0.0f;
}

bool isFormatInterleaved(const AudioStreamBasicDescription& format) {
    return (format.mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0;
}

std::string formatStatusMessage(const char* operation, OSStatus status) {
    return std::string(operation) + " failed (" + std::to_string(static_cast<int>(status)) + ")";
}

class MacOSNativeCaptureEngine final : public Prism::Capture::SystemAudioCapture {
public:
    ~MacOSNativeCaptureEngine() override {
        stop();
    }

    Prism::Capture::Support getSupport() const override {
        if (@available(macOS 14.2, *)) {
            return {true, {}};
        }
        return {false, "Native output-device capture requires macOS 14.2 or newer."};
    }

    std::vector<Prism::Capture::OutputDevice> listOutputDevices() override {
        std::vector<Prism::Capture::OutputDevice> result;
        if (!isSupported()) {
            return result;
        }
        const auto devices = enumerateOutputDevices();
        result.reserve(devices.size());
        for (const auto& device : devices) {
            result.push_back({
                device.uid,
                device.label,
                device.sampleRate,
                static_cast<uint32_t>(device.channelCount),
                device.isDefault,
            });
        }
        return result;
    }

    bool start(const std::string& requestedDeviceId,
               Prism::Capture::StartResult* result,
               std::string* errorMessage) override {
        const auto support = getSupport();
        if (!support.available) {
            if (errorMessage != nullptr) {
                *errorMessage = support.reason;
            }
            return false;
        }
        if (!startInternal(requestedDeviceId, errorMessage)) {
            return false;
        }
        if (result != nullptr) {
            std::lock_guard<std::mutex> lock(stateMutex_);
            result->sampleRate = sampleRate_;
            result->channelCount = static_cast<uint32_t>(channelCount_);
            result->deviceId = activeDeviceUid_;
            result->deviceLabel = activeDeviceLabel_;
        }
        return true;
    }

    void stop() override {
        std::lock_guard<std::mutex> lock(stateMutex_);
        stopLocked();
    }

    Prism::Capture::DrainResult drain(size_t maxChunks) override {
        const size_t drainLimit =
            maxChunks == 0 ? kDefaultDrainChunkLimit : std::min(maxChunks, kMaxQueuedChunks);
        std::deque<CapturedChunk> drained;
        Prism::Capture::DrainResult result;
        {
            std::lock_guard<std::mutex> queueLock(chunkMutex_);
            result.overwriteCount = overwriteCount_;
            const size_t count = std::min(drainLimit, chunkQueue_.size());
            for (size_t index = 0; index < count; ++index) {
                drained.push_back(std::move(chunkQueue_.front()));
                chunkQueue_.pop_front();
            }
            result.queueDepth = chunkQueue_.size();
        }
        result.chunks.reserve(drained.size());
        while (!drained.empty()) {
            auto chunk = std::move(drained.front());
            drained.pop_front();
            result.chunks.push_back({
                std::move(chunk.left),
                std::move(chunk.right),
                static_cast<uint32_t>(chunk.channelCount),
                chunk.capturedAtMilliseconds,
                chunk.sequence,
            });
        }
        return result;
    }

    double nowMilliseconds() const override {
        return monotonicMilliseconds();
    }

    const char* backendName() const override {
        return "CoreAudio";
    }

private:
    static OSStatus StaticIOProc(AudioObjectID inDevice,
                                 const AudioTimeStamp* inNow,
                                 const AudioBufferList* inInputData,
                                 const AudioTimeStamp* inInputTime,
                                 AudioBufferList* outOutputData,
                                 const AudioTimeStamp* inOutputTime,
                                 void* inClientData) {
        auto* self = static_cast<MacOSNativeCaptureEngine*>(inClientData);
        if (self == nullptr) {
            return noErr;
        }
        return self->handleIO(
            inDevice, inNow, inInputData, inInputTime, outOutputData, inOutputTime);
    }

    OSStatus handleIO(AudioObjectID,
                      const AudioTimeStamp*,
                      const AudioBufferList* inputData,
                      const AudioTimeStamp*,
                      AudioBufferList*,
                      const AudioTimeStamp*) {
        AudioStreamBasicDescription format{};
        UInt32 channelCount = 0;

        {
            std::lock_guard<std::mutex> stateLock(stateMutex_);
            if (!active_ || inputData == nullptr) {
                return noErr;
            }
            format = tapFormat_;
            channelCount = channelCount_;
        }

        if (inputData->mNumberBuffers == 0 || format.mBytesPerFrame == 0) {
            return noErr;
        }

        const bool interleaved = isFormatInterleaved(format);
        const AudioBuffer& firstBuffer = inputData->mBuffers[0];
        const UInt32 frames =
            format.mBytesPerFrame == 0 ? 0 : firstBuffer.mDataByteSize / format.mBytesPerFrame;
        if (frames == 0) {
            return noErr;
        }

        CapturedChunk chunk;
        chunk.channelCount = channelCount;
        chunk.capturedAtMilliseconds = monotonicMilliseconds();

        {
            std::lock_guard<std::mutex> stateLock(stateMutex_);
            chunk.sequence = ++sequence_;
        }

        chunk.left.resize(frames);
        chunk.right.resize(frames);

        if (interleaved) {
            const uint8_t* rawData = static_cast<const uint8_t*>(firstBuffer.mData);
            for (UInt32 frameIndex = 0; frameIndex < frames; ++frameIndex) {
                const UInt32 sampleBaseIndex = frameIndex * std::max<UInt32>(1, channelCount);
                const float leftSample =
                    readSampleFromFormat(rawData, format, sampleBaseIndex);
                const float rightSample = channelCount > 1
                    ? readSampleFromFormat(rawData, format, sampleBaseIndex + 1)
                    : leftSample;
                chunk.left[frameIndex] = leftSample;
                chunk.right[frameIndex] = rightSample;
            }
        } else {
            const uint8_t* leftData = static_cast<const uint8_t*>(inputData->mBuffers[0].mData);
            const uint8_t* rightData = static_cast<const uint8_t*>(
                inputData->mNumberBuffers > 1 ? inputData->mBuffers[1].mData
                                              : inputData->mBuffers[0].mData);

            for (UInt32 frameIndex = 0; frameIndex < frames; ++frameIndex) {
                chunk.left[frameIndex] =
                    readSampleFromFormat(leftData, format, frameIndex);
                chunk.right[frameIndex] = inputData->mNumberBuffers > 1
                    ? readSampleFromFormat(rightData, format, frameIndex)
                    : chunk.left[frameIndex];
            }
        }

        {
            std::lock_guard<std::mutex> queueLock(chunkMutex_);
            if (chunkQueue_.size() >= kMaxQueuedChunks) {
                chunkQueue_.pop_front();
                ++overwriteCount_;
            }
            chunkQueue_.push_back(std::move(chunk));
        }

        return noErr;
    }

    bool startInternal(const std::string& requestedDeviceUid, std::string* outErrorMessage) {
        std::lock_guard<std::mutex> lock(stateMutex_);
        stopLocked();

        if (!isSupported()) {
            if (outErrorMessage != nullptr) {
                *outErrorMessage =
                    "Native output-device capture requires macOS 14.2 or newer.";
            }
            return false;
        }

        const auto devices = enumerateOutputDevices();
        if (devices.empty()) {
            if (outErrorMessage != nullptr) {
                *outErrorMessage = "No macOS output devices are available.";
            }
            return false;
        }

        const OutputDeviceInfo* selected = nullptr;
        if (!requestedDeviceUid.empty()) {
            for (const auto& device : devices) {
                if (device.uid == requestedDeviceUid) {
                    selected = &device;
                    break;
                }
            }
            if (selected == nullptr) {
                if (outErrorMessage != nullptr) {
                    *outErrorMessage =
                        "The selected macOS output device is no longer available.";
                }
                return false;
            }
        } else {
            for (const auto& device : devices) {
                if (device.isDefault) {
                    selected = &device;
                    break;
                }
            }
            if (selected == nullptr) {
                selected = &devices.front();
            }
        }

        if (@available(macOS 14.2, *)) {
            @autoreleasepool {
                NSString* deviceUID = toNSString(selected->uid);
                NSString* deviceLabel = toNSString(selected->label);
                NSArray<NSNumber*>* excludedProcesses = @[];
                CATapDescription* tapDescription =
                    [[CATapDescription alloc] initExcludingProcesses:excludedProcesses
                                                         andDeviceUID:deviceUID
                                                           withStream:0];
                tapDescription.name = [NSString stringWithFormat:@"Prism Tap %@", deviceLabel];
                tapDescription.UUID = [NSUUID UUID];
                tapDescription.privateTap = YES;
                tapDescription.muteBehavior = CATapUnmuted;

                const OSStatus tapStatus =
                    AudioHardwareCreateProcessTap(tapDescription, &tapId_);
                if (tapStatus != noErr) {
                    if (outErrorMessage != nullptr) {
                        *outErrorMessage =
                            formatStatusMessage("AudioHardwareCreateProcessTap", tapStatus);
                    }
                    tapId_ = kUnknownObject;
                    return false;
                }

                NSString* aggregateUID = [NSString
                    stringWithFormat:@"com.astra.prism.capture.%@", [NSUUID UUID].UUIDString];
                NSDictionary* aggregateDescription = @{
                    [NSString stringWithUTF8String:kAudioAggregateDeviceNameKey]:
                        [NSString stringWithFormat:@"Prism Capture %@", deviceLabel],
                    [NSString stringWithUTF8String:kAudioAggregateDeviceUIDKey]: aggregateUID,
                    [NSString stringWithUTF8String:kAudioAggregateDeviceIsPrivateKey]: @YES,
                    [NSString stringWithUTF8String:kAudioAggregateDeviceMainSubDeviceKey]:
                        deviceUID,
                    [NSString stringWithUTF8String:kAudioAggregateDeviceTapAutoStartKey]: @YES,
                    [NSString stringWithUTF8String:kAudioAggregateDeviceSubDeviceListKey]: @[
                        @{
                            [NSString stringWithUTF8String:kAudioSubDeviceUIDKey]: deviceUID,
                            [NSString stringWithUTF8String:kAudioSubDeviceNameKey]: deviceLabel,
                            [NSString stringWithUTF8String:kAudioSubDeviceDriftCompensationKey]:
                                @YES,
                            [NSString
                                stringWithUTF8String:kAudioSubDeviceDriftCompensationQualityKey]:
                                @(kAudioAggregateDriftCompensationMediumQuality),
                        }
                    ],
                    [NSString stringWithUTF8String:kAudioAggregateDeviceTapListKey]: @[
                        @{
                            [NSString stringWithUTF8String:kAudioSubTapUIDKey]:
                                tapDescription.UUID.UUIDString,
                            [NSString stringWithUTF8String:kAudioSubTapDriftCompensationKey]:
                                @YES,
                            [NSString
                                stringWithUTF8String:kAudioSubTapDriftCompensationQualityKey]:
                                @(kAudioAggregateDriftCompensationMediumQuality),
                        }
                    ],
                };

                const OSStatus aggregateStatus = AudioHardwareCreateAggregateDevice(
                    (__bridge CFDictionaryRef)aggregateDescription, &aggregateDeviceId_);
                if (aggregateStatus != noErr) {
                    if (outErrorMessage != nullptr) {
                        *outErrorMessage = formatStatusMessage(
                            "AudioHardwareCreateAggregateDevice", aggregateStatus);
                    }
                    AudioHardwareDestroyProcessTap(tapId_);
                    tapId_ = kUnknownObject;
                    aggregateDeviceId_ = kUnknownObject;
                    return false;
                }
            }
        }

        if (!getTapFormat(tapId_, &tapFormat_)) {
            tapFormat_ = AudioStreamBasicDescription{};
            tapFormat_.mSampleRate = selected->sampleRate;
            tapFormat_.mChannelsPerFrame = std::max<UInt32>(2, selected->channelCount);
            tapFormat_.mBitsPerChannel = 32;
            tapFormat_.mBytesPerFrame =
                tapFormat_.mChannelsPerFrame * sizeof(Float32);
            tapFormat_.mFramesPerPacket = 1;
            tapFormat_.mBytesPerPacket =
                tapFormat_.mBytesPerFrame * tapFormat_.mFramesPerPacket;
            tapFormat_.mFormatID = kAudioFormatLinearPCM;
            tapFormat_.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
        }

        const OSStatus ioProcStatus =
            AudioDeviceCreateIOProcID(aggregateDeviceId_, StaticIOProc, this, &ioProcId_);
        if (ioProcStatus != noErr) {
            if (outErrorMessage != nullptr) {
                *outErrorMessage =
                    formatStatusMessage("AudioDeviceCreateIOProcID", ioProcStatus);
            }
            stopLocked();
            return false;
        }

        const OSStatus startStatus = AudioDeviceStart(aggregateDeviceId_, ioProcId_);
        if (startStatus != noErr) {
            if (outErrorMessage != nullptr) {
                *outErrorMessage = formatStatusMessage("AudioDeviceStart", startStatus);
            }
            stopLocked();
            return false;
        }

        {
            std::lock_guard<std::mutex> queueLock(chunkMutex_);
            chunkQueue_.clear();
            overwriteCount_ = 0;
        }

        active_ = true;
        activeDeviceUid_ = selected->uid;
        activeDeviceLabel_ = selected->label;
        sampleRate_ = tapFormat_.mSampleRate > 0 ? tapFormat_.mSampleRate : selected->sampleRate;
        channelCount_ =
            std::max<UInt32>(1, tapFormat_.mChannelsPerFrame > 0 ? tapFormat_.mChannelsPerFrame
                                                                 : selected->channelCount);
        sequence_ = 0;
        return true;
    }

    void stopLocked() {
        active_ = false;

        if (aggregateDeviceId_ != kUnknownObject && ioProcId_ != nullptr) {
            AudioDeviceStop(aggregateDeviceId_, ioProcId_);
            AudioDeviceDestroyIOProcID(aggregateDeviceId_, ioProcId_);
            ioProcId_ = nullptr;
        }

        if (aggregateDeviceId_ != kUnknownObject) {
            AudioHardwareDestroyAggregateDevice(aggregateDeviceId_);
            aggregateDeviceId_ = kUnknownObject;
        }

        if (tapId_ != kUnknownObject) {
            if (@available(macOS 14.2, *)) {
                AudioHardwareDestroyProcessTap(tapId_);
            }
            tapId_ = kUnknownObject;
        }

        tapFormat_ = AudioStreamBasicDescription{};
        activeDeviceUid_.clear();
        activeDeviceLabel_.clear();
        sampleRate_ = 48000.0;
        channelCount_ = 2;
        sequence_ = 0;

        std::lock_guard<std::mutex> queueLock(chunkMutex_);
        chunkQueue_.clear();
        overwriteCount_ = 0;
    }

    bool isSupported() const {
        if (@available(macOS 14.2, *)) {
            return true;
        }
        return false;
    }

    mutable std::mutex stateMutex_;
    std::mutex chunkMutex_;
    std::deque<CapturedChunk> chunkQueue_;
    uint64_t overwriteCount_ = 0;
    uint64_t sequence_ = 0;

    AudioObjectID tapId_ = kUnknownObject;
    AudioObjectID aggregateDeviceId_ = kUnknownObject;
    AudioDeviceIOProcID ioProcId_ = nullptr;
    AudioStreamBasicDescription tapFormat_{};

    bool active_ = false;
    std::string activeDeviceUid_;
    std::string activeDeviceLabel_;
    double sampleRate_ = 48000.0;
    UInt32 channelCount_ = 2;
};

}  // namespace

namespace Prism::Capture {

std::unique_ptr<SystemAudioCapture> createSystemAudioCapture() {
    return std::make_unique<MacOSNativeCaptureEngine>();
}

}  // namespace Prism::Capture

#endif  // defined(__APPLE__)
