#include "AudioVoiceEngine.hpp"
#include "logvisor/logvisor.hpp"

#include <AudioToolbox/AudioToolbox.h>
#include <CoreMIDI/CoreMIDI.h>
#include <CoreAudio/HostTime.h>

#include <mutex>
#include <condition_variable>

namespace boo
{
static logvisor::Module Log("boo::AQS");

static AudioChannel AQSChannelToBooChannel(AudioChannelLabel ch)
{
    switch (ch)
    {
    case kAudioChannelLabel_Left:
        return AudioChannel::FrontLeft;
    case kAudioChannelLabel_Right:
        return AudioChannel::FrontRight;
    case kAudioChannelLabel_LeftSurround:
        return AudioChannel::RearLeft;
    case kAudioChannelLabel_RightSurround:
        return AudioChannel::RearRight;
    case kAudioChannelLabel_Center:
        return AudioChannel::FrontCenter;
    case kAudioChannelLabel_LFEScreen:
        return AudioChannel::LFE;
    case kAudioChannelLabel_LeftSurroundDirect:
        return AudioChannel::RearLeft;
    case kAudioChannelLabel_RightSurroundDirect:
        return AudioChannel::SideRight;
    }
    return AudioChannel::Unknown;
}

struct AQSAudioVoiceEngine : BaseAudioVoiceEngine
{
    AudioQueueRef m_queue = nullptr;
    AudioQueueBufferRef m_buffers[3];
    size_t m_frameBytes;

    MIDIClientRef m_midiClient = 0;

    std::mutex m_engineMutex;
    std::condition_variable m_engineEnterCv;
    std::condition_variable m_engineLeaveCv;
    bool m_inRetrace = false;
    bool m_inCb = false;
    bool m_cbRunning = true;

    static void Callback(AQSAudioVoiceEngine* engine, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer)
    {
        if (!engine->m_cbRunning)
            return;

        std::unique_lock<std::mutex> lk(engine->m_engineMutex);
        engine->m_inCb = true;
        if (!engine->m_inRetrace)
        {
            if (engine->m_engineEnterCv.wait_for(lk,
            std::chrono::nanoseconds(engine->m_mixInfo.m_periodFrames * 1000000000 /
                                     size_t(engine->m_mixInfo.m_sampleRate))) == std::cv_status::timeout ||
                                     !engine->m_inRetrace)
            {
                inBuffer->mAudioDataByteSize = engine->m_frameBytes;
                memset(inBuffer->mAudioData, 0, engine->m_frameBytes);
                AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nullptr);
                engine->m_engineLeaveCv.notify_one();
                engine->m_inCb = false;
                return;
            }
        }

        engine->_pumpAndMixVoices(engine->m_mixInfo.m_periodFrames,
                                  reinterpret_cast<float*>(inBuffer->mAudioData));
        inBuffer->mAudioDataByteSize = engine->m_frameBytes;
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nullptr);

