#include "config.h" #include "oboe.h" #include #include #include #include "alnumeric.h" #include "core/device.h" #include "core/logging.h" #include "oboe/Oboe.h" namespace { constexpr char device_name[] = "Oboe Default"; struct OboePlayback final : public BackendBase, public oboe::AudioStreamCallback { OboePlayback(DeviceBase *device) : BackendBase{device} { } oboe::ManagedStream mStream; oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) override; void open(const char *name) override; bool reset() override; void start() override; void stop() override; }; oboe::DataCallbackResult OboePlayback::onAudioReady(oboe::AudioStream *oboeStream, void *audioData, int32_t numFrames) { assert(numFrames > 0); const int32_t numChannels{oboeStream->getChannelCount()}; mDevice->renderSamples(audioData, static_cast(numFrames), static_cast(numChannels)); return oboe::DataCallbackResult::Continue; } void OboePlayback::open(const char *name) { if(!name) name = device_name; else if(std::strcmp(name, device_name) != 0) throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found", name}; /* Open a basic output stream, just to ensure it can work. */ oboe::ManagedStream stream; oboe::Result result{oboe::AudioStreamBuilder{}.setDirection(oboe::Direction::Output) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) ->openManagedStream(stream)}; if(result != oboe::Result::OK) throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s", oboe::convertToText(result)}; mDevice->DeviceName = name; } bool OboePlayback::reset() { oboe::AudioStreamBuilder builder; builder.setDirection(oboe::Direction::Output); builder.setPerformanceMode(oboe::PerformanceMode::LowLatency); /* Don't let Oboe convert. We should be able to handle anything it gives * back. */ builder.setSampleRateConversionQuality(oboe::SampleRateConversionQuality::None); builder.setChannelConversionAllowed(false); builder.setFormatConversionAllowed(false); builder.setCallback(this); if(mDevice->Flags.test(FrequencyRequest)) builder.setSampleRate(static_cast(mDevice->Frequency)); if(mDevice->Flags.test(ChannelsRequest)) { /* Only use mono or stereo at user request. There's no telling what * other counts may be inferred as. */ builder.setChannelCount((mDevice->FmtChans==DevFmtMono) ? oboe::ChannelCount::Mono : (mDevice->FmtChans==DevFmtStereo) ? oboe::ChannelCount::Stereo : oboe::ChannelCount::Unspecified); } if(mDevice->Flags.test(SampleTypeRequest)) { oboe::AudioFormat format{oboe::AudioFormat::Unspecified}; switch(mDevice->FmtType) { case DevFmtByte: case DevFmtUByte: case DevFmtShort: case DevFmtUShort: format = oboe::AudioFormat::I16; break; case DevFmtInt: case DevFmtUInt: case DevFmtFloat: format = oboe::AudioFormat::Float; break; } builder.setFormat(format); } oboe::Result result{builder.openManagedStream(mStream)}; /* If the format failed, try asking for the defaults. */ while(result == oboe::Result::ErrorInvalidFormat) { if(builder.getFormat() != oboe::AudioFormat::Unspecified) builder.setFormat(oboe::AudioFormat::Unspecified); else if(builder.getSampleRate() != oboe::kUnspecified) builder.setSampleRate(oboe::kUnspecified); else if(builder.getChannelCount() != oboe::ChannelCount::Unspecified) builder.setChannelCount(oboe::ChannelCount::Unspecified); else break; result = builder.openManagedStream(mStream); } if(result != oboe::Result::OK) throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s", oboe::convertToText(result)}; mStream->setBufferSizeInFrames(mini(static_cast(mDevice->BufferSize), mStream->getBufferCapacityInFrames())); TRACE("Got stream with properties:\n%s", oboe::convertToText(mStream.get())); if(static_cast(mStream->getChannelCount()) != mDevice->channelsFromFmt()) { if(mStream->getChannelCount() >= 2) mDevice->FmtChans = DevFmtStereo; else if(mStream->getChannelCount() == 1) mDevice->FmtChans = DevFmtMono; else throw al::backend_exception{al::backend_error::DeviceError, "Got unhandled channel count: %d", mStream->getChannelCount()}; } setDefaultWFXChannelOrder(); switch(mStream->getFormat()) { case oboe::AudioFormat::I16: mDevice->FmtType = DevFmtShort; break; case oboe::AudioFormat::Float: mDevice->FmtType = DevFmtFloat; break; case oboe::AudioFormat::Unspecified: case oboe::AudioFormat::Invalid: throw al::backend_exception{al::backend_error::DeviceError, "Got unhandled sample type: %s", oboe::convertToText(mStream->getFormat())}; } mDevice->Frequency = static_cast(mStream->getSampleRate()); /* Ensure the period size is no less than 10ms. It's possible for FramesPerCallback to be 0 * indicating variable updates, but OpenAL should have a reasonable minimum update size set. * FramesPerBurst may not necessarily be correct, but hopefully it can act as a minimum * update size. */ mDevice->UpdateSize = maxu(mDevice->Frequency / 100, static_cast(mStream->getFramesPerBurst())); mDevice->BufferSize = maxu(mDevice->UpdateSize * 2, static_cast(mStream->getBufferSizeInFrames())); return true; } void OboePlayback::start() { const oboe::Result result{mStream->start()}; if(result != oboe::Result::OK) throw al::backend_exception{al::backend_error::DeviceError, "Failed to start stream: %s", oboe::convertToText(result)}; } void OboePlayback::stop() { oboe::Result result{mStream->stop()}; if(result != oboe::Result::OK) throw al::backend_exception{al::backend_error::DeviceError, "Failed to stop stream: %s", oboe::convertToText(result)}; } struct OboeCapture final : public BackendBase { OboeCapture(DeviceBase *device) : BackendBase{device} { } oboe::ManagedStream mStream; std::vector mSamples; uint mLastAvail{0u}; void open(const char *name) override; void start() override; void stop() override; void captureSamples(al::byte *buffer, uint samples) override; uint availableSamples() override; }; void OboeCapture::open(const char *name) { if(!name) name = device_name; else if(std::strcmp(name, device_name) != 0) throw al::backend_exception{al::backend_error::NoDevice, "Device name \"%s\" not found", name}; oboe::AudioStreamBuilder builder; builder.setDirection(oboe::Direction::Input) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) ->setSampleRateConversionQuality(oboe::SampleRateConversionQuality::High) ->setChannelConversionAllowed(true) ->setFormatConversionAllowed(true) ->setBufferCapacityInFrames(static_cast(mDevice->BufferSize)) ->setSampleRate(static_cast(mDevice->Frequency)); /* Only use mono or stereo at user request. There's no telling what * other counts may be inferred as. */ switch(mDevice->FmtChans) { case DevFmtMono: builder.setChannelCount(oboe::ChannelCount::Mono); break; case DevFmtStereo: builder.setChannelCount(oboe::ChannelCount::Stereo); break; case DevFmtQuad: case DevFmtX51: case DevFmtX61: case DevFmtX71: case DevFmtX3D71: case DevFmtAmbi3D: throw al::backend_exception{al::backend_error::DeviceError, "%s capture not supported", DevFmtChannelsString(mDevice->FmtChans)}; } /* FIXME: This really should support UByte, but Oboe doesn't. We'll need to * use a temp buffer and convert. */ switch(mDevice->FmtType) { case DevFmtShort: builder.setFormat(oboe::AudioFormat::I16); break; case DevFmtFloat: builder.setFormat(oboe::AudioFormat::Float); break; case DevFmtByte: case DevFmtUByte: case DevFmtUShort: case DevFmtInt: case DevFmtUInt: throw al::backend_exception{al::backend_error::DeviceError, "%s capture samples not supported", DevFmtTypeString(mDevice->FmtType)}; } oboe::Result result{builder.openManagedStream(mStream)}; if(result != oboe::Result::OK) throw al::backend_exception{al::backend_error::DeviceError, "Failed to create stream: %s", oboe::convertToText(result)}; if(static_cast(mDevice->BufferSize) > mStream->getBufferCapacityInFrames()) throw al::backend_exception{al::backend_error::DeviceError, "Buffer size too large (%u > %d)", mDevice->BufferSize, mStream->getBufferCapacityInFrames()}; auto buffer_result = mStream->setBufferSizeInFrames(static_cast(mDevice->BufferSize)); if(!buffer_result) throw al::backend_exception{al::backend_error::DeviceError, "Failed to set buffer size: %s", oboe::convertToText(buffer_result.error())}; else if(buffer_result.value() < static_cast(mDevice->BufferSize)) throw al::backend_exception{al::backend_error::DeviceError, "Failed to set large enough buffer size (%u > %d)", mDevice->BufferSize, buffer_result.value()}; mDevice->BufferSize = static_cast(buffer_result.value()); TRACE("Got stream with properties:\n%s", oboe::convertToText(mStream.get())); mDevice->DeviceName = name; } void OboeCapture::start() { const oboe::Result result{mStream->start()}; if(result != oboe::Result::OK) throw al::backend_exception{al::backend_error::DeviceError, "Failed to start stream: %s", oboe::convertToText(result)}; } void OboeCapture::stop() { /* Capture any unread samples before stopping. Oboe drops whatever's left * in the stream. */ if(auto availres = mStream->getAvailableFrames()) { const auto avail = std::max(static_cast(availres.value()), mLastAvail); const size_t frame_size{static_cast(mStream->getBytesPerFrame())}; const size_t pos{mSamples.size()}; mSamples.resize(pos + avail*frame_size); auto result = mStream->read(&mSamples[pos], availres.value(), 0); uint got{bool{result} ? static_cast(result.value()) : 0u}; if(got < avail) std::fill_n(&mSamples[pos + got*frame_size], (avail-got)*frame_size, al::byte{}); mLastAvail = 0; } const oboe::Result result{mStream->stop()}; if(result != oboe::Result::OK) throw al::backend_exception{al::backend_error::DeviceError, "Failed to stop stream: %s", oboe::convertToText(result)}; } uint OboeCapture::availableSamples() { /* Keep track of the max available frame count, to ensure it doesn't go * backwards. */ if(auto result = mStream->getAvailableFrames()) mLastAvail = std::max(static_cast(result.value()), mLastAvail); const auto frame_size = static_cast(mStream->getBytesPerFrame()); return static_cast(mSamples.size()/frame_size) + mLastAvail; } void OboeCapture::captureSamples(al::byte *buffer, uint samples) { const auto frame_size = static_cast(mStream->getBytesPerFrame()); if(const size_t storelen{mSamples.size()}) { const auto instore = static_cast(storelen / frame_size); const uint tocopy{std::min(samples, instore) * frame_size}; std::copy_n(mSamples.begin(), tocopy, buffer); mSamples.erase(mSamples.begin(), mSamples.begin() + tocopy); buffer += tocopy; samples -= tocopy/frame_size; if(!samples) return; } auto result = mStream->read(buffer, static_cast(samples), 0); uint got{bool{result} ? static_cast(result.value()) : 0u}; if(got < samples) std::fill_n(buffer + got*frame_size, (samples-got)*frame_size, al::byte{}); mLastAvail = std::max(mLastAvail, samples) - samples; } } // namespace bool OboeBackendFactory::init() { return true; } bool OboeBackendFactory::querySupport(BackendType type) { return type == BackendType::Playback || type == BackendType::Capture; } std::string OboeBackendFactory::probe(BackendType type) { switch(type) { case BackendType::Playback: case BackendType::Capture: /* Includes null char. */ return std::string{device_name, sizeof(device_name)}; } return std::string{}; } BackendPtr OboeBackendFactory::createBackend(DeviceBase *device, BackendType type) { if(type == BackendType::Playback) return BackendPtr{new OboePlayback{device}}; if(type == BackendType::Capture) return BackendPtr{new OboeCapture{device}}; return BackendPtr{}; } BackendFactory &OboeBackendFactory::getFactory() { static OboeBackendFactory factory{}; return factory; }