aboutsummaryrefslogtreecommitdiffstats
path: root/alc/backends/oss.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'alc/backends/oss.cpp')
-rw-r--r--alc/backends/oss.cpp751
1 files changed, 751 insertions, 0 deletions
diff --git a/alc/backends/oss.cpp b/alc/backends/oss.cpp
new file mode 100644
index 00000000..8cfe9e96
--- /dev/null
+++ b/alc/backends/oss.cpp
@@ -0,0 +1,751 @@
+/**
+ * OpenAL cross platform audio library
+ * Copyright (C) 1999-2007 by authors.
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ * Or go to http://www.gnu.org/copyleft/lgpl.html
+ */
+
+#include "config.h"
+
+#include "backends/oss.h"
+
+#include <fcntl.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <atomic>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <exception>
+#include <functional>
+#include <memory>
+#include <new>
+#include <string>
+#include <thread>
+#include <utility>
+
+#include "AL/al.h"
+
+#include "alcmain.h"
+#include "alconfig.h"
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "aloptional.h"
+#include "alu.h"
+#include "logging.h"
+#include "ringbuffer.h"
+#include "threads.h"
+#include "vector.h"
+
+#include <sys/soundcard.h>
+
+/*
+ * The OSS documentation talks about SOUND_MIXER_READ, but the header
+ * only contains MIXER_READ. Play safe. Same for WRITE.
+ */
+#ifndef SOUND_MIXER_READ
+#define SOUND_MIXER_READ MIXER_READ
+#endif
+#ifndef SOUND_MIXER_WRITE
+#define SOUND_MIXER_WRITE MIXER_WRITE
+#endif
+
+#if defined(SOUND_VERSION) && (SOUND_VERSION < 0x040000)
+#define ALC_OSS_COMPAT
+#endif
+#ifndef SNDCTL_AUDIOINFO
+#define ALC_OSS_COMPAT
+#endif
+
+/*
+ * FreeBSD strongly discourages the use of specific devices,
+ * such as those returned in oss_audioinfo.devnode
+ */
+#ifdef __FreeBSD__
+#define ALC_OSS_DEVNODE_TRUC
+#endif
+
+namespace {
+
+constexpr char DefaultName[] = "OSS Default";
+std::string DefaultPlayback{"/dev/dsp"};
+std::string DefaultCapture{"/dev/dsp"};
+
+struct DevMap {
+ std::string name;
+ std::string device_name;
+};
+
+bool checkName(const al::vector<DevMap> &list, const std::string &name)
+{
+ return std::find_if(list.cbegin(), list.cend(),
+ [&name](const DevMap &entry) -> bool
+ { return entry.name == name; }
+ ) != list.cend();
+}
+
+al::vector<DevMap> PlaybackDevices;
+al::vector<DevMap> CaptureDevices;
+
+
+#ifdef ALC_OSS_COMPAT
+
+#define DSP_CAP_OUTPUT 0x00020000
+#define DSP_CAP_INPUT 0x00010000
+void ALCossListPopulate(al::vector<DevMap> *devlist, int type)
+{
+ devlist->emplace_back(DevMap{DefaultName, (type==DSP_CAP_INPUT) ? DefaultCapture : DefaultPlayback});
+}
+
+#else
+
+void ALCossListAppend(al::vector<DevMap> *list, const char *handle, size_t hlen, const char *path, size_t plen)
+{
+#ifdef ALC_OSS_DEVNODE_TRUC
+ for(size_t i{0};i < plen;i++)
+ {
+ if(path[i] == '.')
+ {
+ if(strncmp(path + i, handle + hlen + i - plen, plen - i) == 0)
+ hlen = hlen + i - plen;
+ plen = i;
+ }
+ }
+#endif
+ if(handle[0] == '\0')
+ {
+ handle = path;
+ hlen = plen;
+ }
+
+ std::string basename{handle, hlen};
+ basename.erase(std::find(basename.begin(), basename.end(), '\0'), basename.end());
+ std::string devname{path, plen};
+ devname.erase(std::find(devname.begin(), devname.end(), '\0'), devname.end());
+
+ auto iter = std::find_if(list->cbegin(), list->cend(),
+ [&devname](const DevMap &entry) -> bool
+ { return entry.device_name == devname; }
+ );
+ if(iter != list->cend())
+ return;
+
+ int count{1};
+ std::string newname{basename};
+ while(checkName(PlaybackDevices, newname))
+ {
+ newname = basename;
+ newname += " #";
+ newname += std::to_string(++count);
+ }
+
+ list->emplace_back(DevMap{std::move(newname), std::move(devname)});
+ const DevMap &entry = list->back();
+
+ TRACE("Got device \"%s\", \"%s\"\n", entry.name.c_str(), entry.device_name.c_str());
+}
+
+void ALCossListPopulate(al::vector<DevMap> *devlist, int type_flag)
+{
+ int fd{open("/dev/mixer", O_RDONLY)};
+ if(fd < 0)
+ {
+ TRACE("Could not open /dev/mixer: %s\n", strerror(errno));
+ goto done;
+ }
+
+ oss_sysinfo si;
+ if(ioctl(fd, SNDCTL_SYSINFO, &si) == -1)
+ {
+ TRACE("SNDCTL_SYSINFO failed: %s\n", strerror(errno));
+ goto done;
+ }
+
+ for(int i{0};i < si.numaudios;i++)
+ {
+ oss_audioinfo ai;
+ ai.dev = i;
+ if(ioctl(fd, SNDCTL_AUDIOINFO, &ai) == -1)
+ {
+ ERR("SNDCTL_AUDIOINFO (%d) failed: %s\n", i, strerror(errno));
+ continue;
+ }
+ if(!(ai.caps&type_flag) || ai.devnode[0] == '\0')
+ continue;
+
+ const char *handle;
+ size_t len;
+ if(ai.handle[0] != '\0')
+ {
+ len = strnlen(ai.handle, sizeof(ai.handle));
+ handle = ai.handle;
+ }
+ else
+ {
+ len = strnlen(ai.name, sizeof(ai.name));
+ handle = ai.name;
+ }
+
+ ALCossListAppend(devlist, handle, len, ai.devnode,
+ strnlen(ai.devnode, sizeof(ai.devnode)));
+ }
+
+done:
+ if(fd >= 0)
+ close(fd);
+ fd = -1;
+
+ const char *defdev{((type_flag==DSP_CAP_INPUT) ? DefaultCapture : DefaultPlayback).c_str()};
+ auto iter = std::find_if(devlist->cbegin(), devlist->cend(),
+ [defdev](const DevMap &entry) -> bool
+ { return entry.device_name == defdev; }
+ );
+ if(iter == devlist->cend())
+ devlist->insert(devlist->begin(), DevMap{DefaultName, defdev});
+ else
+ {
+ DevMap entry{std::move(*iter)};
+ devlist->erase(iter);
+ devlist->insert(devlist->begin(), std::move(entry));
+ }
+ devlist->shrink_to_fit();
+}
+
+#endif
+
+int log2i(ALCuint x)
+{
+ int y = 0;
+ while (x > 1)
+ {
+ x >>= 1;
+ y++;
+ }
+ return y;
+}
+
+
+struct OSSPlayback final : public BackendBase {
+ OSSPlayback(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~OSSPlayback() override;
+
+ int mixerProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean reset() override;
+ ALCboolean start() override;
+ void stop() override;
+
+ int mFd{-1};
+
+ al::vector<ALubyte> mMixData;
+
+ std::atomic<bool> mKillNow{true};
+ std::thread mThread;
+
+ DEF_NEWDEL(OSSPlayback)
+};
+
+OSSPlayback::~OSSPlayback()
+{
+ if(mFd != -1)
+ close(mFd);
+ mFd = -1;
+}
+
+
+int OSSPlayback::mixerProc()
+{
+ SetRTPriority();
+ althrd_setname(MIXER_THREAD_NAME);
+
+ const int frame_size{mDevice->frameSizeFromFmt()};
+
+ lock();
+ while(!mKillNow.load(std::memory_order_acquire) &&
+ mDevice->Connected.load(std::memory_order_acquire))
+ {
+ pollfd pollitem{};
+ pollitem.fd = mFd;
+ pollitem.events = POLLOUT;
+
+ unlock();
+ int pret{poll(&pollitem, 1, 1000)};
+ lock();
+ if(pret < 0)
+ {
+ if(errno == EINTR || errno == EAGAIN)
+ continue;
+ ERR("poll failed: %s\n", strerror(errno));
+ aluHandleDisconnect(mDevice, "Failed waiting for playback buffer: %s", strerror(errno));
+ break;
+ }
+ else if(pret == 0)
+ {
+ WARN("poll timeout\n");
+ continue;
+ }
+
+ ALubyte *write_ptr{mMixData.data()};
+ size_t to_write{mMixData.size()};
+ aluMixData(mDevice, write_ptr, to_write/frame_size);
+ while(to_write > 0 && !mKillNow.load(std::memory_order_acquire))
+ {
+ ssize_t wrote{write(mFd, write_ptr, to_write)};
+ if(wrote < 0)
+ {
+ if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
+ continue;
+ ERR("write failed: %s\n", strerror(errno));
+ aluHandleDisconnect(mDevice, "Failed writing playback samples: %s",
+ strerror(errno));
+ break;
+ }
+
+ to_write -= wrote;
+ write_ptr += wrote;
+ }
+ }
+ unlock();
+
+ return 0;
+}
+
+
+ALCenum OSSPlayback::open(const ALCchar *name)
+{
+ const char *devname{DefaultPlayback.c_str()};
+ if(!name)
+ name = DefaultName;
+ else
+ {
+ if(PlaybackDevices.empty())
+ ALCossListPopulate(&PlaybackDevices, DSP_CAP_OUTPUT);
+
+ auto iter = std::find_if(PlaybackDevices.cbegin(), PlaybackDevices.cend(),
+ [&name](const DevMap &entry) -> bool
+ { return entry.name == name; }
+ );
+ if(iter == PlaybackDevices.cend())
+ return ALC_INVALID_VALUE;
+ devname = iter->device_name.c_str();
+ }
+
+ mFd = ::open(devname, O_WRONLY);
+ if(mFd == -1)
+ {
+ ERR("Could not open %s: %s\n", devname, strerror(errno));
+ return ALC_INVALID_VALUE;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean OSSPlayback::reset()
+{
+ int numFragmentsLogSize;
+ int log2FragmentSize;
+ unsigned int periods;
+ audio_buf_info info;
+ ALuint frameSize;
+ int numChannels;
+ int ossFormat;
+ int ossSpeed;
+ const char *err;
+
+ switch(mDevice->FmtType)
+ {
+ case DevFmtByte:
+ ossFormat = AFMT_S8;
+ break;
+ case DevFmtUByte:
+ ossFormat = AFMT_U8;
+ break;
+ case DevFmtUShort:
+ case DevFmtInt:
+ case DevFmtUInt:
+ case DevFmtFloat:
+ mDevice->FmtType = DevFmtShort;
+ /* fall-through */
+ case DevFmtShort:
+ ossFormat = AFMT_S16_NE;
+ break;
+ }
+
+ periods = mDevice->BufferSize / mDevice->UpdateSize;
+ numChannels = mDevice->channelsFromFmt();
+ ossSpeed = mDevice->Frequency;
+ frameSize = numChannels * mDevice->bytesFromFmt();
+ /* According to the OSS spec, 16 bytes (log2(16)) is the minimum. */
+ log2FragmentSize = maxi(log2i(mDevice->UpdateSize*frameSize), 4);
+ numFragmentsLogSize = (periods << 16) | log2FragmentSize;
+
+#define CHECKERR(func) if((func) < 0) { \
+ err = #func; \
+ goto err; \
+}
+ /* Don't fail if SETFRAGMENT fails. We can handle just about anything
+ * that's reported back via GETOSPACE */
+ ioctl(mFd, SNDCTL_DSP_SETFRAGMENT, &numFragmentsLogSize);
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_SETFMT, &ossFormat));
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_CHANNELS, &numChannels));
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_SPEED, &ossSpeed));
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_GETOSPACE, &info));
+ if(0)
+ {
+ err:
+ ERR("%s failed: %s\n", err, strerror(errno));
+ return ALC_FALSE;
+ }
+#undef CHECKERR
+
+ if(mDevice->channelsFromFmt() != numChannels)
+ {
+ ERR("Failed to set %s, got %d channels instead\n", DevFmtChannelsString(mDevice->FmtChans),
+ numChannels);
+ return ALC_FALSE;
+ }
+
+ if(!((ossFormat == AFMT_S8 && mDevice->FmtType == DevFmtByte) ||
+ (ossFormat == AFMT_U8 && mDevice->FmtType == DevFmtUByte) ||
+ (ossFormat == AFMT_S16_NE && mDevice->FmtType == DevFmtShort)))
+ {
+ ERR("Failed to set %s samples, got OSS format %#x\n", DevFmtTypeString(mDevice->FmtType),
+ ossFormat);
+ return ALC_FALSE;
+ }
+
+ mDevice->Frequency = ossSpeed;
+ mDevice->UpdateSize = info.fragsize / frameSize;
+ mDevice->BufferSize = info.fragments * mDevice->UpdateSize;
+
+ SetDefaultChannelOrder(mDevice);
+
+ mMixData.resize(mDevice->UpdateSize * mDevice->frameSizeFromFmt());
+
+ return ALC_TRUE;
+}
+
+ALCboolean OSSPlayback::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&OSSPlayback::mixerProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create playback thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+void OSSPlayback::stop()
+{
+ if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+ return;
+ mThread.join();
+
+ if(ioctl(mFd, SNDCTL_DSP_RESET) != 0)
+ ERR("Error resetting device: %s\n", strerror(errno));
+}
+
+
+struct OSScapture final : public BackendBase {
+ OSScapture(ALCdevice *device) noexcept : BackendBase{device} { }
+ ~OSScapture() override;
+
+ int recordProc();
+
+ ALCenum open(const ALCchar *name) override;
+ ALCboolean start() override;
+ void stop() override;
+ ALCenum captureSamples(ALCvoid *buffer, ALCuint samples) override;
+ ALCuint availableSamples() override;
+
+ int mFd{-1};
+
+ RingBufferPtr mRing{nullptr};
+
+ std::atomic<bool> mKillNow{true};
+ std::thread mThread;
+
+ DEF_NEWDEL(OSScapture)
+};
+
+OSScapture::~OSScapture()
+{
+ if(mFd != -1)
+ close(mFd);
+ mFd = -1;
+}
+
+
+int OSScapture::recordProc()
+{
+ SetRTPriority();
+ althrd_setname(RECORD_THREAD_NAME);
+
+ const int frame_size{mDevice->frameSizeFromFmt()};
+ while(!mKillNow.load(std::memory_order_acquire))
+ {
+ pollfd pollitem{};
+ pollitem.fd = mFd;
+ pollitem.events = POLLIN;
+
+ int sret{poll(&pollitem, 1, 1000)};
+ if(sret < 0)
+ {
+ if(errno == EINTR || errno == EAGAIN)
+ continue;
+ ERR("poll failed: %s\n", strerror(errno));
+ aluHandleDisconnect(mDevice, "Failed to check capture samples: %s", strerror(errno));
+ break;
+ }
+ else if(sret == 0)
+ {
+ WARN("poll timeout\n");
+ continue;
+ }
+
+ auto vec = mRing->getWriteVector();
+ if(vec.first.len > 0)
+ {
+ ssize_t amt{read(mFd, vec.first.buf, vec.first.len*frame_size)};
+ if(amt < 0)
+ {
+ ERR("read failed: %s\n", strerror(errno));
+ aluHandleDisconnect(mDevice, "Failed reading capture samples: %s",
+ strerror(errno));
+ break;
+ }
+ mRing->writeAdvance(amt/frame_size);
+ }
+ }
+
+ return 0;
+}
+
+
+ALCenum OSScapture::open(const ALCchar *name)
+{
+ const char *devname{DefaultCapture.c_str()};
+ if(!name)
+ name = DefaultName;
+ else
+ {
+ if(CaptureDevices.empty())
+ ALCossListPopulate(&CaptureDevices, DSP_CAP_INPUT);
+
+ auto iter = std::find_if(CaptureDevices.cbegin(), CaptureDevices.cend(),
+ [&name](const DevMap &entry) -> bool
+ { return entry.name == name; }
+ );
+ if(iter == CaptureDevices.cend())
+ return ALC_INVALID_VALUE;
+ devname = iter->device_name.c_str();
+ }
+
+ mFd = ::open(devname, O_RDONLY);
+ if(mFd == -1)
+ {
+ ERR("Could not open %s: %s\n", devname, strerror(errno));
+ return ALC_INVALID_VALUE;
+ }
+
+ int ossFormat{};
+ switch(mDevice->FmtType)
+ {
+ case DevFmtByte:
+ ossFormat = AFMT_S8;
+ break;
+ case DevFmtUByte:
+ ossFormat = AFMT_U8;
+ break;
+ case DevFmtShort:
+ ossFormat = AFMT_S16_NE;
+ break;
+ case DevFmtUShort:
+ case DevFmtInt:
+ case DevFmtUInt:
+ case DevFmtFloat:
+ ERR("%s capture samples not supported\n", DevFmtTypeString(mDevice->FmtType));
+ return ALC_INVALID_VALUE;
+ }
+
+ int periods{4};
+ int numChannels{mDevice->channelsFromFmt()};
+ int frameSize{numChannels * mDevice->bytesFromFmt()};
+ int ossSpeed{static_cast<int>(mDevice->Frequency)};
+ int log2FragmentSize{log2i(mDevice->BufferSize * frameSize / periods)};
+
+ /* according to the OSS spec, 16 bytes are the minimum */
+ log2FragmentSize = std::max(log2FragmentSize, 4);
+ int numFragmentsLogSize{(periods << 16) | log2FragmentSize};
+
+ audio_buf_info info;
+ const char *err;
+#define CHECKERR(func) if((func) < 0) { \
+ err = #func; \
+ goto err; \
+}
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_SETFRAGMENT, &numFragmentsLogSize));
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_SETFMT, &ossFormat));
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_CHANNELS, &numChannels));
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_SPEED, &ossSpeed));
+ CHECKERR(ioctl(mFd, SNDCTL_DSP_GETISPACE, &info));
+ if(0)
+ {
+ err:
+ ERR("%s failed: %s\n", err, strerror(errno));
+ close(mFd);
+ mFd = -1;
+ return ALC_INVALID_VALUE;
+ }
+#undef CHECKERR
+
+ if(mDevice->channelsFromFmt() != numChannels)
+ {
+ ERR("Failed to set %s, got %d channels instead\n", DevFmtChannelsString(mDevice->FmtChans),
+ numChannels);
+ close(mFd);
+ mFd = -1;
+ return ALC_INVALID_VALUE;
+ }
+
+ if(!((ossFormat == AFMT_S8 && mDevice->FmtType == DevFmtByte) ||
+ (ossFormat == AFMT_U8 && mDevice->FmtType == DevFmtUByte) ||
+ (ossFormat == AFMT_S16_NE && mDevice->FmtType == DevFmtShort)))
+ {
+ ERR("Failed to set %s samples, got OSS format %#x\n", DevFmtTypeString(mDevice->FmtType), ossFormat);
+ close(mFd);
+ mFd = -1;
+ return ALC_INVALID_VALUE;
+ }
+
+ mRing = CreateRingBuffer(mDevice->BufferSize, frameSize, false);
+ if(!mRing)
+ {
+ ERR("Ring buffer create failed\n");
+ close(mFd);
+ mFd = -1;
+ return ALC_OUT_OF_MEMORY;
+ }
+
+ mDevice->DeviceName = name;
+ return ALC_NO_ERROR;
+}
+
+ALCboolean OSScapture::start()
+{
+ try {
+ mKillNow.store(false, std::memory_order_release);
+ mThread = std::thread{std::mem_fn(&OSScapture::recordProc), this};
+ return ALC_TRUE;
+ }
+ catch(std::exception& e) {
+ ERR("Could not create record thread: %s\n", e.what());
+ }
+ catch(...) {
+ }
+ return ALC_FALSE;
+}
+
+void OSScapture::stop()
+{
+ if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
+ return;
+ mThread.join();
+
+ if(ioctl(mFd, SNDCTL_DSP_RESET) != 0)
+ ERR("Error resetting device: %s\n", strerror(errno));
+}
+
+ALCenum OSScapture::captureSamples(ALCvoid *buffer, ALCuint samples)
+{
+ mRing->read(buffer, samples);
+ return ALC_NO_ERROR;
+}
+
+ALCuint OSScapture::availableSamples()
+{ return mRing->readSpace(); }
+
+} // namespace
+
+
+BackendFactory &OSSBackendFactory::getFactory()
+{
+ static OSSBackendFactory factory{};
+ return factory;
+}
+
+bool OSSBackendFactory::init()
+{
+ if(auto devopt = ConfigValueStr(nullptr, "oss", "device"))
+ DefaultPlayback = std::move(*devopt);
+ if(auto capopt = ConfigValueStr(nullptr, "oss", "capture"))
+ DefaultCapture = std::move(*capopt);
+
+ return true;
+}
+
+bool OSSBackendFactory::querySupport(BackendType type)
+{ return (type == BackendType::Playback || type == BackendType::Capture); }
+
+void OSSBackendFactory::probe(DevProbe type, std::string *outnames)
+{
+ auto add_device = [outnames](const DevMap &entry) -> void
+ {
+#ifdef HAVE_STAT
+ struct stat buf;
+ if(stat(entry.device_name.c_str(), &buf) == 0)
+#endif
+ {
+ /* Includes null char. */
+ outnames->append(entry.name.c_str(), entry.name.length()+1);
+ }
+ };
+
+ switch(type)
+ {
+ case DevProbe::Playback:
+ PlaybackDevices.clear();
+ ALCossListPopulate(&PlaybackDevices, DSP_CAP_OUTPUT);
+ std::for_each(PlaybackDevices.cbegin(), PlaybackDevices.cend(), add_device);
+ break;
+
+ case DevProbe::Capture:
+ CaptureDevices.clear();
+ ALCossListPopulate(&CaptureDevices, DSP_CAP_INPUT);
+ std::for_each(CaptureDevices.cbegin(), CaptureDevices.cend(), add_device);
+ break;
+ }
+}
+
+BackendPtr OSSBackendFactory::createBackend(ALCdevice *device, BackendType type)
+{
+ if(type == BackendType::Playback)
+ return BackendPtr{new OSSPlayback{device}};
+ if(type == BackendType::Capture)
+ return BackendPtr{new OSScapture{device}};
+ return nullptr;
+}