diff --git a/Sources/CSFBAudioEngine/Input/BufferInput.cpp b/Sources/CSFBAudioEngine/Input/BufferInput.cpp new file mode 100644 index 000000000..86164c194 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/BufferInput.cpp @@ -0,0 +1,47 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#import +#import +#import + +#import "BufferInput.hpp" + +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(sLog, "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..dfa3c6733 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/BufferInput.hpp @@ -0,0 +1,54 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#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..68d4c53c4 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/DataInput.cpp @@ -0,0 +1,43 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#import +#import + +#import "DataInput.hpp" + +SFB::DataInput::DataInput(CFDataRef data) +{ + if(!data) { + os_log_error(sLog, "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(sLog, "_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..c7ac764a5 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/DataInput.hpp @@ -0,0 +1,43 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#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..ad6031808 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileContentsInput.cpp @@ -0,0 +1,69 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#import +#import +#import + +#import + +#import "FileContentsInput.hpp" +#import "scope_exit.hpp" + +SFB::FileContentsInput::FileContentsInput(CFURLRef url) +{ + if(!url) { + os_log_error(sLog, "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..e4ac9e50f --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileContentsInput.hpp @@ -0,0 +1,33 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#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..ba077f9b0 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileInput.cpp @@ -0,0 +1,84 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#import + +#import "FileInput.hpp" +#import "scope_exit.hpp" + +SFB::FileInput::FileInput(CFURLRef url) +{ + if(!url) { + os_log_error(sLog, "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..7e6da83d3 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/FileInput.hpp @@ -0,0 +1,47 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#pragma once + +#import +#import + +#import "InputSource.hpp" + +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..82d8407db --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/InputSource.cpp @@ -0,0 +1,218 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#import +#import + +#import "InputSource.hpp" +#import "scope_exit.hpp" + +#import "BufferInput.hpp" +#import "DataInput.hpp" +#import "FileContentsInput.hpp" +#import "FileInput.hpp" +#import "MemoryMappedFileInput.hpp" + +namespace SFB { + +const os_log_t InputSource::sLog = 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(sLog, "Open() called on that is already open", this); + return; + } + + _Open(); + isOpen_ = true; +} + +void SFB::InputSource::Close() +{ + if(!IsOpen()) { + os_log_debug(sLog, "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(sLog, "Read() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + if(!buffer || count < 0) { + os_log_error(sLog, "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(sLog, "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(sLog, "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(sLog, "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(sLog, "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(sLog, "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(sLog, "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(sLog, "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(sLog, "SeekToOffset() called on that hasn't been opened", this); + throw std::logic_error("Input source not open"); + } + + if(!_SupportsSeeking()) { + os_log_error(sLog, "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(sLog, "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..0b8d0a836 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/InputSource.hpp @@ -0,0 +1,202 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#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. + 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. + 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. + CFDataRef _Nullable CopyData(int64_t count); + + /// Reads and returns up to `count` bytes from the input source in a `std::vector` object. + std::vector ReadBlock(std::vector::size_type count); + + // MARK: Position + + /// Returns `true` if the input source is at the end of input. + bool AtEOF() const; + + /// Returns the current read position of the input source in bytes. + int64_t Position() const; + + /// Returns the number of bytes in the input source. + int64_t Length() const; + + // MARK: Seeking + + /// Returns `true` if the input source is seekable. + 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>> + V ReadValue() + { + if(!IsOpen()) { + os_log_error(sLog, "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>> + U ReadUnsigned(ByteOrder order = ByteOrder::host) + { + if(!IsOpen()) { + os_log_error(sLog, "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>> + S ReadSigned(ByteOrder order = ByteOrder::host) { return std::make_signed(ReadUnsigned>(order)); } + + // MARK: Debugging + + /// Returns a description of the input source. + CFStringRef _Nonnull CopyDescription() const noexcept; + +protected: + /// The shared log for all `InputSource` instances. + static const os_log_t _Nonnull sLog; + + 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..4489bd046 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.cpp @@ -0,0 +1,79 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#import +#import +#import + +#import +#import + +#import "MemoryMappedFileInput.hpp" +#import "scope_exit.hpp" + +SFB::MemoryMappedFileInput::MemoryMappedFileInput(CFURLRef url) +{ + if(!url) { + os_log_error(sLog, "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..40b5608d7 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/MemoryMappedFileInput.hpp @@ -0,0 +1,33 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#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 3b2708799..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBDataInputSource.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#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 a62e3f31a..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBDataInputSource.m +++ /dev/null @@ -1,104 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#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 = [NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:@{ NSURLErrorKey: self.url }]; - 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 8c67fc299..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#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 51c32e230..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileContentsInputSource.m +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#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 == nil) - 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 7d52bda14..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileInputSource.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#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 ae4c2d706..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBFileInputSource.m +++ /dev/null @@ -1,146 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#import -#import - -#import "SFBFileInputSource.h" -#import "SFBInputSource+Internal.h" - -@interface SFBFileInputSource () -{ -@private - struct stat _filestats; - FILE *_file; -} -@end - -@implementation SFBFileInputSource - -#if 0 -- (instancetype)initWithURL:(NSURL *)url -{ - NSParameterAssert(url != nil); - NSParameterAssert(url.isFileURL); - - return [super initWithURL:url]; -} -#endif - -- (BOOL)openReturningError:(NSError **)error -{ - _file = fopen(self.url.fileSystemRepresentation, "r"); - if(!_file) { - int err = errno; - os_log_error(gSFBInputSourceLog, "fopen failed: %{public}s (%d)", strerror(err), err); - if(error) - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:@{ NSURLErrorKey: self.url }]; - 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 = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:@{ NSURLErrorKey: self.url }]; - - 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 = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:@{ NSURLErrorKey: self.url }]; - 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 = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:@{ NSURLErrorKey: self.url }]; - 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 = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:@{ NSURLErrorKey: self.url }]; - 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 = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:@{ NSURLErrorKey: self.url }]; - return NO; - } - return YES; -} - -@end diff --git a/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h b/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h index 20a52f68b..98cbe2215 100644 --- a/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h +++ b/Sources/CSFBAudioEngine/Input/SFBInputSource+Internal.h @@ -4,20 +4,16 @@ // MIT license // -#import - #import "SFBInputSource.h" +#import "InputSource.hpp" 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; @end NS_ASSUME_NONNULL_END diff --git a/Sources/CSFBAudioEngine/Input/SFBInputSource.m b/Sources/CSFBAudioEngine/Input/SFBInputSource.m deleted file mode 100644 index 8338d99d3..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBInputSource.m +++ /dev/null @@ -1,336 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#import "SFBInputSource+Internal.h" - -#import "SFBDataInputSource.h" -#import "SFBFileContentsInputSource.h" -#import "SFBFileInputSource.h" -#import "SFBMemoryMappedFileInputSource.h" - -#import "NSData+SFBExtensions.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) -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - gSFBInputSourceLog = os_log_create("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); - - if(flags & SFBInputSourceFlagsMemoryMapFiles) - return [[SFBMemoryMappedFileInputSource alloc] initWithURL:url error:error]; - else if(flags & SFBInputSourceFlagsLoadFilesInMemory) - return [[SFBFileContentsInputSource alloc] initWithContentsOfURL:url error:error]; - else - return [[SFBFileInputSource alloc] initWithURL:url]; - - if(error) - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:@{ NSURLErrorKey: url }]; - return nil; -} - -+ (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 == nil) - 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 == nil) - 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(); -} - -- (NSString *)description -{ - if(_url) - return [NSString stringWithFormat:@"<%@ %p: \"%@\">", [self class], self, [[NSFileManager defaultManager] displayNameAtPath:_url.path]]; - else - return [NSString stringWithFormat:@"<%@ %p>", [self class], 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) - *error = [NSError errorWithDomain:SFBInputSourceErrorDomain code:SFBInputSourceErrorCodeNotSeekable userInfo:nil]; - 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 = [NSError errorWithDomain:NSPOSIXErrorDomain code:EINVAL userInfo:@{ NSURLErrorKey: self.url }]; - 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..3bdbb8863 --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/SFBInputSource.mm @@ -0,0 +1,456 @@ +// +// Copyright (c) 2010-2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#import + +#import "SFBInputSource+Internal.h" + +#import "BufferInput.hpp" +#import "DataInput.hpp" +#import "FileContentsInput.hpp" +#import "FileInput.hpp" +#import "MemoryMappedFileInput.hpp" + +#import "NSData+SFBExtensions.h" + +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 40d2ea087..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#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 e4b06a0ac..000000000 --- a/Sources/CSFBAudioEngine/Input/SFBMemoryMappedFileInputSource.m +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (c) 2010-2025 Stephen F. Booth -// Part of https://github.com/sbooth/SFBAudioEngine -// MIT license -// - -#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 == nil) - 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..71c9ec49e --- /dev/null +++ b/Sources/CSFBAudioEngine/Input/scope_exit.hpp @@ -0,0 +1,29 @@ +// +// Copyright (c) 2025 Stephen F. Booth +// Part of https://github.com/sbooth/SFBAudioEngine +// MIT license +// + +#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 ccd6b678c..73b0e1f4f 100644 --- a/Tests/SFBAudioEngineTests/SFBAudioEngineTests.swift +++ b/Tests/SFBAudioEngineTests/SFBAudioEngineTests.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2012-2024 Stephen F. Booth +// Copyright (c) 2012-2025 Stephen F. Booth // Part of https://github.com/sbooth/SFBAudioEngine // MIT license // @@ -10,6 +10,8 @@ import XCTest final class SFBAudioEngineTests: XCTestCase { func testInputSourceFromData() throws { let input = InputSource(data: Data(repeating: 0xfe, count: 16)) + XCTAssertEqual(input.isOpen, false) + try input.open() XCTAssertEqual(input.isOpen, true) XCTAssertEqual(input.supportsSeeking, true) XCTAssertEqual(try input.offset, 0)