        engine->m_engineLeaveCv.notify_one();
        engine->m_inCb = false;
    }

    static void DummyCallback(AQSAudioVoiceEngine* engine, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer) {}

    AudioChannelSet _getAvailableSet()
    {
        const unsigned chCount = 8;
        AudioStreamBasicDescription desc = {};
        desc.mSampleRate = 96000;
        desc.mFormatID = kAudioFormatLinearPCM;
        desc.mFormatFlags = kLinearPCMFormatFlagIsFloat;
        desc.mBytesPerPacket = chCount * 4;
        desc.mFramesPerPacket = 1;
        desc.mBytesPerFrame = chCount * 4;
        desc.mChannelsPerFrame = chCount;
        desc.mBitsPerChannel = 32;

        AudioQueueRef queue;
        if (AudioQueueNewOutput(&desc, AudioQueueOutputCallback(DummyCallback),
                                this, nullptr, nullptr, 0, &queue))
        {
            Log.report(logvisor::Error, "unable to create output audio queue");
            return AudioChannelSet::Unknown;
        }

        UInt32 hwChannels;
        UInt32 channelsSz = sizeof(UInt32);
        if (AudioQueueGetProperty(queue, kAudioQueueDeviceProperty_NumberChannels, &hwChannels, &channelsSz))
        {
            Log.report(logvisor::Error, "unable to get channel count from audio queue");
            AudioQueueDispose(queue, true);
            return AudioChannelSet::Unknown;
        }

        AudioQueueDispose(queue, true);

        switch (hwChannels)
        {
        case 2:
            return AudioChannelSet::Stereo;
        case 4:
            return AudioChannelSet::Quad;
        case 6:
            return AudioChannelSet::Surround51;
        case 8:
            return AudioChannelSet::Surround71;
        default: break;
        }

        return AudioChannelSet::Unknown;
    }

    std::vector<std::pair<std::string, std::string>> enumerateMIDIDevices() const
    {
        if (!m_midiClient)
            return {};

        std::vector<std::pair<std::string, std::string>> ret;

        ItemCount numDevices = MIDIGetNumberOfDevices();
        ret.reserve(numDevices);
        for (int i=int(numDevices)-1 ; i>=0 ; --i)
        {
            MIDIDeviceRef dev = MIDIGetDevice(i);
            if (!dev)
                continue;

            SInt32 idNum;
            if (MIDIObjectGetIntegerProperty(dev, kMIDIPropertyUniqueID, &idNum))
                continue;

            CFStringRef namestr;
            const char* nameCstr;
            if (MIDIObjectGetStringProperty(dev, kMIDIPropertyName, &namestr))
                continue;

            if (!(nameCstr = CFStringGetCStringPtr(namestr, kCFStringEncodingUTF8)))
            {
                CFRelease(namestr);
                continue;
            }

            char idStr[9];
            snprintf(idStr, 9, "%08X\n", idNum);
            ret.push_back(std::make_pair(std::string(idStr),
                                         std::string(nameCstr)));

            CFRelease(namestr);
        }

        return ret;
    }

    static MIDIDeviceRef LookupMIDIDevice(const char* name)
    {
        ItemCount numDevices = MIDIGetNumberOfDevices();
        for (ItemCount i=0 ; i<numDevices ; ++i)
        {
            MIDIDeviceRef dev = MIDIGetDevice(i);
            if (!dev)
                continue;

            SInt32 idNum;
            if (MIDIObjectGetIntegerProperty(dev, kMIDIPropertyUniqueID, &idNum))
                continue;

            char idStr[9];
            snprintf(idStr, 9, "%08X\n", idNum);
            if (strcmp(idStr, name))
                continue;

            return dev;
        }

        return {};
    }

    static MIDIEndpointRef LookupMIDISource(const char* name)
    {
        MIDIDeviceRef dev = LookupMIDIDevice(name);
        if (!dev)
            return {};

        ItemCount numEnt = MIDIDeviceGetNumberOfEntities(dev);
        for (ItemCount i=0 ; i<numEnt ; ++i)
        {
            MIDIEntityRef ent = MIDIDeviceGetEntity(dev, i);
            if (ent)
            {
                ItemCount numSrc = MIDIEntityGetNumberOfSources(ent);
                for (ItemCount s=0 ; s<numSrc ; ++s)
                {
                    MIDIEndpointRef src = MIDIEntityGetSource(ent, s);
                    if (src)
                        return src;
                }
            }
        }

        return {};
    }

    static MIDIEndpointRef LookupMIDIDest(const char* name)
    {
        MIDIDeviceRef dev = LookupMIDIDevice(name);
        if (!dev)
            return {};

        ItemCount numEnt = MIDIDeviceGetNumberOfEntities(dev);
        for (ItemCount i=0 ; i<numEnt ; ++i)
        {
            MIDIEntityRef ent = MIDIDeviceGetEntity(dev, i);
            if (ent)
            {
                ItemCount numDest = MIDIEntityGetNumberOfDestinations(ent);
                for (ItemCount d=0 ; d<numDest ; ++d)
                {
                    MIDIEndpointRef dst = MIDIEntityGetDestination(ent, d);
                    if (dst)
                        return dst;
                }
            }
        }

        return {};
    }

    static void MIDIReceiveProc(const MIDIPacketList* pktlist,
                                IMIDIReceiver* readProcRefCon,
                                void*)
    {
        const MIDIPacket* packet = &pktlist->packet[0];
        for (int i=0 ; i<pktlist->numPackets ; ++i)
        {
            std::vector<uint8_t> bytes(std::cbegin(packet->data), std::cbegin(packet->data) + packet->length);
            readProcRefCon->m_receiver(std::move(bytes), AudioConvertHostTimeToNanos(packet->timeStamp) / 1.0e9);
            packet = MIDIPacketNext(packet);
        }
    }

    struct MIDIIn : public IMIDIIn
    {
        MIDIEndpointRef m_midi = 0;
        MIDIPortRef m_midiPort = 0;

        MIDIIn(bool virt, ReceiveFunctor&& receiver)
        : IMIDIIn(virt, std::move(receiver)) {}

        ~MIDIIn()
        {
            if (m_midi)
                MIDIEndpointDispose(m_midi);
            if (m_midiPort)
                MIDIPortDispose(m_midiPort);
        }

        std::string description() const
        {
            CFStringRef namestr;
            const char* nameCstr;
            if (MIDIObjectGetStringProperty(m_midi, kMIDIPropertyName, &namestr))
                return {};

            if (!(nameCstr = CFStringGetCStringPtr(namestr, kCFStringEncodingUTF8)))
            {
                CFRelease(namestr);
                return {};
            }

            CFRelease(namestr);
            return nameCstr;
        }
    };

    struct MIDIOut : public IMIDIOut
    {
        MIDIEndpointRef m_midi = 0;
        MIDIPortRef m_midiPort = 0;

        MIDIOut(bool virt)
        : IMIDIOut(virt) {}

        ~MIDIOut()
        {
            if (m_midi)
                MIDIEndpointDispose(m_midi);
            if (m_midiPort)
                MIDIPortDispose(m_midiPort);
        }

        std::string description() const
        {
            CFStringRef namestr;
            const char* nameCstr;
            if (MIDIObjectGetStringProperty(m_midi, kMIDIPropertyName, &namestr))
                return {};

            if (!(nameCstr = CFStringGetCStringPtr(namestr, kCFStringEncodingUTF8)))
            {
                CFRelease(namestr);
                return {};
            }

            CFRelease(namestr);
            return nameCstr;
        }

        size_t send(const void* buf, size_t len) const
        {
            union
            {
                MIDIPacketList head;
                Byte storage[512];
            } list;
            MIDIPacket* curPacket = MIDIPacketListInit(&list.head);
            if (MIDIPacketListAdd(&list.head, sizeof(list), curPacket, AudioGetCurrentHostTime(),
                                  len, reinterpret_cast<const Byte*>(buf)))
            {
                if (m_midiPort)
                    MIDISend(m_midiPort, m_midi, &list.head);
                else
                    MIDIReceived(m_midi, &list.head);
                return len;
            }
            return 0;
        }
    };

    struct MIDIInOut : public IMIDIInOut
    {
        MIDIEndpointRef m_midiIn = 0;
        MIDIPortRef m_midiPortIn = 0;
        MIDIEndpointRef m_midiOut = 0;
        MIDIPortRef m_midiPortOut = 0;

        MIDIInOut(bool virt, ReceiveFunctor&& receiver)
        : IMIDIInOut(virt, std::move(receiver)) {}

        ~MIDIInOut()
        {
            if (m_midiIn)
                MIDIEndpointDispose(m_midiIn);
            if (m_midiPortIn)
                MIDIPortDispose(m_midiPortIn);
            if (m_midiOut)
                MIDIEndpointDispose(m_midiOut);
            if (m_midiPortOut)
                MIDIPortDispose(m_midiPortOut);
        }

        std::string description() const
        {
            CFStringRef namestr;
            const char* nameCstr;
            if (MIDIObjectGetStringProperty(m_midiIn, kMIDIPropertyName, &namestr))
                return {};

            if (!(nameCstr = CFStringGetCStringPtr(namestr, kCFStringEncodingUTF8)))
            {
                CFRelease(namestr);
                return {};
            }

            CFRelease(namestr);
            return nameCstr;
        }

        size_t send(const void* buf, size_t len) const
        {
            union
            {
                MIDIPacketList head;
                Byte storage[512];
            } list;
            MIDIPacket* curPacket = MIDIPacketListInit(&list.head);
            if (MIDIPacketListAdd(&list.head, sizeof(list), curPacket, AudioGetCurrentHostTime(),
                                  len, reinterpret_cast<const Byte*>(buf)))
            {
                if (m_midiPortOut)
                    MIDISend(m_midiPortOut, m_midiOut, &list.head);
                else
                    MIDIReceived(m_midiOut, &list.head);
                return len;
            }
            return 0;
        }
    };

    unsigned m_midiInCounter = 0;
    unsigned m_midiOutCounter = 0;

    std::unique_ptr<IMIDIIn> newVirtualMIDIIn(ReceiveFunctor&& receiver)
    {
        if (!m_midiClient)
            return {};

        std::unique_ptr<IMIDIIn> ret = std::make_unique<MIDIIn>(true, std::move(receiver));
        if (!ret)
            return {};

        char name[256];
        snprintf(name, 256, "Boo MIDI Virtual In %u", m_midiInCounter++);
        CFStringRef midiName = CFStringCreateWithCStringNoCopy(nullptr, name, kCFStringEncodingUTF8, kCFAllocatorNull);
        OSStatus stat;
        if ((stat = MIDIDestinationCreate(m_midiClient, midiName, MIDIReadProc(MIDIReceiveProc),
                                  static_cast<IMIDIReceiver*>(ret.get()),
                                  &static_cast<MIDIIn&>(*ret).m_midi)))
            ret.reset();
        CFRelease(midiName);

        return ret;
    }

    std::unique_ptr<IMIDIOut> newVirtualMIDIOut()
    {
        if (!m_midiClient)
            return {};

        std::unique_ptr<IMIDIOut> ret = std::make_unique<MIDIOut>(true);
        if (!ret)
            return {};

        char name[256];
        snprintf(name, 256, "Boo MIDI Virtual Out %u", m_midiOutCounter++);
        CFStringRef midiName = CFStringCreateWithCStringNoCopy(nullptr, name, kCFStringEncodingUTF8, kCFAllocatorNull);
        if (MIDISourceCreate(m_midiClient, midiName, &static_cast<MIDIOut&>(*ret).m_midi))
            ret.reset();
        CFRelease(midiName);

        return ret;
    }

    std::unique_ptr<IMIDIInOut> newVirtualMIDIInOut(ReceiveFunctor&& receiver)
    {
        if (!m_midiClient)
            return {};

        std::unique_ptr<IMIDIInOut> ret = std::make_unique<MIDIInOut>(true, std::move(receiver));
        if (!ret)
            return {};

        char name[256];
        snprintf(name, 256, "Boo MIDI Virtual In %u", m_midiInCounter++);
        CFStringRef midiName = CFStringCreateWithCStringNoCopy(nullptr, name, kCFStringEncodingUTF8, kCFAllocatorNull);
        if (MIDIDestinationCreate(m_midiClient, midiName, MIDIReadProc(MIDIReceiveProc),
                                  static_cast<IMIDIReceiver*>(ret.get()),
                                  &static_cast<MIDIInOut&>(*ret).m_midiIn))
            ret.reset();
        CFRelease(midiName);

        if (!ret)
            return {};

        snprintf(name, 256, "Boo MIDI Virtual Out %u", m_midiOutCounter++);
        midiName = CFStringCreateWithCStringNoCopy(nullptr, name, kCFStringEncodingUTF8, kCFAllocatorNull);
        if (MIDISourceCreate(m_midiClient, midiName, &static_cast<MIDIInOut&>(*ret).m_midiOut))
            ret.reset();
        CFRelease(midiName);

        return ret;
    }

    std::unique_ptr<IMIDIIn> newRealMIDIIn(const char* name, ReceiveFunctor&& receiver)
    {
        if (!m_midiClient)
            return {};

        MIDIEndpointRef src = LookupMIDISource(name);
        if (!src)
            return {};

        std::unique_ptr<IMIDIIn> ret = std::make_unique<MIDIIn>(false, std::move(receiver));
        if (!ret)
            return {};

        char mname[256];
        snprintf(mname, 256, "Boo MIDI Real In %u", m_midiInCounter++);
        CFStringRef midiName = CFStringCreateWithCStringNoCopy(nullptr, mname, kCFStringEncodingUTF8, kCFAllocatorNull);
        if (MIDIInputPortCreate(m_midiClient, midiName, MIDIReadProc(MIDIReceiveProc),
                                static_cast<IMIDIReceiver*>(ret.get()),
                                &static_cast<MIDIIn&>(*ret).m_midiPort))
            ret.reset();
        else
            MIDIPortConnectSource(static_cast<MIDIIn&>(*ret).m_midiPort, src, nullptr);
        CFRelease(midiName);

        return ret;
    }

    std::unique_ptr<IMIDIOut> newRealMIDIOut(const char* name)
    {
        if (!m_midiClient)
            return {};

        MIDIEndpointRef dst = LookupMIDIDest(name);
        if (!dst)
            return {};

        std::unique_ptr<IMIDIOut> ret = std::make_unique<MIDIOut>(false);
        if (!ret)
            return {};

        char mname[256];
        snprintf(mname, 256, "Boo MIDI Real Out %u", m_midiOutCounter++);
        CFStringRef midiName = CFStringCreateWithCStringNoCopy(nullptr, mname, kCFStringEncodingUTF8, kCFAllocatorNull);
        if (MIDIOutputPortCreate(m_midiClient, midiName, &static_cast<MIDIOut&>(*ret).m_midiPort))
            ret.reset();
        else
            static_cast<MIDIOut&>(*ret).m_midi = dst;
        CFRelease(midiName);

        return ret;
    }

    std::unique_ptr<IMIDIInOut> newRealMIDIInOut(const char* name, ReceiveFunctor&& receiver)
    {
        if (!m_midiClient)
            return {};

        MIDIEndpointRef src = LookupMIDISource(name);
        if (!src)
            return {};

        MIDIEndpointRef dst = LookupMIDIDest(name);
        if (!dst)
            return {};

        std::unique_ptr<IMIDIInOut> ret = std::make_unique<MIDIInOut>(false, std::move(receiver));
        if (!ret)
            return {};

        char mname[256];
        snprintf(mname, 256, "Boo MIDI Real In %u", m_midiInCounter++);
        CFStringRef midiName = CFStringCreateWithCStringNoCopy(nullptr, mname, kCFStringEncodingUTF8, kCFAllocatorNull);
        if (MIDIInputPortCreate(m_midiClient, midiName, MIDIReadProc(MIDIReceiveProc),
                                static_cast<IMIDIReceiver*>(ret.get()),
                                &static_cast<MIDIInOut&>(*ret).m_midiPortIn))
            ret.reset();
        else
            MIDIPortConnectSource(static_cast<MIDIInOut&>(*ret).m_midiPortIn, src, nullptr);
        CFRelease(midiName);

        if (!ret)
            return {};

        snprintf(mname, 256, "Boo MIDI Real Out %u", m_midiOutCounter++);
        midiName = CFStringCreateWithCStringNoCopy(nullptr, mname, kCFStringEncodingUTF8, kCFAllocatorNull);
        if (MIDIOutputPortCreate(m_midiClient, midiName, &static_cast<MIDIInOut&>(*ret).m_midiPortOut))
            ret.reset();
        else
            static_cast<MIDIInOut&>(*ret).m_midiOut = dst;
        CFRelease(midiName);

        return ret;
    }

    bool useMIDILock() const {return true;}

    AQSAudioVoiceEngine()
    {
        m_mixInfo.m_channels = _getAvailableSet();
        unsigned chCount = ChannelCount(m_mixInfo.m_channels);

        AudioStreamBasicDescription desc = {};
        desc.mSampleRate = 96000;
        desc.mFormatID = kAudioFormatLinearPCM;
        desc.mFormatFlags = kLinearPCMFormatFlagIsFloat;
        desc.mBytesPerPacket = chCount * 4;
        desc.mFramesPerPacket = 1;
        desc.mBytesPerFrame = chCount * 4;
        desc.mChannelsPerFrame = chCount;
        desc.mBitsPerChannel = 32;

        OSStatus err;
        if ((err = AudioQueueNewOutput(&desc, AudioQueueOutputCallback(Callback),
                                       this, nullptr, nullptr, 0, &m_queue)))
        {
            Log.report(logvisor::Fatal, "unable to create output audio queue");
            return;
        }

        Float64 actualSampleRate;
        UInt32 argSize = 8;
        err = AudioQueueGetProperty(m_queue, kAudioQueueDeviceProperty_SampleRate, &actualSampleRate, &argSize);
        AudioQueueDispose(m_queue, true);
        if (err)
        {
            Log.report(logvisor::Fatal, "unable to get native sample rate from audio queue");
            return;
        }

        desc.mSampleRate = actualSampleRate;
        if ((err = AudioQueueNewOutput(&desc, AudioQueueOutputCallback(Callback),
                                       this, nullptr, nullptr, 0, &m_queue)))
        {
            Log.report(logvisor::Fatal, "unable to create output audio queue");
            return;
        }

        m_mixInfo.m_sampleRate = actualSampleRate;
        m_mixInfo.m_sampleFormat = SOXR_FLOAT32_I;
        m_mixInfo.m_bitsPerSample = 32;
        m_5msFrames = actualSampleRate * 5 / 1000;

        ChannelMap& chMapOut = m_mixInfo.m_channelMap;
        if (chCount > 2)
        {
            AudioChannelLayout layout;
            UInt32 layoutSz = sizeof(layout);
            if (AudioQueueGetProperty(m_queue, kAudioQueueProperty_ChannelLayout, &layout, &layoutSz))
            {
                Log.report(logvisor::Fatal, "unable to get channel layout from audio queue");
                return;
            }

            switch (layout.mChannelLayoutTag)
            {
            case kAudioChannelLayoutTag_UseChannelDescriptions:
                chMapOut.m_channelCount = layout.mNumberChannelDescriptions;
                for (int i=0 ; i<layout.mNumberChannelDescriptions ; ++i)
                {
                    AudioChannel ch = AQSChannelToBooChannel(layout.mChannelDescriptions[i].mChannelLabel);
                    chMapOut.m_channels[i] = ch;
                }
                break;
            case kAudioChannelLayoutTag_UseChannelBitmap:
                if ((layout.mChannelBitmap & kAudioChannelBit_Left) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::FrontLeft;
                if ((layout.mChannelBitmap & kAudioChannelBit_Right) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::FrontRight;
                if ((layout.mChannelBitmap & kAudioChannelBit_Center) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::FrontCenter;
                if ((layout.mChannelBitmap & kAudioChannelBit_LFEScreen) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::LFE;
                if ((layout.mChannelBitmap & kAudioChannelBit_LeftSurround) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::RearLeft;
                if ((layout.mChannelBitmap & kAudioChannelBit_RightSurround) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::RearRight;
                if ((layout.mChannelBitmap & kAudioChannelBit_LeftSurroundDirect) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::SideLeft;
                if ((layout.mChannelBitmap & kAudioChannelBit_RightSurroundDirect) != 0)
                    chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::SideRight;
                break;
            case kAudioChannelLayoutTag_Stereo:
            case kAudioChannelLayoutTag_StereoHeadphones:
                chMapOut.m_channelCount = 2;
                chMapOut.m_channels[0] = AudioChannel::FrontLeft;
                chMapOut.m_channels[1] = AudioChannel::FrontRight;
                break;
            case kAudioChannelLayoutTag_Quadraphonic:
                chMapOut.m_channelCount = 4;
                chMapOut.m_channels[0] = AudioChannel::FrontLeft;
                chMapOut.m_channels[1] = AudioChannel::FrontRight;
                chMapOut.m_channels[2] = AudioChannel::RearLeft;
                chMapOut.m_channels[3] = AudioChannel::RearRight;
                break;
            case kAudioChannelLayoutTag_Pentagonal:
                chMapOut.m_channelCount = 5;
                chMapOut.m_channels[0] = AudioChannel::FrontLeft;
                chMapOut.m_channels[1] = AudioChannel::FrontRight;
                chMapOut.m_channels[2] = AudioChannel::RearLeft;
                chMapOut.m_channels[3] = AudioChannel::RearRight;
                chMapOut.m_channels[4] = AudioChannel::FrontCenter;
                break;
            default:
                Log.report(logvisor::Fatal,
                           "unknown channel layout %u; using stereo",
                           layout.mChannelLayoutTag);
                chMapOut.m_channelCount = 2;
                chMapOut.m_channels[0] = AudioChannel::FrontLeft;
                chMapOut.m_channels[1] = AudioChannel::FrontRight;
                break;
            }
        }
        else
        {
            chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::FrontLeft;
            chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::FrontRight;
        }

        while (chMapOut.m_channelCount < chCount)
            chMapOut.m_channels[chMapOut.m_channelCount++] = AudioChannel::Unknown;

        m_mixInfo.m_periodFrames = m_5msFrames * 3;
        for (int i=0 ; i<3 ; ++i)
            if (AudioQueueAllocateBuffer(m_queue, m_mixInfo.m_periodFrames * chCount * 4, &m_buffers[i]))
            {
                Log.report(logvisor::Fatal, "unable to create audio queue buffer");
                AudioQueueDispose(m_queue, false);
                m_queue = nullptr;
                return;
            }

        m_frameBytes = m_mixInfo.m_periodFrames * m_mixInfo.m_channelMap.m_channelCount * 4;

        for (unsigned i=0 ; i<3 ; ++i)
        {
            memset(m_buffers[i]->mAudioData, 0, m_frameBytes);
            m_buffers[i]->mAudioDataByteSize = m_frameBytes;
            AudioQueueEnqueueBuffer(m_queue, m_buffers[i], 0, nullptr);
        }
        AudioQueuePrime(m_queue, 0, nullptr);
        AudioQueueStart(m_queue, nullptr);

        /* Also create shared MIDI client */
        MIDIClientCreate(CFSTR("Boo MIDI"), nullptr, nullptr, &m_midiClient);
    }

    ~AQSAudioVoiceEngine()
    {
        m_cbRunning = false;
        if (m_inCb)
            m_engineEnterCv.notify_one();
        AudioQueueDispose(m_queue, true);
        if (m_midiClient)
            MIDIClientDispose(m_midiClient);
    }

    /* This is temperamental for AudioQueueServices
     * (which has unpredictable buffering windows).
     * _pumpAndMixVoicesRetrace() is highly recommended. */
    void pumpAndMixVoices()
    {
        std::unique_lock<std::mutex> lk(m_engineMutex);
        if (m_inCb)
        {
            /* Wake up callback */
            m_engineEnterCv.notify_one();
            /* Wait for callback completion */
            m_engineLeaveCv.wait(lk);
        }
    }

    void _pumpAndMixVoicesRetrace()
    {
        std::unique_lock<std::mutex> lk(m_engineMutex);
        m_inRetrace = true;
        while (m_inRetrace)
        {
            if (m_inCb) /* Wake up callback */
                m_engineEnterCv.notify_one();
            /* Wait for callback completion */
            m_engineLeaveCv.wait(lk);
        }
    }

    void _retraceBreak()
    {
        std::unique_lock<std::mutex> lk(m_engineMutex);
        m_inRetrace = false;
        if (m_inCb) /* Break out of callback */
            m_engineEnterCv.notify_one();
        else /* Break out of client */
            m_engineLeaveCv.notify_one();
    }
};

std::unique_ptr<IAudioVoiceEngine> NewAudioVoiceEngine()
{
    std::unique_ptr<IAudioVoiceEngine> ret = std::make_unique<AQSAudioVoiceEngine>();
    if (!static_cast<AQSAudioVoiceEngine&>(*ret).m_queue)
        return {};
    return ret;
}

}