diff --git a/Sources/CSFBAudioEngine/Input/BufferInput.cpp b/Sources/CSFBAudioEngine/Input/BufferInput.cpp new file mode 100644 index 000000000..0b632f82d --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/BufferInput.cpp @@ -0,0 +1,48 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import "BufferInput.hpp" + +#import +#import +#import + +sfb::BufferInput::BufferInput(const void *buf, int64_t len, BufferAdoption behavior) + : buf_{const_cast(buf)}, + free_{behavior == BufferAdoption::copy || behavior == BufferAdoption::noCopyAndFree}, len_{len} { + if (!buf || len < 0) { + os_log_error(log_, "Cannot create BufferInput with null buffer or negative length"); + throw std::invalid_argument("Null buffer or negative length"); + } + + if (behavior == BufferAdoption::copy) { + buf_ = std::malloc(len_); + if (!buf_) { + throw std::bad_alloc(); + } + std::memcpy(buf_, buf, len_); + } +} + +sfb::BufferInput::~BufferInput() noexcept { + if (free_) { + std::free(buf_); + } +} + +int64_t sfb::BufferInput::_read(void *buffer, int64_t count) { + const auto remaining = len_ - pos_; + count = std::min(count, remaining); + memcpy(buffer, reinterpret_cast(reinterpret_cast(buf_) + pos_), count); + pos_ += count; + return count; +} + +CFStringRef sfb::BufferInput::_copyDescription() const noexcept { + return CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, CFSTR(""), this, + len_, buf_); +} diff --git a/Sources/CSFBAudioEngine/Input/BufferInput.hpp b/Sources/CSFBAudioEngine/Input/BufferInput.hpp new file mode 100644 index 000000000..3c5e58cb2 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/BufferInput.hpp @@ -0,0 +1,54 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#pragma once + +#import "InputSource.hpp" + +namespace sfb { + +class BufferInput : public InputSource { + public: + /// Buffer adoption behaviors. + enum class BufferAdoption { copy, noCopy, noCopyAndFree }; + BufferInput(const void *_Nonnull buf, int64_t len, BufferAdoption behavior = BufferAdoption::copy); + ~BufferInput() noexcept; + + // This class is non-copyable. + BufferInput(const BufferInput &) = delete; + BufferInput(BufferInput &&) = delete; + + // This class is non-assignable. + BufferInput &operator=(const BufferInput &) = delete; + BufferInput &operator=(BufferInput &&) = delete; + + protected: + explicit BufferInput() noexcept = default; + + /// The data buffer. + void *_Nonnull buf_{nullptr}; + /// Whether the buffer should be freed in the destructor. + bool free_{false}; + /// The length of the buffer in bytes. + int64_t len_{0}; + /// The current byte position in the buffer. + int64_t pos_{0}; + + private: + void _open() override { pos_ = 0; } + void _close() override {} + bool _atEOF() const noexcept override { return len_ == pos_; } + int64_t _position() const noexcept override { return pos_; } + int64_t _length() const noexcept override { return len_; } + bool _supportsSeeking() const noexcept override { return true; } + void _seekToPosition(int64_t position) override { pos_ = position; } + + int64_t _read(void *_Nonnull buffer, int64_t count) override; + CFStringRef _Nonnull _copyDescription() const noexcept override; +}; + +} /* namespace sfb */ diff --git a/Sources/CSFBAudioEngine/Input/DataInput.cpp b/Sources/CSFBAudioEngine/Input/DataInput.cpp new file mode 100644 index 000000000..f1e192a6f --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/DataInput.cpp @@ -0,0 +1,38 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import "DataInput.hpp" + +#import +#import + +sfb::DataInput::DataInput(CFDataRef data) { + if (!data) { + os_log_error(log_, "Cannot create DataInput with null data"); + throw std::invalid_argument("Null data"); + } + data_ = static_cast(CFRetain(data)); +} + +sfb::DataInput::~DataInput() noexcept { CFRelease(data_); } + +int64_t sfb::DataInput::_read(void *buffer, int64_t count) { + if (count > std::numeric_limits::max()) { + os_log_error(log_, "_Read() called on with count greater than maximum allowable value", this); + throw std::invalid_argument("Count greater than maximum allowable value"); + } + const int64_t remaining = CFDataGetLength(data_) - pos_; + count = std::min(count, remaining); + const auto range = CFRangeMake(pos_, count); + CFDataGetBytes(data_, range, static_cast(buffer)); + pos_ += count; + return count; +} + +CFStringRef sfb::DataInput::_copyDescription() const noexcept { + return CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, CFSTR(""), this, data_); +} diff --git a/Sources/CSFBAudioEngine/Input/DataInput.hpp b/Sources/CSFBAudioEngine/Input/DataInput.hpp new file mode 100644 index 000000000..4afe96bb0 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/DataInput.hpp @@ -0,0 +1,43 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#pragma once + +#import "InputSource.hpp" + +namespace sfb { + +class DataInput : public InputSource { + public: + explicit DataInput(CFDataRef _Nonnull data); + ~DataInput() noexcept; + + // This class is non-copyable. + DataInput(const DataInput &) = delete; + DataInput(DataInput &&) = delete; + + // This class is non-assignable. + DataInput &operator=(const DataInput &) = delete; + DataInput &operator=(DataInput &&) = delete; + + private: + void _open() noexcept override { pos_ = 0; } + void _close() noexcept override {} + bool _atEOF() const noexcept override { return CFDataGetLength(data_) == pos_; } + int64_t _position() const noexcept override { return pos_; } + int64_t _length() const noexcept override { return CFDataGetLength(data_); } + bool _supportsSeeking() const noexcept override { return true; } + void _seekToPosition(int64_t position) override { pos_ = position; } + + int64_t _read(void *_Nonnull buffer, int64_t count) override; + CFStringRef _Nonnull _copyDescription() const noexcept override; + + CFDataRef _Nonnull data_{nullptr}; + CFIndex pos_{0}; +}; + +} /* namespace sfb */ diff --git a/Sources/CSFBAudioEngine/Input/FileContentsInput.cpp b/Sources/CSFBAudioEngine/Input/FileContentsInput.cpp new file mode 100644 index 000000000..b71782b27 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileContentsInput.cpp @@ -0,0 +1,74 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import "FileContentsInput.hpp" + +#import "scope_exit.hpp" + +#import +#import +#import + +#import + +sfb::FileContentsInput::FileContentsInput(CFURLRef url) { + if (!url) { + os_log_error(log_, "Cannot create FileContentsInput with null URL"); + throw std::invalid_argument("Null URL"); + } + url_ = static_cast(CFRetain(url)); + free_ = true; +} + +void sfb::FileContentsInput::_open() { + UInt8 path[PATH_MAX]; + auto success = CFURLGetFileSystemRepresentation(url_, FALSE, path, PATH_MAX); + if (!success) { + throw std::runtime_error("Unable to get URL file system representation"); + } + + auto file = std::fopen(reinterpret_cast(path), "r"); + if (!file) { + throw std::system_error{errno, std::generic_category()}; + } + + // Ensure the file is closed + const auto guard = scope_exit{[&file]() noexcept { std::fclose(file); }}; + + auto fd = ::fileno(file); + + struct stat s; + if (::fstat(fd, &s)) { + throw std::system_error{errno, std::generic_category()}; + } + + buf_ = std::malloc(s.st_size); + if (!buf_) { + throw std::bad_alloc(); + } + + const auto nitems = std::fread(buf_, 1, s.st_size, file); + if (nitems != s.st_size && std::ferror(file)) { + throw std::system_error{errno, std::generic_category()}; + } + + len_ = nitems; + pos_ = 0; +} + +void sfb::FileContentsInput::_close() noexcept { + std::free(buf_); + buf_ = nullptr; +} + +CFStringRef sfb::FileContentsInput::_copyDescription() const noexcept { + CFStringRef lastPathComponent = CFURLCopyLastPathComponent(url_); + const auto guard = scope_exit{[&lastPathComponent]() noexcept { CFRelease(lastPathComponent); }}; + return CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, + CFSTR(""), this, len_, + buf_, lastPathComponent); +} diff --git a/Sources/CSFBAudioEngine/Input/FileContentsInput.hpp b/Sources/CSFBAudioEngine/Input/FileContentsInput.hpp new file mode 100644 index 000000000..ad7ec7324 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileContentsInput.hpp @@ -0,0 +1,33 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#pragma once + +#import "BufferInput.hpp" + +namespace sfb { + +class FileContentsInput : public BufferInput { + public: + explicit FileContentsInput(CFURLRef _Nonnull url); + ~FileContentsInput() noexcept = default; + + // This class is non-copyable. + FileContentsInput(const FileContentsInput &) = delete; + FileContentsInput(FileContentsInput &&) = delete; + + // This class is non-assignable. + FileContentsInput &operator=(const FileContentsInput &) = delete; + FileContentsInput &operator=(FileContentsInput &&) = delete; + + private: + void _open() override; + void _close() noexcept override; + CFStringRef _Nonnull _copyDescription() const noexcept override; +}; + +} /* namespace sfb */ diff --git a/Sources/CSFBAudioEngine/Input/FileInput.cpp b/Sources/CSFBAudioEngine/Input/FileInput.cpp new file mode 100644 index 000000000..ac4589094 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileInput.cpp @@ -0,0 +1,87 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import "FileInput.hpp" + +#import "scope_exit.hpp" + +#import + +sfb::FileInput::FileInput(CFURLRef url) { + if (!url) { + os_log_error(log_, "Cannot create FileInput with null URL"); + throw std::invalid_argument("Null URL"); + } + url_ = static_cast(CFRetain(url)); +} + +sfb::FileInput::~FileInput() noexcept { + if (file_) { + std::fclose(file_); + } +} + +void sfb::FileInput::_open() { + UInt8 path[PATH_MAX]; + auto success = CFURLGetFileSystemRepresentation(url_, FALSE, path, PATH_MAX); + if (!success) { + throw std::runtime_error("Unable to get URL file system representation"); + } + + file_ = std::fopen(reinterpret_cast(path), "r"); + if (!file_) { + throw std::system_error{errno, std::generic_category()}; + } + + struct stat s; + if (::fstat(::fileno(file_), &s)) { + std::fclose(file_); + file_ = nullptr; + throw std::system_error{errno, std::generic_category()}; + } + + len_ = s.st_size; + + // Regular files are always seekable + if (S_ISREG(s.st_mode)) { + seekable_ = true; + } else if (const auto offset = ::ftello(file_); offset != -1) { + if (::fseeko(file_, offset, SEEK_SET) == 0) { + seekable_ = true; + } + } +} + +void sfb::FileInput::_close() { + const auto defer = scope_exit{[this]() noexcept { file_ = nullptr; }}; + if (std::fclose(file_)) { + throw std::system_error{errno, std::generic_category()}; + } +} + +int64_t sfb::FileInput::_read(void *buffer, int64_t count) { + const auto nitems = std::fread(buffer, 1, count, file_); + if (nitems != count && std::ferror(file_)) { + throw std::system_error{errno, std::generic_category()}; + } + return nitems; +} + +int64_t sfb::FileInput::_position() const { + const auto offset = ::ftello(file_); + if (offset == -1) { + throw std::system_error{errno, std::generic_category()}; + } + return offset; +} + +CFStringRef sfb::FileInput::_copyDescription() const noexcept { + CFStringRef lastPathComponent = CFURLCopyLastPathComponent(url_); + const auto guard = scope_exit{[&lastPathComponent]() noexcept { CFRelease(lastPathComponent); }}; + return CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, CFSTR(""), this, + lastPathComponent); +} diff --git a/Sources/CSFBAudioEngine/Input/FileInput.hpp b/Sources/CSFBAudioEngine/Input/FileInput.hpp new file mode 100644 index 000000000..b28860921 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileInput.hpp @@ -0,0 +1,51 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#pragma once + +#import "InputSource.hpp" + +#import +#import + +namespace sfb { + +class FileInput : public InputSource { + public: + explicit FileInput(CFURLRef _Nonnull url); + ~FileInput() noexcept; + + // This class is non-copyable. + FileInput(const FileInput &) = delete; + FileInput(FileInput &&) = delete; + + // This class is non-assignable. + FileInput &operator=(const FileInput &) = delete; + FileInput &operator=(FileInput &&) = delete; + + private: + bool _atEOF() const noexcept override { return std::feof(file_) != 0; } + int64_t _length() const noexcept override { return len_; } + bool _supportsSeeking() const noexcept override { return seekable_; } + void _seekToPosition(int64_t position) override { + if (::fseeko(file_, static_cast(position), SEEK_SET)) { + throw std::system_error{errno, std::generic_category()}; + } + } + + void _open() override; + void _close() override; + int64_t _read(void *_Nonnull buffer, int64_t count) override; + int64_t _position() const override; + CFStringRef _Nonnull _copyDescription() const noexcept override; + + FILE *_Nullable file_{nullptr}; + int64_t len_{0}; + bool seekable_{false}; +}; + +} /* namespace sfb */ diff --git a/Sources/CSFBAudioEngine/Input/InputSource.cpp b/Sources/CSFBAudioEngine/Input/InputSource.cpp new file mode 100644 index 000000000..f47cf8e1c --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/InputSource.cpp @@ -0,0 +1,211 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import "InputSource.hpp" + +#import "BufferInput.hpp" +#import "DataInput.hpp" +#import "FileContentsInput.hpp" +#import "FileInput.hpp" +#import "MemoryMappedFileInput.hpp" +#import "scope_exit.hpp" + +#import +#import + +namespace sfb { + +const os_log_t InputSource::log_ = os_log_create("org.sbooth.AudioEngine", "InputSource"); + +} /* namespace sfb */ + +sfb::InputSource::unique_ptr sfb::InputSource::createForURL(CFURLRef url, FileReadMode mode) { + switch (mode) { + case FileReadMode::normal: + return std::make_unique(url); + case FileReadMode::memoryMap: + return std::make_unique(url); + case FileReadMode::loadInMemory: + return std::make_unique(url); + } +} + +sfb::InputSource::unique_ptr sfb::InputSource::createWithData(CFDataRef data) { + return std::make_unique(data); +} + +sfb::InputSource::unique_ptr sfb::InputSource::createWithBytes(const void *buf, int64_t len) { + return std::make_unique(buf, len, BufferInput::BufferAdoption::copy); +} + +sfb::InputSource::unique_ptr sfb::InputSource::createWithBytesNoCopy(const void *buf, int64_t len, bool free) { + return std::make_unique( + buf, len, free ? BufferInput::BufferAdoption::noCopyAndFree : BufferInput::BufferAdoption::noCopy); +} + +sfb::InputSource::~InputSource() noexcept { + if (url_) { + CFRelease(url_); + } +} + +void sfb::InputSource::open() { + if (isOpen()) { + os_log_debug(log_, "Open() called on that is already open", this); + return; + } + + _open(); + isOpen_ = true; +} + +void sfb::InputSource::close() { + if (!isOpen()) { + os_log_debug(log_, "Close() called on that hasn't been opened", this); + return; + } + + const auto defer = scope_exit{[this]() noexcept { isOpen_ = false; }}; + _close(); +} + +int64_t sfb::InputSource::read(void *buffer, int64_t count) { + if (!isOpen()) { + os_log_error(log_, "Read() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + if (!buffer || count < 0) { + os_log_error(log_, "Read() called on with null buffer or invalid count", this); + throw std::invalid_argument("Null buffer or negative count"); + } + + return _read(buffer, count); +} + +CFDataRef sfb::InputSource::copyData(int64_t count) { + if (!isOpen()) { + os_log_error(log_, "CopyData() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + if (count < 0 || count > std::numeric_limits::max()) { + os_log_error(log_, "CopyData() called on with invalid count", this); + throw std::invalid_argument("Invalid count"); + } + + if (count == 0) { + return CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, nullptr, 0, kCFAllocatorNull); + } + + void *buf = std::malloc(count); + if (!buf) { + throw std::bad_alloc(); + } + + try { + const auto read = _read(buf, count); + auto data = + CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, static_cast(buf), read, kCFAllocatorMalloc); + if (!data) { + std::free(buf); + } + return data; + } catch (...) { + std::free(buf); + throw; + } +} + +std::vector sfb::InputSource::readBlock(std::vector::size_type count) { + if (!isOpen()) { + os_log_error(log_, "ReadBlock() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + if (count == 0) { + return {}; + } + + std::vector vec; + vec.reserve(count); + vec.resize(_read(vec.data(), vec.capacity())); + return vec; +} + +bool sfb::InputSource::atEOF() const { + if (!isOpen()) { + os_log_error(log_, "AtEOF() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + return _atEOF(); +} + +int64_t sfb::InputSource::position() const { + if (!isOpen()) { + os_log_error(log_, "Position() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + return _position(); +} + +int64_t sfb::InputSource::length() const { + if (!isOpen()) { + os_log_error(log_, "Length() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + return _length(); +} + +bool sfb::InputSource::supportsSeeking() const { + if (!isOpen()) { + os_log_error(log_, "SupportsSeeking() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + return _supportsSeeking(); +} + +void sfb::InputSource::seekToOffset(int64_t offset, SeekAnchor whence) { + if (!isOpen()) { + os_log_error(log_, "SeekToOffset() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + if (!_supportsSeeking()) { + os_log_error(log_, "SeekToOffset() called on that doesn't support seeking", this); + throw std::logic_error("Seeking not supported"); + } + + const auto len = _length(); + + switch (whence) { + case SeekAnchor::start: + /* unchanged */ + break; + + case SeekAnchor::current: + offset += _position(); + break; + + case SeekAnchor::end: + offset += len; + break; + } + + if (offset < 0 || offset > len) { + os_log_error(log_, "SeekToOffset() called on with invalid position %lld", this, offset); + throw std::out_of_range("Invalid seek position"); + } + + return _seekToPosition(offset); +} + +CFStringRef sfb::InputSource::copyDescription() const noexcept { return _copyDescription(); } diff --git a/Sources/CSFBAudioEngine/Input/InputSource.hpp b/Sources/CSFBAudioEngine/Input/InputSource.hpp new file mode 100644 index 000000000..5e090b0d8 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/InputSource.hpp @@ -0,0 +1,222 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#pragma once + +#import + +#import +#import + +#import +#import +#import +#import + +namespace sfb { + +/// An input source. +class InputSource { + public: + using unique_ptr = std::unique_ptr; + + enum class FileReadMode { + normal, + memoryMap, + loadInMemory, + }; + static unique_ptr createForURL(CFURLRef _Nonnull url, FileReadMode mode = FileReadMode::normal); + static unique_ptr createWithData(CFDataRef _Nonnull data); + static unique_ptr createWithBytes(const void *_Nonnull buf, int64_t len); + static unique_ptr createWithBytesNoCopy(const void *_Nonnull buf, int64_t len, bool free = true); + + virtual ~InputSource() noexcept; + + // This class is non-copyable. + InputSource(const InputSource &) = delete; + InputSource(InputSource &&) = delete; + + // This class is non-assignable. + InputSource &operator=(const InputSource &) = delete; + InputSource &operator=(InputSource &&) = delete; + + /// Returns the URL, if any, of the input source. + [[nodiscard]] CFURLRef _Nullable getURL() const noexcept { return url_; } + + // MARK: Opening and Closing + + /// Opens the input source. + void open(); + + /// Closes the input source. + void close(); + + /// Returns `true` if the input source is open. + [[nodiscard]] bool isOpen() const noexcept { return isOpen_; } + + // MARK: Reading + + /// Reads up to `count` bytes from the input source into `buffer` and returns the number of bytes read. + int64_t read(void *_Nonnull buffer, int64_t count); + + /// Reads and returns up to `count` bytes from the input source in a `CFData` object. + [[nodiscard]] CFDataRef _Nullable copyData(int64_t count); + + /// Reads and returns up to `count` bytes from the input source in a `std::vector` object. + [[nodiscard]] std::vector readBlock(std::vector::size_type count); + + // MARK: Position + + /// Returns `true` if the input source is at the end of input. + [[nodiscard]] bool atEOF() const; + + /// Returns the current read position of the input source in bytes. + [[nodiscard]] int64_t position() const; + + /// Returns the number of bytes in the input source. + [[nodiscard]] int64_t length() const; + + // MARK: Seeking + + /// Returns `true` if the input source is seekable. + [[nodiscard]] bool supportsSeeking() const; + + /// Possible seek anchor points. + enum class SeekAnchor { + start, + current, + end, + }; + + /// Seeks to `offset` bytes relative to `whence`. + void seekToOffset(int64_t offset, SeekAnchor whence = SeekAnchor::start); + + // MARK: Helpers + + /// Reads and returns a value from the input source. + template && + std::is_trivially_default_constructible_v>> + [[nodiscard]] V readValue() { + if (!isOpen()) { + os_log_error(log_, "ReadValue() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + V value; + if (_read(&value, sizeof(V)) != sizeof(V)) { + throw std::runtime_error("Insufficient data"); + } + return value; + } + + /// Possible byte orders. + enum class ByteOrder { + little, + big, + host, + swapped, + }; + + /// Reads and returns an unsigned integer value in the specified byte order. + template || std::is_same_v || + std::is_same_v>> + [[nodiscard]] U readUnsigned(ByteOrder order = ByteOrder::host) { + if (!isOpen()) { + os_log_error(log_, "ReadUnsigned() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + U value; + if (_read(&value, sizeof(U)) != sizeof(U)) { + throw std::runtime_error("Insufficient data"); + } + + if constexpr (std::is_same_v) { + switch (order) { + case ByteOrder::little: + return OSSwapLittleToHostInt16(value); + case ByteOrder::big: + return OSSwapBigToHostInt16(value); + case ByteOrder::host: + return value; + case ByteOrder::swapped: + return OSSwapInt16(value); + } + } else if constexpr (std::is_same_v) { + switch (order) { + case ByteOrder::little: + return OSSwapLittleToHostInt32(value); + case ByteOrder::big: + return OSSwapBigToHostInt32(value); + case ByteOrder::host: + return value; + case ByteOrder::swapped: + return OSSwapInt32(value); + } + } else if constexpr (std::is_same_v) { + switch (order) { + case ByteOrder::little: + return OSSwapLittleToHostInt64(value); + case ByteOrder::big: + return OSSwapBigToHostInt64(value); + case ByteOrder::host: + return value; + case ByteOrder::swapped: + return OSSwapInt64(value); + } + } else { + static_assert(false, "Unsupported unsigned integer type"); + } + } + + /// Reads and returns a signed integer value in the specified byte order. + template || std::is_same_v || + std::is_same_v>> + [[nodiscard]] S readSigned(ByteOrder order = ByteOrder::host) { + return std::make_signed(ReadUnsigned>(order)); + } + + // MARK: Debugging + + /// Returns a description of the input source. + [[nodiscard]] CFStringRef _Nonnull copyDescription() const noexcept CF_RETURNS_RETAINED; + + protected: + /// The shared log for all `InputSource` instances. + static const os_log_t _Nonnull log_; + + explicit InputSource() noexcept = default; + + /// The location of the input. + CFURLRef _Nullable url_{nullptr}; + + private: + // Subclasses must implement the following methods + virtual void _open() = 0; + virtual void _close() = 0; + virtual int64_t _read(void *_Nonnull buffer, int64_t count) = 0; + virtual bool _atEOF() const = 0; + virtual int64_t _position() const = 0; + virtual int64_t _length() const = 0; + + // Optional seeking support + virtual bool _supportsSeeking() const { return false; } + + virtual void _seekToPosition(int64_t position) { throw std::logic_error("Seeking not supported"); } + + // Optional description + virtual CFStringRef _Nonnull _copyDescription() const noexcept { + return CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, CFSTR(""), this); + } + + /// `true` if the input source is open. + bool isOpen_{false}; +}; + +} /* namespace sfb */ diff --git a/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.cpp b/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.cpp new file mode 100644 index 000000000..22106db98 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.cpp @@ -0,0 +1,85 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import "MemoryMappedFileInput.hpp" + +#import "scope_exit.hpp" + +#import +#import +#import + +#import +#import + +sfb::MemoryMappedFileInput::MemoryMappedFileInput(CFURLRef url) { + if (!url) { + os_log_error(log_, "Cannot create MemoryMappedFileInput with null URL"); + throw std::invalid_argument("Null URL"); + } + url_ = static_cast(CFRetain(url)); + free_ = false; +} + +sfb::MemoryMappedFileInput::~MemoryMappedFileInput() noexcept { + if (buf_) { + munmap(buf_, len_); + } +} + +void sfb::MemoryMappedFileInput::_open() { + UInt8 path[PATH_MAX]; + auto success = CFURLGetFileSystemRepresentation(url_, FALSE, path, PATH_MAX); + if (!success) { + throw std::runtime_error("Unable to get URL file system representation"); + } + + auto file = std::fopen(reinterpret_cast(path), "r"); + if (!file) { + throw std::system_error{errno, std::generic_category()}; + } + + // Ensure the file is closed + const auto guard = scope_exit{[&file]() noexcept { std::fclose(file); }}; + + auto fd = ::fileno(file); + + struct stat s; + if (::fstat(fd, &s)) { + throw std::system_error{errno, std::generic_category()}; + } + + // Only regular files can be mapped + if (!S_ISREG(s.st_mode)) { + throw std::system_error{ENOTSUP, std::generic_category()}; + } + + // Map the file to memory + auto region = mmap(nullptr, s.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (region == MAP_FAILED) { + throw std::system_error{errno, std::generic_category()}; + } + + buf_ = region; + len_ = s.st_size; + pos_ = 0; +} + +void sfb::MemoryMappedFileInput::_close() { + const auto defer = scope_exit{[this]() noexcept { buf_ = nullptr; }}; + if (munmap(buf_, len_)) { + throw std::system_error{errno, std::generic_category()}; + } +} + +CFStringRef sfb::MemoryMappedFileInput::_copyDescription() const noexcept { + CFStringRef lastPathComponent = CFURLCopyLastPathComponent(url_); + const auto guard = scope_exit{[&lastPathComponent]() noexcept { CFRelease(lastPathComponent); }}; + return CFStringCreateWithFormat(kCFAllocatorDefault, nullptr, + CFSTR(""), this, + len_, buf_, lastPathComponent); +} diff --git a/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.hpp b/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.hpp new file mode 100644 index 000000000..c2a1e8da4 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.hpp @@ -0,0 +1,33 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#pragma once + +#import "BufferInput.hpp" + +namespace sfb { + +class MemoryMappedFileInput : public BufferInput { + public: + explicit MemoryMappedFileInput(CFURLRef _Nonnull url); + ~MemoryMappedFileInput() noexcept; + + // This class is non-copyable. + MemoryMappedFileInput(const MemoryMappedFileInput &) = delete; + MemoryMappedFileInput(MemoryMappedFileInput &&) = delete; + + // This class is non-assignable. + MemoryMappedFileInput &operator=(const MemoryMappedFileInput &) = delete; + MemoryMappedFileInput &operator=(MemoryMappedFileInput &&) = delete; + + private: + void _open() override; + void _close() override; + CFStringRef _Nonnull _copyDescription() const noexcept override; +}; + +} /* namespace sfb */ diff --git a/Sources/CSFBAudioEngine/Input/SFBDataInputSource.h b/Sources/CSFBAudioEngine/Input/SFBDataInputSource.h deleted file mode 100644 index 8e182a6a2..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBDataInputSource.h +++ /dev/null @@ -1,20 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBInputSource+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface SFBDataInputSource : SFBInputSource -+ (instancetype)new NS_UNAVAILABLE; -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithURL:(nullable NSURL *)url NS_UNAVAILABLE; -- (instancetype)initWithData:(NSData *)data; -- (instancetype)initWithData:(NSData *)data url:(nullable NSURL *)url NS_DESIGNATED_INITIALIZER; -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/CSFBAudioEngine/Input/SFBDataInputSource.m b/Sources/CSFBAudioEngine/Input/SFBDataInputSource.m deleted file mode 100644 index 3797c414c..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBDataInputSource.m +++ /dev/null @@ -1,96 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBDataInputSource.h" - -@interface SFBDataInputSource () { - @private - NSData *_data; - NSUInteger _pos; -} -@end - -@implementation SFBDataInputSource - -- (instancetype)initWithData:(NSData *)data { - return [self initWithData:data url:nil]; -} - -- (instancetype)initWithData:(NSData *)data url:(NSURL *)url { - NSParameterAssert(data != nil); - - if ((self = [super initWithURL:url])) { - _data = [data copy]; - } - return self; -} - -- (BOOL)openReturningError:(NSError **)error { - return YES; -} - -- (BOOL)closeReturningError:(NSError **)error { - _data = nil; - return YES; -} - -- (BOOL)isOpen { - return _data != nil; -} - -- (BOOL)readBytes:(void *)buffer length:(NSInteger)length bytesRead:(NSInteger *)bytesRead error:(NSError **)error { - NSParameterAssert(buffer != NULL); - NSParameterAssert(length >= 0); - NSParameterAssert(bytesRead != NULL); - - NSUInteger count = (NSUInteger)length; - NSUInteger remaining = _data.length - _pos; - if (count > remaining) { - count = remaining; - } - - [_data getBytes:buffer range:NSMakeRange(_pos, count)]; - _pos += count; - *bytesRead = (NSInteger)count; - - return YES; -} - -- (BOOL)atEOF { - return _pos == _data.length; -} - -- (BOOL)getOffset:(NSInteger *)offset error:(NSError **)error { - NSParameterAssert(offset != NULL); - *offset = (NSInteger)_pos; - return YES; -} - -- (BOOL)getLength:(NSInteger *)length error:(NSError **)error { - NSParameterAssert(length != NULL); - *length = (NSInteger)_data.length; - return YES; -} - -- (BOOL)supportsSeeking { - return YES; -} - -- (BOOL)seekToOffset:(NSInteger)offset error:(NSError **)error { - NSParameterAssert(offset >= 0); - if ((NSUInteger)offset > _data.length) { - if (error) { - *error = [self posixErrorWithCode:EINVAL]; - } - return NO; - } - - _pos = (NSUInteger)offset; - return YES; -} - -@end diff --git a/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.h b/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.h deleted file mode 100644 index 28e15adc5..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBDataInputSource.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface SFBFileContentsInputSource : SFBDataInputSource -+ (instancetype)new NS_UNAVAILABLE; -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithData:(NSData *)data url:(nullable NSURL *)url NS_UNAVAILABLE; -- (nullable instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)error NS_DESIGNATED_INITIALIZER; -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.m b/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.m deleted file mode 100644 index c5301831a..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.m +++ /dev/null @@ -1,24 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBFileContentsInputSource.h" - -@implementation SFBFileContentsInputSource - -- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)error { - NSParameterAssert(url != nil); - NSParameterAssert(url.isFileURL); - - NSData *data = [NSData dataWithContentsOfURL:url options:0 error:error]; - if (!data) { - return nil; - } - - return [super initWithData:data url:url]; -} - -@end diff --git a/Sources/CSFBAudioEngine/Input/SFBFileInputSource.h b/Sources/CSFBAudioEngine/Input/SFBFileInputSource.h deleted file mode 100644 index 9e3d712dc..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileInputSource.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBInputSource+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface SFBFileInputSource : SFBInputSource -+ (instancetype)new NS_UNAVAILABLE; -- (instancetype)init NS_UNAVAILABLE; -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/CSFBAudioEngine/Input/SFBFileInputSource.m b/Sources/CSFBAudioEngine/Input/SFBFileInputSource.m deleted file mode 100644 index 44d893f49..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileInputSource.m +++ /dev/null @@ -1,136 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBFileInputSource.h" - -#import "SFBInputSource+Internal.h" - -#import -#import - -@interface SFBFileInputSource () { - @private - struct stat _filestats; - FILE *_file; -} -@end - -@implementation SFBFileInputSource - -- (BOOL)openReturningError:(NSError **)error { - _file = fopen(_url.fileSystemRepresentation, "r"); - if (!_file) { - int err = errno; - os_log_error(gSFBInputSourceLog, "fopen failed: %{public}s (%d)", strerror(err), err); - if (error) { - *error = [self posixErrorWithCode:err]; - } - return NO; - } - - if (fstat(fileno(_file), &_filestats) == -1) { - int err = errno; - os_log_error(gSFBInputSourceLog, "fstat failed: %{public}s (%d)", strerror(err), err); - if (error) { - *error = [self posixErrorWithCode:err]; - } - - if (fclose(_file)) { - os_log_info(gSFBInputSourceLog, "fclose failed: %{public}s (%d)", strerror(errno), errno); - } - _file = NULL; - - return NO; - } - - return YES; -} - -- (BOOL)closeReturningError:(NSError **)error { - if (_file) { - int result = fclose(_file); - _file = NULL; - if (result) { - int err = errno; - os_log_error(gSFBInputSourceLog, "fclose failed: %{public}s (%d)", strerror(err), err); - if (error) { - *error = [self posixErrorWithCode:err]; - } - return NO; - } - } - return YES; -} - -- (BOOL)isOpen { - return _file != NULL; -} - -- (BOOL)readBytes:(void *)buffer length:(NSInteger)length bytesRead:(NSInteger *)bytesRead error:(NSError **)error { - NSParameterAssert(buffer != NULL); - NSParameterAssert(length >= 0); - NSParameterAssert(bytesRead != NULL); - - size_t read = fread(buffer, 1, (size_t)length, _file); - if (read != (size_t)length && ferror(_file)) { - int err = errno; - os_log_error(gSFBInputSourceLog, "fread error: %{public}s (%d)", strerror(err), err); - if (error) { - *error = [self posixErrorWithCode:err]; - } - return NO; - } - *bytesRead = (NSInteger)read; - return YES; -} - -- (BOOL)atEOF { - return feof(_file) != 0; -} - -- (BOOL)getOffset:(NSInteger *)offset error:(NSError **)error { - NSParameterAssert(offset != NULL); - off_t result = ftello(_file); - if (result == -1) { - int err = errno; - os_log_error(gSFBInputSourceLog, "ftello failed: %{public}s (%d)", strerror(err), err); - if (error) { - *error = [self posixErrorWithCode:err]; - } - return NO; - } - *offset = result; - return YES; -} - -- (BOOL)getLength:(NSInteger *)length error:(NSError **)error { - NSParameterAssert(length != NULL); - *length = _filestats.st_size; - return YES; -} - -- (BOOL)supportsSeeking { - // Regular files are always seekable. - // Punt on testing whether ftello() and fseeko() actually work. - return S_ISREG(_filestats.st_mode); -} - -- (BOOL)seekToOffset:(NSInteger)offset error:(NSError **)error { - NSParameterAssert(offset >= 0); - if (fseeko(_file, offset, SEEK_SET)) { - int err = errno; - os_log_error(gSFBInputSourceLog, "fseeko(%ld, SEEK_SET) error: %{public}s (%d)", (long)offset, strerror(err), - err); - if (error) { - *error = [self posixErrorWithCode:err]; - } - return NO; - } - return YES; -} - -@end diff --git a/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h b/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h index 1a46c57cf..ce05487e3 100644 --- a/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h +++ b/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h @@ -5,20 +5,15 @@ // Part of https://github.com/sbooth/SFBAudioEngine // +#import "InputSource.hpp" #import "SFBInputSource.h" -#import - NS_ASSUME_NONNULL_BEGIN -extern os_log_t gSFBInputSourceLog; - @interface SFBInputSource () { @package - NSURL *_url; + sfb::InputSource::unique_ptr _input; } -- (instancetype)initWithURL:(nullable NSURL *)url NS_DESIGNATED_INITIALIZER; -- (NSError *)posixErrorWithCode:(NSInteger)code; @end NS_ASSUME_NONNULL_END diff --git a/Sources/CSFBAudioEngine/Input/SFBInputSource.m b/Sources/CSFBAudioEngine/Input/SFBInputSource.m deleted file mode 100644 index 613c94fd2..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBInputSource.m +++ /dev/null @@ -1,356 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "NSData+SFBExtensions.h" -#import "SFBDataInputSource.h" -#import "SFBFileContentsInputSource.h" -#import "SFBFileInputSource.h" -#import "SFBInputSource+Internal.h" -#import "SFBMemoryMappedFileInputSource.h" - -// NSError domain for InputSource and subclasses -NSErrorDomain const SFBInputSourceErrorDomain = @"org.sbooth.AudioEngine.InputSource"; - -os_log_t gSFBInputSourceLog = NULL; - -static void SFBCreateInputSourceLog(void) __attribute__((constructor)); -static void SFBCreateInputSourceLog(void) { - gSFBInputSourceLog = os_log_create("org.sbooth.AudioEngine", "InputSource"); -} - -@implementation SFBInputSource - -+ (void)load { - [NSError setUserInfoValueProviderForDomain:SFBInputSourceErrorDomain - provider:^id(NSError *err, NSErrorUserInfoKey userInfoKey) { - switch (err.code) { - case SFBInputSourceErrorCodeFileNotFound: - if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey]) { - return NSLocalizedString(@"The requested file was not found.", @""); - } - break; - - case SFBInputSourceErrorCodeInputOutput: - if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey]) { - return NSLocalizedString(@"An input/output error occurred.", @""); - } - break; - - case SFBInputSourceErrorCodeNotSeekable: - if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey]) { - return NSLocalizedString(@"The input does not support seeking.", @""); - } - break; - } - - return nil; - }]; -} - -+ (instancetype)inputSourceForURL:(NSURL *)url error:(NSError **)error { - return [SFBInputSource inputSourceForURL:url flags:0 error:error]; -} - -+ (instancetype)inputSourceForURL:(NSURL *)url flags:(SFBInputSourceFlags)flags error:(NSError **)error { - NSParameterAssert(url != nil); - NSParameterAssert(url.isFileURL); - - if (flags & SFBInputSourceFlagsMemoryMapFiles) { - return [[SFBMemoryMappedFileInputSource alloc] initWithURL:url error:error]; - } - if (flags & SFBInputSourceFlagsLoadFilesInMemory) { - return [[SFBFileContentsInputSource alloc] initWithContentsOfURL:url error:error]; - } - return [[SFBFileInputSource alloc] initWithURL:url]; -} - -+ (instancetype)inputSourceWithData:(NSData *)data { - NSParameterAssert(data != nil); - return [[SFBDataInputSource alloc] initWithData:data]; -} - -+ (instancetype)inputSourceWithBytes:(const void *)bytes length:(NSInteger)length { - NSParameterAssert(bytes != NULL); - NSParameterAssert(length >= 0); - NSData *data = [NSData dataWithBytes:bytes length:(NSUInteger)length]; - if (!data) { - return nil; - } - return [[SFBDataInputSource alloc] initWithData:data]; -} - -+ (instancetype)inputSourceWithBytesNoCopy:(void *)bytes length:(NSInteger)length freeWhenDone:(BOOL)freeWhenDone { - NSParameterAssert(bytes != NULL); - NSParameterAssert(length >= 0); - NSData *data = [NSData dataWithBytesNoCopy:bytes length:(NSUInteger)length freeWhenDone:freeWhenDone]; - if (!data) { - return nil; - } - return [[SFBDataInputSource alloc] initWithData:data]; -} - -- (instancetype)initWithURL:(NSURL *)url { - if ((self = [super init])) { - _url = url; - } - return self; -} - -- (void)dealloc { - if (self.isOpen) { - [self closeReturningError:nil]; - } -} - -- (BOOL)openReturningError:(NSError **)error { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (BOOL)closeReturningError:(NSError **)error { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (BOOL)isOpen { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (BOOL)readBytes:(void *)buffer length:(NSInteger)length bytesRead:(NSInteger *)bytesRead error:(NSError **)error { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (BOOL)getOffset:(NSInteger *)offset error:(NSError **)error { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (BOOL)getLength:(NSInteger *)length error:(NSError **)error { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (BOOL)supportsSeeking { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (BOOL)seekToOffset:(NSInteger)offset error:(NSError **)error { - [self doesNotRecognizeSelector:_cmd]; - __builtin_unreachable(); -} - -- (NSError *)posixErrorWithCode:(NSInteger)code { - NSDictionary *userInfo = nil; - if (_url) { - userInfo = [NSDictionary dictionaryWithObject:_url forKey:NSURLErrorKey]; - } - return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]; -} - -- (NSString *)description { - if (_url) { - return [NSString stringWithFormat:@"<%@ %p: \"%@\">", [self class], (__bridge void *)self, - [[NSFileManager defaultManager] displayNameAtPath:_url.path]]; - } - return [NSString stringWithFormat:@"<%@ %p>", [self class], (__bridge void *)self]; -} - -@end - -@implementation SFBInputSource (SFBSignedIntegerReading) -- (BOOL)readInt8:(int8_t *)i8 error:(NSError **)error { - return [self readUInt8:(uint8_t *)i8 error:error]; -} -- (BOOL)readInt16:(int16_t *)i16 error:(NSError **)error { - return [self readUInt16:(uint16_t *)i16 error:error]; -} -- (BOOL)readInt32:(int32_t *)i32 error:(NSError **)error { - return [self readUInt32:(uint32_t *)i32 error:error]; -} -- (BOOL)readInt64:(int64_t *)i64 error:(NSError **)error { - return [self readUInt64:(uint64_t *)i64 error:error]; -} -@end - -@implementation SFBInputSource (SFBUnsignedIntegerReading) -- (BOOL)readUInt8:(uint8_t *)ui8 error:(NSError **)error { - NSInteger bytesRead; - return [self readBytes:ui8 length:sizeof(uint8_t) bytesRead:&bytesRead error:error] && bytesRead == sizeof(uint8_t); -} - -- (BOOL)readUInt16:(uint16_t *)ui16 error:(NSError **)error { - NSInteger bytesRead; - return [self readBytes:ui16 length:sizeof(uint16_t) bytesRead:&bytesRead error:error] && - bytesRead == sizeof(uint16_t); -} - -- (BOOL)readUInt32:(uint32_t *)ui32 error:(NSError **)error { - NSInteger bytesRead; - return [self readBytes:ui32 length:sizeof(uint32_t) bytesRead:&bytesRead error:error] && - bytesRead == sizeof(uint32_t); -} - -- (BOOL)readUInt64:(uint64_t *)ui64 error:(NSError **)error { - NSInteger bytesRead; - return [self readBytes:ui64 length:sizeof(uint64_t) bytesRead:&bytesRead error:error] && - bytesRead == sizeof(uint64_t); -} - -@end - -@implementation SFBInputSource (SFBBigEndianReading) - -- (BOOL)readUInt16BigEndian:(uint16_t *)ui16 error:(NSError **)error { - NSParameterAssert(ui16 != nil); - if (![self readUInt16:ui16 error:error]) { - return NO; - } - *ui16 = OSSwapHostToBigInt16(*ui16); - return YES; -} - -- (BOOL)readUInt32BigEndian:(uint32_t *)ui32 error:(NSError **)error { - NSParameterAssert(ui32 != nil); - if (![self readUInt32:ui32 error:error]) { - return NO; - } - *ui32 = OSSwapHostToBigInt32(*ui32); - return YES; -} - -- (BOOL)readUInt64BigEndian:(uint64_t *)ui64 error:(NSError **)error { - NSParameterAssert(ui64 != nil); - if (![self readUInt64:ui64 error:error]) { - return NO; - } - *ui64 = OSSwapHostToBigInt64(*ui64); - return YES; -} - -@end - -@implementation SFBInputSource (SFBLittleEndianReading) - -- (BOOL)readUInt16LittleEndian:(uint16_t *)ui16 error:(NSError **)error { - NSParameterAssert(ui16 != nil); - if (![self readUInt16:ui16 error:error]) { - return NO; - } - *ui16 = OSSwapHostToLittleInt16(*ui16); - return YES; -} - -- (BOOL)readUInt32LittleEndian:(uint32_t *)ui32 error:(NSError **)error { - NSParameterAssert(ui32 != nil); - if (![self readUInt32:ui32 error:error]) { - return NO; - } - *ui32 = OSSwapHostToLittleInt32(*ui32); - return YES; -} - -- (BOOL)readUInt64LittleEndian:(uint64_t *)ui64 error:(NSError **)error { - NSParameterAssert(ui64 != nil); - if (![self readUInt64:ui64 error:error]) { - return NO; - } - *ui64 = OSSwapHostToLittleInt64(*ui64); - return YES; -} - -@end - -@implementation SFBInputSource (SFBDataReading) - -- (NSData *)readDataOfLength:(NSUInteger)length error:(NSError **)error { - if (length == 0) { - return [NSData data]; - } - - void *buf = malloc(length); - if (!buf) { - if (error) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:ENOMEM userInfo:nil]; - } - return nil; - } - - NSInteger bytesRead = 0; - if (![self readBytes:buf length:length bytesRead:&bytesRead error:error]) { - free(buf); - return nil; - } - - return [NSData dataWithBytesNoCopy:buf length:bytesRead freeWhenDone:YES]; -} - -@end - -@implementation SFBInputSource (SFBHeaderReading) - -- (NSData *)readHeaderOfLength:(NSUInteger)length skipID3v2Tag:(BOOL)skipID3v2Tag error:(NSError **)error { - NSParameterAssert(length > 0); - - if (!self.supportsSeeking) { - if (error) { - NSDictionary *userInfo = nil; - if (_url) { - userInfo = [NSDictionary dictionaryWithObject:_url forKey:NSURLErrorKey]; - } - *error = [NSError errorWithDomain:SFBInputSourceErrorDomain - code:SFBInputSourceErrorCodeNotSeekable - userInfo:userInfo]; - } - return nil; - } - - NSInteger originalOffset; - if (![self getOffset:&originalOffset error:error]) { - return nil; - } - - if (![self seekToOffset:0 error:error]) { - return nil; - } - - if (skipID3v2Tag) { - NSInteger offset = 0; - - // Attempt to detect and minimally parse an ID3v2 tag header - NSData *data = [self readDataOfLength:SFBID3v2HeaderSize error:error]; - if ([data isID3v2Header]) { - offset = [data id3v2TagTotalSize]; - } - - if (![self seekToOffset:offset error:error]) { - return nil; - } - } - - NSData *data = [self readDataOfLength:length error:error]; - if (!data) { - return nil; - } - - if (data.length < length) { - if (error) { - *error = [self posixErrorWithCode:EINVAL]; - } - return nil; - } - - if (![self seekToOffset:originalOffset error:error]) { - return nil; - } - - return data; -} - -@end diff --git a/Sources/CSFBAudioEngine/Input/SFBInputSource.mm b/Sources/CSFBAudioEngine/Input/SFBInputSource.mm new file mode 100644 index 000000000..c9786a0d3 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/SFBInputSource.mm @@ -0,0 +1,484 @@ +// +// SPDX-FileCopyrightText: 2010 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import "BufferInput.hpp" +#import "DataInput.hpp" +#import "FileContentsInput.hpp" +#import "FileInput.hpp" +#import "MemoryMappedFileInput.hpp" +#import "NSData+SFBExtensions.h" +#import "SFBInputSource+Internal.h" + +#import + +namespace { + +NSError *NSErrorFromInputSourceException(const std::exception *e) noexcept { + NSCParameterAssert(e != nullptr); + + // TODO: Set NSURLErrorKey? + + if (const auto se = dynamic_cast(e); se) { + return [NSError errorWithDomain:NSPOSIXErrorDomain + code:se->code().value() + userInfo:@{ + NSDebugDescriptionErrorKey : @(se->code().message().c_str()) + }]; + } + + if (const auto ia = dynamic_cast(e); ia) { + return [NSError errorWithDomain:NSPOSIXErrorDomain + code:EINVAL + userInfo:@{ + NSDebugDescriptionErrorKey : @(ia->what()) + }]; + } + + if (const auto oor = dynamic_cast(e); oor) { + return [NSError errorWithDomain:NSPOSIXErrorDomain + code:EDOM + userInfo:@{ + NSDebugDescriptionErrorKey : @(oor->what()) + }]; + } + + return [NSError errorWithDomain:SFBInputSourceErrorDomain + code:SFBInputSourceErrorCodeInputOutput + userInfo:@{ + NSDebugDescriptionErrorKey : @(e->what()) + }]; +} + +} /* namespace */ + +// NSError domain for InputSource and subclasses +NSErrorDomain const SFBInputSourceErrorDomain = @"org.sbooth.AudioEngine.InputSource"; + +@implementation SFBInputSource + ++ (void)load { + [NSError setUserInfoValueProviderForDomain:SFBInputSourceErrorDomain + provider:^id(NSError *err, NSErrorUserInfoKey userInfoKey) { + if ([userInfoKey isEqualToString:NSLocalizedDescriptionKey]) { + switch (err.code) { + case SFBInputSourceErrorCodeFileNotFound: + return NSLocalizedString(@"The requested file was not found.", @""); + case SFBInputSourceErrorCodeInputOutput: + return NSLocalizedString(@"An input/output error occurred.", @""); + case SFBInputSourceErrorCodeNotSeekable: + return NSLocalizedString(@"The input does not support seeking.", @""); + } + } + return nil; + }]; +} + ++ (instancetype)inputSourceForURL:(NSURL *)url error:(NSError **)error { + return [SFBInputSource inputSourceForURL:url flags:0 error:error]; +} + ++ (instancetype)inputSourceForURL:(NSURL *)url flags:(SFBInputSourceFlags)flags error:(NSError **)error { + NSParameterAssert(url != nil); + NSParameterAssert(url.isFileURL); + + try { + SFBInputSource *inputSource = [[SFBInputSource alloc] init]; + if (inputSource) { + if (flags & SFBInputSourceFlagsMemoryMapFiles) { + inputSource->_input = std::make_unique((__bridge CFURLRef)url); + } else if (flags & SFBInputSourceFlagsLoadFilesInMemory) { + inputSource->_input = std::make_unique((__bridge CFURLRef)url); + } else { + inputSource->_input = std::make_unique((__bridge CFURLRef)url); + } + } + return inputSource; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return nil; + } +} + ++ (instancetype)inputSourceWithData:(NSData *)data { + NSParameterAssert(data != nil); + + try { + SFBInputSource *inputSource = [[SFBInputSource alloc] init]; + if (inputSource) { + inputSource->_input = std::make_unique((__bridge CFDataRef)data); + } + return inputSource; + } catch (const std::exception &e) { + return nil; + } +} + ++ (instancetype)inputSourceWithBytes:(const void *)bytes length:(NSInteger)length { + NSParameterAssert(bytes != nullptr); + NSParameterAssert(length >= 0); + + try { + SFBInputSource *inputSource = [[SFBInputSource alloc] init]; + if (inputSource) { + inputSource->_input = std::make_unique(bytes, length); + } + return inputSource; + } catch (const std::exception &e) { + return nil; + } +} + ++ (instancetype)inputSourceWithBytesNoCopy:(void *)bytes length:(NSInteger)length freeWhenDone:(BOOL)freeWhenDone { + NSParameterAssert(bytes != nullptr); + NSParameterAssert(length >= 0); + + try { + SFBInputSource *inputSource = [[SFBInputSource alloc] init]; + if (inputSource) { + inputSource->_input = + std::make_unique(bytes, length, + freeWhenDone ? sfb::BufferInput::BufferAdoption::noCopyAndFree + : sfb::BufferInput::BufferAdoption::noCopy); + } + return inputSource; + } catch (const std::exception &e) { + return nil; + } +} + +- (NSURL *)url { + return (__bridge NSURL *)_input->getURL(); +} + +- (BOOL)openReturningError:(NSError **)error { + try { + _input->open(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)closeReturningError:(NSError **)error { + try { + _input->close(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)isOpen { + return _input->isOpen(); +} + +- (BOOL)readBytes:(void *)buffer length:(NSInteger)length bytesRead:(NSInteger *)bytesRead error:(NSError **)error { + NSParameterAssert(bytesRead != nullptr); + + try { + *bytesRead = _input->read(buffer, length); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)atEOF { + try { + return _input->atEOF(); + } catch (const std::exception &e) { + // FIXME: Is `NO` the best error return? + return NO; + } +} + +- (BOOL)getOffset:(NSInteger *)offset error:(NSError **)error { + NSParameterAssert(offset != nullptr); + + try { + *offset = _input->position(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)getLength:(NSInteger *)length error:(NSError **)error { + NSParameterAssert(length != nullptr); + + try { + *length = _input->length(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)supportsSeeking { + try { + return _input->supportsSeeking(); + } catch (...) { + return NO; + } +} + +- (BOOL)seekToOffset:(NSInteger)offset error:(NSError **)error { + try { + _input->seekToOffset(offset); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (NSString *)description { + return (__bridge_transfer NSString *)_input->copyDescription(); +} + +@end + +@implementation SFBInputSource (SFBSignedIntegerReading) +- (BOOL)readInt8:(int8_t *)i8 error:(NSError **)error { + return [self readUInt8:(uint8_t *)i8 error:error]; +} +- (BOOL)readInt16:(int16_t *)i16 error:(NSError **)error { + return [self readUInt16:(uint16_t *)i16 error:error]; +} +- (BOOL)readInt32:(int32_t *)i32 error:(NSError **)error { + return [self readUInt32:(uint32_t *)i32 error:error]; +} +- (BOOL)readInt64:(int64_t *)i64 error:(NSError **)error { + return [self readUInt64:(uint64_t *)i64 error:error]; +} +@end + +@implementation SFBInputSource (SFBUnsignedIntegerReading) +- (BOOL)readUInt8:(uint8_t *)ui8 error:(NSError **)error { + NSParameterAssert(ui8 != nil); + try { + *ui8 = _input->readValue(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)readUInt16:(uint16_t *)ui16 error:(NSError **)error { + NSParameterAssert(ui16 != nil); + try { + *ui16 = _input->readUnsigned(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)readUInt32:(uint32_t *)ui32 error:(NSError **)error { + NSParameterAssert(ui32 != nil); + try { + *ui32 = _input->readUnsigned(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)readUInt64:(uint64_t *)ui64 error:(NSError **)error { + NSParameterAssert(ui64 != nil); + try { + *ui64 = _input->readUnsigned(); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +@end + +@implementation SFBInputSource (SFBBigEndianReading) + +- (BOOL)readUInt16BigEndian:(uint16_t *)ui16 error:(NSError **)error { + NSParameterAssert(ui16 != nil); + try { + *ui16 = _input->readUnsigned(sfb::InputSource::ByteOrder::big); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)readUInt32BigEndian:(uint32_t *)ui32 error:(NSError **)error { + NSParameterAssert(ui32 != nil); + try { + *ui32 = _input->readUnsigned(sfb::InputSource::ByteOrder::big); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)readUInt64BigEndian:(uint64_t *)ui64 error:(NSError **)error { + NSParameterAssert(ui64 != nil); + try { + *ui64 = _input->readUnsigned(sfb::InputSource::ByteOrder::big); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +@end + +@implementation SFBInputSource (SFBLittleEndianReading) + +- (BOOL)readUInt16LittleEndian:(uint16_t *)ui16 error:(NSError **)error { + NSParameterAssert(ui16 != nil); + try { + *ui16 = _input->readUnsigned(sfb::InputSource::ByteOrder::little); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)readUInt32LittleEndian:(uint32_t *)ui32 error:(NSError **)error { + NSParameterAssert(ui32 != nil); + try { + *ui32 = _input->readUnsigned(sfb::InputSource::ByteOrder::little); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +- (BOOL)readUInt64LittleEndian:(uint64_t *)ui64 error:(NSError **)error { + NSParameterAssert(ui64 != nil); + try { + *ui64 = _input->readUnsigned(sfb::InputSource::ByteOrder::little); + return YES; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return NO; + } +} + +@end + +@implementation SFBInputSource (SFBDataReading) + +- (NSData *)readDataOfLength:(NSUInteger)length error:(NSError **)error { + try { + return (__bridge_transfer NSData *)_input->copyData(length); + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return nil; + } +} + +@end + +@implementation SFBInputSource (SFBHeaderReading) + +- (NSData *)readHeaderOfLength:(NSUInteger)length skipID3v2Tag:(BOOL)skipID3v2Tag error:(NSError **)error { + NSParameterAssert(length > 0); + + if (!_input->supportsSeeking()) { + if (error) { + *error = [NSError errorWithDomain:SFBInputSourceErrorDomain + code:SFBInputSourceErrorCodeNotSeekable + userInfo:nil]; + } + return nil; + } + + try { + const auto originalOffset = _input->position(); + _input->seekToOffset(0); + + if (skipID3v2Tag) { + int64_t offset = 0; + + // Attempt to detect and minimally parse an ID3v2 tag header + NSData *data = (__bridge_transfer NSData *)_input->copyData(SFBID3v2HeaderSize); + if ([data isID3v2Header]) { + offset = [data id3v2TagTotalSize]; + } + + _input->seekToOffset(offset); + } + + NSData *data = (__bridge_transfer NSData *)_input->copyData(length); + if (data.length < length) { + if (error) { + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:@{NSURLErrorKey : self.url}]; + } + return nil; + } + + _input->seekToOffset(originalOffset); + + return data; + } catch (const std::exception &e) { + if (error) { + *error = NSErrorFromInputSourceException(&e); + } + return nil; + } +} + +@end diff --git a/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.h b/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.h deleted file mode 100644 index b026b01bc..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBDataInputSource.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface SFBMemoryMappedFileInputSource : SFBDataInputSource -+ (instancetype)new NS_UNAVAILABLE; -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithData:(NSData *)data url:(nullable NSURL *)url NS_UNAVAILABLE; -- (nullable instancetype)initWithURL:(NSURL *)url error:(NSError **)error NS_DESIGNATED_INITIALIZER; -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.m b/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.m deleted file mode 100644 index c8b837f0e..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.m +++ /dev/null @@ -1,24 +0,0 @@ -// -// SPDX-FileCopyrightText: 2010 Stephen F. Booth -// SPDX-License-Identifier: MIT -// -// Part of https://github.com/sbooth/SFBAudioEngine -// - -#import "SFBMemoryMappedFileInputSource.h" - -@implementation SFBMemoryMappedFileInputSource - -- (instancetype)initWithURL:(NSURL *)url error:(NSError **)error { - NSParameterAssert(url != nil); - NSParameterAssert(url.isFileURL); - - NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedAlways error:error]; - if (!data) { - return nil; - } - - return [super initWithData:data url:url]; -} - -@end diff --git a/Sources/CSFBAudioEngine/Input/scope_exit.hpp b/Sources/CSFBAudioEngine/Input/scope_exit.hpp new file mode 100644 index 000000000..b3f2f437e --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/scope_exit.hpp @@ -0,0 +1,31 @@ +// +// SPDX-FileCopyrightText: 2025 Stephen F. Booth +// SPDX-License-Identifier: MIT +// +// Part of https://github.com/sbooth/SFBAudioEngine +// + +#import + +namespace sfb { + +template + requires std::is_nothrow_invocable_v +class scope_exit final { + public: + explicit scope_exit(F &&f) noexcept(std::is_nothrow_constructible_v) : exit_func_(f) {} + ~scope_exit() noexcept { exit_func_(); } + + // This class is non-copyable. + scope_exit(const scope_exit &) = delete; + scope_exit(scope_exit &&) = delete; + + // This class is non-assignable. + scope_exit &operator=(const scope_exit &) = delete; + scope_exit &operator=(scope_exit &&) = delete; + + private: + F exit_func_; +}; + +} /* namespace sfb */ diff --git a/Tests/SFBAudioEngineTests/SFBAudioEngineTests.swift b/Tests/SFBAudioEngineTests/SFBAudioEngineTests.swift index 996ac3113..86017623f 100644 --- a/Tests/SFBAudioEngineTests/SFBAudioEngineTests.swift +++ b/Tests/SFBAudioEngineTests/SFBAudioEngineTests.swift @@ -9,17 +9,6 @@ import XCTest @testable import SFBAudioEngine final class SFBAudioEngineTests: XCTestCase { - func testInputSourceFromData() throws { - let input = InputSource(data: Data(repeating: 0xfe, count: 16)) - XCTAssertEqual(input.isOpen, true) - XCTAssertEqual(input.supportsSeeking, true) - XCTAssertEqual(try input.offset, 0) - let i: UInt8 = try input.read() - XCTAssertEqual(i, 0xfe) - XCTAssertEqual(try input.offset, 1) - XCTAssertEqual(try input.length, 16) - } - func testOutputTargetFromData() throws { let output = OutputTarget.makeForData() XCTAssertEqual(output.isOpen, true)