Restructured the source code in the Playdate bindings target.
This commit is contained in:
@@ -3,11 +3,15 @@ PRODUCT := HelloPlaydate.pdx
|
|||||||
include $(REPO_ROOT)/Examples/swift.mk
|
include $(REPO_ROOT)/Examples/swift.mk
|
||||||
|
|
||||||
# MARK: - Build PlaydateKit Wrapper Swift Module
|
# MARK: - Build PlaydateKit Wrapper Swift Module
|
||||||
build/Modules/playdate_device.o: $(REPO_ROOT)/Sources/PlaydateKit/*.swift
|
# The wrapper sources are organized in nested domain folders, so collect
|
||||||
|
# them recursively (a plain *.swift glob would miss the subdirectories).
|
||||||
|
PLAYDATEKIT_SOURCES := $(shell find $(REPO_ROOT)/Sources/PlaydateKit -name '*.swift')
|
||||||
|
|
||||||
|
build/Modules/playdate_device.o: $(PLAYDATEKIT_SOURCES)
|
||||||
@mkdir -p build/Modules
|
@mkdir -p build/Modules
|
||||||
$(SWIFT_EXEC) $(SWIFT_FLAGS) $(SWIFT_FLAGS_DEVICE) -c $^ -emit-module -o $@
|
$(SWIFT_EXEC) $(SWIFT_FLAGS) $(SWIFT_FLAGS_DEVICE) -c $^ -emit-module -o $@
|
||||||
|
|
||||||
build/Modules/playdate_simulator.o: $(REPO_ROOT)/Sources/PlaydateKit/*.swift
|
build/Modules/playdate_simulator.o: $(PLAYDATEKIT_SOURCES)
|
||||||
@mkdir -p build/Modules
|
@mkdir -p build/Modules
|
||||||
$(SWIFT_EXEC) $(SWIFT_FLAGS) $(SWIFT_FLAGS_SIMULATOR) -c $^ -emit-module -o $@
|
$(SWIFT_EXEC) $(SWIFT_FLAGS) $(SWIFT_FLAGS_SIMULATOR) -c $^ -emit-module -o $@
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,8 +1,3 @@
|
|||||||
//
|
|
||||||
// Display.swift
|
|
||||||
// Wraps `playdate->display` (pd_api_display.h).
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
/// The display API: resolution, refresh rate, scaling, and effects.
|
/// The display API: resolution, refresh rate, scaling, and effects.
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
//
|
|
||||||
// File.swift
|
|
||||||
// Wraps `playdate->file` (pd_api_file.h).
|
|
||||||
//
|
|
||||||
// Paths are relative to the game's Data directory (read/write) or the
|
|
||||||
// game's pdx (read-only), per the mode used to open them.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
|
||||||
|
|
||||||
private var fileAPI: UnsafePointer<playdate_file> { Playdate.fileAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The most recent file system error as a thrown error.
|
|
||||||
private func lastFileError() -> PlaydateError {
|
|
||||||
PlaydateError(cString: fileAPI.pointee.geterr.unsafelyUnwrapped())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The file API: access to the game's Data directory and pdx contents.
|
|
||||||
public enum File {}
|
|
||||||
|
|
||||||
extension File {
|
|
||||||
// MARK: - Types
|
|
||||||
|
|
||||||
/// How to open a file.
|
|
||||||
public struct Options: OptionSet, Sendable {
|
|
||||||
public let rawValue: UInt32
|
|
||||||
public init(rawValue: UInt32) { self.rawValue = rawValue }
|
|
||||||
|
|
||||||
/// Read from the game pdx, then the Data directory.
|
|
||||||
public static let read = Options(rawValue: UInt32(kFileRead.rawValue))
|
|
||||||
/// Read from the Data directory only.
|
|
||||||
public static let readData = Options(rawValue: UInt32(kFileReadData.rawValue))
|
|
||||||
/// Write to the Data directory, truncating an existing file.
|
|
||||||
public static let write = Options(rawValue: UInt32(kFileWrite.rawValue))
|
|
||||||
/// Write to the Data directory, appending to an existing file.
|
|
||||||
public static let append = Options(rawValue: UInt32(kFileAppend.rawValue))
|
|
||||||
|
|
||||||
var cValue: FileOptions { FileOptions(FileOptions.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about a file or directory, mirroring `FileStat`.
|
|
||||||
public struct Stat: Sendable {
|
|
||||||
public let isDirectory: Bool
|
|
||||||
public let size: UInt32
|
|
||||||
public let modified: System.DateTime
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The origin used by `Handle.seek(to:from:)`.
|
|
||||||
public enum SeekOrigin: Int32, Sendable {
|
|
||||||
case start = 0
|
|
||||||
case current = 1
|
|
||||||
case end = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Directory operations
|
|
||||||
|
|
||||||
/// Calls `each` with the name of every file in `path`. Subdirectory names
|
|
||||||
/// end in a slash. Throws if the directory does not exist.
|
|
||||||
public static func listFiles(at path: String, showHidden: Bool = false,
|
|
||||||
_ each: (String) -> Void) throws(PlaydateError) {
|
|
||||||
let result = withoutActuallyEscaping(each) { each in
|
|
||||||
var callback = each
|
|
||||||
return path.withPlaydateCString { cPath in
|
|
||||||
withUnsafeMutablePointer(to: &callback) { callbackPointer in
|
|
||||||
fileAPI.pointee.listfiles.unsafelyUnwrapped(cPath, { cName, userdata in
|
|
||||||
guard let cName, let userdata else { return }
|
|
||||||
let each = userdata.assumingMemoryBound(to: ((String) -> Void).self).pointee
|
|
||||||
each(String(playdateCString: cName))
|
|
||||||
}, callbackPointer, showHidden ? 1 : 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if result != 0 { throw lastFileError() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about the file or directory at `path`.
|
|
||||||
public static func stat(_ path: String) throws(PlaydateError) -> Stat {
|
|
||||||
var stat = FileStat()
|
|
||||||
let result = path.withPlaydateCString { fileAPI.pointee.stat.unsafelyUnwrapped($0, &stat) }
|
|
||||||
if result != 0 { throw lastFileError() }
|
|
||||||
return Stat(
|
|
||||||
isDirectory: stat.isdir != 0,
|
|
||||||
size: stat.size,
|
|
||||||
modified: System.DateTime(
|
|
||||||
year: UInt16(stat.m_year), month: UInt8(stat.m_month), day: UInt8(stat.m_day),
|
|
||||||
hour: UInt8(stat.m_hour), minute: UInt8(stat.m_minute), second: UInt8(stat.m_second)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a directory (and intermediate directories) in the Data directory.
|
|
||||||
public static func mkdir(_ path: String) throws(PlaydateError) {
|
|
||||||
let result = path.withPlaydateCString { fileAPI.pointee.mkdir.unsafelyUnwrapped($0) }
|
|
||||||
if result != 0 { throw lastFileError() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deletes the file or directory at `path`. Directories require
|
|
||||||
/// `recursive` to be deleted with their contents.
|
|
||||||
public static func unlink(_ path: String, recursive: Bool = false) throws(PlaydateError) {
|
|
||||||
let result = path.withPlaydateCString {
|
|
||||||
fileAPI.pointee.unlink.unsafelyUnwrapped($0, recursive ? 1 : 0)
|
|
||||||
}
|
|
||||||
if result != 0 { throw lastFileError() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renames (moves) a file in the Data directory, overwriting any existing
|
|
||||||
/// file at the destination.
|
|
||||||
public static func rename(from: String, to: String) throws(PlaydateError) {
|
|
||||||
let result = from.withPlaydateCString { cFrom in
|
|
||||||
to.withPlaydateCString { cTo in
|
|
||||||
fileAPI.pointee.rename.unsafelyUnwrapped(cFrom, cTo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if result != 0 { throw lastFileError() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Open files
|
|
||||||
|
|
||||||
/// An open file. Wraps `SDFile`. The file is closed on deinit if it has
|
|
||||||
/// not been closed explicitly.
|
|
||||||
public final class Handle {
|
|
||||||
let pointer: UnsafeMutableRawPointer
|
|
||||||
private var isClosed = false
|
|
||||||
|
|
||||||
/// Opens the file at `path`.
|
|
||||||
public init(path: String, mode: Options) throws(PlaydateError) {
|
|
||||||
let pointer = path.withPlaydateCString {
|
|
||||||
fileAPI.pointee.open.unsafelyUnwrapped($0, mode.cValue)
|
|
||||||
}
|
|
||||||
guard let pointer else { throw lastFileError() }
|
|
||||||
self.pointer = pointer
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if !isClosed {
|
|
||||||
_ = fileAPI.pointee.close.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Closes the file. Further operations are invalid.
|
|
||||||
public func close() throws(PlaydateError) {
|
|
||||||
guard !isClosed else { return }
|
|
||||||
isClosed = true
|
|
||||||
if fileAPI.pointee.close.unsafelyUnwrapped(pointer) != 0 { throw lastFileError() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads up to `buffer.count` bytes into `buffer`. Returns the number
|
|
||||||
/// of bytes read; 0 indicates end of file.
|
|
||||||
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(PlaydateError) -> Int {
|
|
||||||
let result = fileAPI.pointee.read.unsafelyUnwrapped(
|
|
||||||
pointer, buffer.baseAddress, UInt32(buffer.count))
|
|
||||||
if result < 0 { throw lastFileError() }
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads up to `length` bytes and returns them.
|
|
||||||
public func read(length: Int) throws(PlaydateError) -> [UInt8] {
|
|
||||||
var bytes = [UInt8](repeating: 0, count: length)
|
|
||||||
let result = bytes.withUnsafeMutableBytes { buffer in
|
|
||||||
fileAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
|
||||||
}
|
|
||||||
if result < 0 { throw lastFileError() }
|
|
||||||
bytes.removeLast(length - Int(result))
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes the buffer to the file. Returns the number of bytes written.
|
|
||||||
@discardableResult
|
|
||||||
public func write(_ buffer: UnsafeRawBufferPointer) throws(PlaydateError) -> Int {
|
|
||||||
let result = fileAPI.pointee.write.unsafelyUnwrapped(
|
|
||||||
pointer, buffer.baseAddress, UInt32(buffer.count))
|
|
||||||
if result < 0 { throw lastFileError() }
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes the bytes to the file. Returns the number of bytes written.
|
|
||||||
@discardableResult
|
|
||||||
public func write(_ bytes: [UInt8]) throws(PlaydateError) -> Int {
|
|
||||||
let result = bytes.withUnsafeBytes { buffer in
|
|
||||||
fileAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
|
||||||
}
|
|
||||||
if result < 0 { throw lastFileError() }
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes the string's UTF-8 to the file. Returns the bytes written.
|
|
||||||
@discardableResult
|
|
||||||
public func write(_ string: String) throws(PlaydateError) -> Int {
|
|
||||||
let result = string.withPlaydateUTF8 { bytes, count in
|
|
||||||
fileAPI.pointee.write.unsafelyUnwrapped(pointer, bytes, UInt32(count))
|
|
||||||
}
|
|
||||||
if result < 0 { throw lastFileError() }
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flushes buffered writes to disk. Returns the bytes written.
|
|
||||||
@discardableResult
|
|
||||||
public func flush() throws(PlaydateError) -> Int {
|
|
||||||
let result = fileAPI.pointee.flush.unsafelyUnwrapped(pointer)
|
|
||||||
if result < 0 { throw lastFileError() }
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current read/write offset.
|
|
||||||
public func tell() throws(PlaydateError) -> Int {
|
|
||||||
let result = fileAPI.pointee.tell.unsafelyUnwrapped(pointer)
|
|
||||||
if result < 0 { throw lastFileError() }
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the read/write offset to `offset` relative to `origin`.
|
|
||||||
public func seek(to offset: Int, from origin: SeekOrigin = .start) throws(PlaydateError) {
|
|
||||||
if fileAPI.pointee.seek.unsafelyUnwrapped(pointer, Int32(offset), origin.rawValue) != 0 {
|
|
||||||
throw lastFileError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension File {
|
||||||
|
/// An open file. Wraps `SDFile`. The file is closed on deinit if it has
|
||||||
|
/// not been closed explicitly.
|
||||||
|
public final class Handle {
|
||||||
|
let pointer: UnsafeMutableRawPointer
|
||||||
|
private var isClosed = false
|
||||||
|
|
||||||
|
/// Opens the file at `path`.
|
||||||
|
public init(path: String, mode: Options) throws(PlaydateError) {
|
||||||
|
let pointer = path.withPlaydateCString {
|
||||||
|
fileAPI.pointee.open.unsafelyUnwrapped($0, mode.cValue)
|
||||||
|
}
|
||||||
|
guard let pointer else { throw lastFileError() }
|
||||||
|
self.pointer = pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if !isClosed {
|
||||||
|
_ = fileAPI.pointee.close.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the file. Further operations are invalid.
|
||||||
|
public func close() throws(PlaydateError) {
|
||||||
|
guard !isClosed else { return }
|
||||||
|
isClosed = true
|
||||||
|
if fileAPI.pointee.close.unsafelyUnwrapped(pointer) != 0 { throw lastFileError() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads up to `buffer.count` bytes into `buffer`. Returns the number
|
||||||
|
/// of bytes read; 0 indicates end of file.
|
||||||
|
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(PlaydateError) -> Int {
|
||||||
|
let result = fileAPI.pointee.read.unsafelyUnwrapped(
|
||||||
|
pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||||
|
if result < 0 { throw lastFileError() }
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads up to `length` bytes and returns them.
|
||||||
|
public func read(length: Int) throws(PlaydateError) -> [UInt8] {
|
||||||
|
var bytes = [UInt8](repeating: 0, count: length)
|
||||||
|
let result = bytes.withUnsafeMutableBytes { buffer in
|
||||||
|
fileAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||||
|
}
|
||||||
|
if result < 0 { throw lastFileError() }
|
||||||
|
bytes.removeLast(length - Int(result))
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the buffer to the file. Returns the number of bytes written.
|
||||||
|
@discardableResult
|
||||||
|
public func write(_ buffer: UnsafeRawBufferPointer) throws(PlaydateError) -> Int {
|
||||||
|
let result = fileAPI.pointee.write.unsafelyUnwrapped(
|
||||||
|
pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||||
|
if result < 0 { throw lastFileError() }
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the bytes to the file. Returns the number of bytes written.
|
||||||
|
@discardableResult
|
||||||
|
public func write(_ bytes: [UInt8]) throws(PlaydateError) -> Int {
|
||||||
|
let result = bytes.withUnsafeBytes { buffer in
|
||||||
|
fileAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||||
|
}
|
||||||
|
if result < 0 { throw lastFileError() }
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the string's UTF-8 to the file. Returns the bytes written.
|
||||||
|
@discardableResult
|
||||||
|
public func write(_ string: String) throws(PlaydateError) -> Int {
|
||||||
|
let result = string.withPlaydateUTF8 { bytes, count in
|
||||||
|
fileAPI.pointee.write.unsafelyUnwrapped(pointer, bytes, UInt32(count))
|
||||||
|
}
|
||||||
|
if result < 0 { throw lastFileError() }
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes buffered writes to disk. Returns the bytes written.
|
||||||
|
@discardableResult
|
||||||
|
public func flush() throws(PlaydateError) -> Int {
|
||||||
|
let result = fileAPI.pointee.flush.unsafelyUnwrapped(pointer)
|
||||||
|
if result < 0 { throw lastFileError() }
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current read/write offset.
|
||||||
|
public func tell() throws(PlaydateError) -> Int {
|
||||||
|
let result = fileAPI.pointee.tell.unsafelyUnwrapped(pointer)
|
||||||
|
if result < 0 { throw lastFileError() }
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the read/write offset to `offset` relative to `origin`.
|
||||||
|
public func seek(to offset: Int, from origin: SeekOrigin = .start) throws(PlaydateError) {
|
||||||
|
if fileAPI.pointee.seek.unsafelyUnwrapped(pointer, Int32(offset), origin.rawValue) != 0 {
|
||||||
|
throw lastFileError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extension File {
|
||||||
|
/// The origin used by `Handle.seek(to:from:)`.
|
||||||
|
public enum SeekOrigin: Int32, Sendable {
|
||||||
|
case start = 0
|
||||||
|
case current = 1
|
||||||
|
case end = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
var fileAPI: UnsafePointer<playdate_file> { Playdate.fileAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
/// The most recent file system error as a thrown error.
|
||||||
|
func lastFileError() -> PlaydateError {
|
||||||
|
PlaydateError(cString: fileAPI.pointee.geterr.unsafelyUnwrapped())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file API: access to the game's Data directory and pdx contents.
|
||||||
|
public enum File {}
|
||||||
|
|
||||||
|
extension File {
|
||||||
|
// MARK: - Directory operations
|
||||||
|
|
||||||
|
/// Calls `each` with the name of every file in `path`. Subdirectory names
|
||||||
|
/// end in a slash. Throws if the directory does not exist.
|
||||||
|
public static func listFiles(at path: String, showHidden: Bool = false,
|
||||||
|
_ each: (String) -> Void) throws(PlaydateError) {
|
||||||
|
let result = withoutActuallyEscaping(each) { each in
|
||||||
|
var callback = each
|
||||||
|
return path.withPlaydateCString { cPath in
|
||||||
|
withUnsafeMutablePointer(to: &callback) { callbackPointer in
|
||||||
|
fileAPI.pointee.listfiles.unsafelyUnwrapped(cPath, { cName, userdata in
|
||||||
|
guard let cName, let userdata else { return }
|
||||||
|
let each = userdata.assumingMemoryBound(to: ((String) -> Void).self).pointee
|
||||||
|
each(String(playdateCString: cName))
|
||||||
|
}, callbackPointer, showHidden ? 1 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result != 0 { throw lastFileError() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about the file or directory at `path`.
|
||||||
|
public static func stat(_ path: String) throws(PlaydateError) -> Stat {
|
||||||
|
var stat = FileStat()
|
||||||
|
let result = path.withPlaydateCString { fileAPI.pointee.stat.unsafelyUnwrapped($0, &stat) }
|
||||||
|
if result != 0 { throw lastFileError() }
|
||||||
|
return Stat(
|
||||||
|
isDirectory: stat.isdir != 0,
|
||||||
|
size: stat.size,
|
||||||
|
modified: System.DateTime(
|
||||||
|
year: UInt16(stat.m_year), month: UInt8(stat.m_month), day: UInt8(stat.m_day),
|
||||||
|
hour: UInt8(stat.m_hour), minute: UInt8(stat.m_minute), second: UInt8(stat.m_second)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a directory (and intermediate directories) in the Data directory.
|
||||||
|
public static func mkdir(_ path: String) throws(PlaydateError) {
|
||||||
|
let result = path.withPlaydateCString { fileAPI.pointee.mkdir.unsafelyUnwrapped($0) }
|
||||||
|
if result != 0 { throw lastFileError() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes the file or directory at `path`. Directories require
|
||||||
|
/// `recursive` to be deleted with their contents.
|
||||||
|
public static func unlink(_ path: String, recursive: Bool = false) throws(PlaydateError) {
|
||||||
|
let result = path.withPlaydateCString {
|
||||||
|
fileAPI.pointee.unlink.unsafelyUnwrapped($0, recursive ? 1 : 0)
|
||||||
|
}
|
||||||
|
if result != 0 { throw lastFileError() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renames (moves) a file in the Data directory, overwriting any existing
|
||||||
|
/// file at the destination.
|
||||||
|
public static func rename(from: String, to: String) throws(PlaydateError) {
|
||||||
|
let result = from.withPlaydateCString { cFrom in
|
||||||
|
to.withPlaydateCString { cTo in
|
||||||
|
fileAPI.pointee.rename.unsafelyUnwrapped(cFrom, cTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result != 0 { throw lastFileError() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension File {
|
||||||
|
/// How to open a file.
|
||||||
|
public struct Options: OptionSet, Sendable {
|
||||||
|
public let rawValue: UInt32
|
||||||
|
public init(rawValue: UInt32) { self.rawValue = rawValue }
|
||||||
|
|
||||||
|
/// Read from the game pdx, then the Data directory.
|
||||||
|
public static let read = Options(rawValue: UInt32(kFileRead.rawValue))
|
||||||
|
/// Read from the Data directory only.
|
||||||
|
public static let readData = Options(rawValue: UInt32(kFileReadData.rawValue))
|
||||||
|
/// Write to the Data directory, truncating an existing file.
|
||||||
|
public static let write = Options(rawValue: UInt32(kFileWrite.rawValue))
|
||||||
|
/// Write to the Data directory, appending to an existing file.
|
||||||
|
public static let append = Options(rawValue: UInt32(kFileAppend.rawValue))
|
||||||
|
|
||||||
|
var cValue: FileOptions { FileOptions(FileOptions.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extension File {
|
||||||
|
/// Information about a file or directory, mirroring `FileStat`.
|
||||||
|
public struct Stat: Sendable {
|
||||||
|
public let isDirectory: Bool
|
||||||
|
public let size: UInt32
|
||||||
|
public let modified: System.DateTime
|
||||||
|
}
|
||||||
|
}
|
||||||
-69
@@ -1,8 +1,3 @@
|
|||||||
//
|
|
||||||
// GraphicsBitmap.swift
|
|
||||||
// Bitmap and BitmapTable wrappers around LCDBitmap / LCDBitmapTable.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
extension Graphics {
|
extension Graphics {
|
||||||
@@ -44,16 +39,6 @@ extension Graphics {
|
|||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
/// The bitmap's dimensions, row stride, and raw pixel/mask storage.
|
|
||||||
/// The pointers are owned by the bitmap.
|
|
||||||
public struct Data {
|
|
||||||
public let width: Int
|
|
||||||
public let height: Int
|
|
||||||
public let rowBytes: Int
|
|
||||||
public let mask: UnsafeMutablePointer<UInt8>?
|
|
||||||
public let data: UnsafeMutablePointer<UInt8>?
|
|
||||||
}
|
|
||||||
|
|
||||||
public var data: Data {
|
public var data: Data {
|
||||||
var width: Int32 = 0, height: Int32 = 0, rowBytes: Int32 = 0
|
var width: Int32 = 0, height: Int32 = 0, rowBytes: Int32 = 0
|
||||||
var mask: UnsafeMutablePointer<UInt8>?
|
var mask: UnsafeMutablePointer<UInt8>?
|
||||||
@@ -164,58 +149,4 @@ extension Graphics {
|
|||||||
Int32(width), Int32(height), flip.cValue)
|
Int32(width), Int32(height), flip.cValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collection of bitmaps loaded from an image table. Wraps `LCDBitmapTable`.
|
|
||||||
public final class BitmapTable {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer) {
|
|
||||||
self.pointer = pointer
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocates a table with room for `count` bitmaps of the given size.
|
|
||||||
public convenience init(count: Int, width: Int, height: Int) {
|
|
||||||
let pointer = gfx.pointee.newBitmapTable.unsafelyUnwrapped(Int32(count), Int32(width), Int32(height))
|
|
||||||
self.init(pointer: pointer.unsafelyUnwrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads an image table from a file.
|
|
||||||
public convenience init(path: String) throws(PlaydateError) {
|
|
||||||
var error: UnsafePointer<CChar>?
|
|
||||||
let pointer = path.withPlaydateCString { gfx.pointee.loadBitmapTable.unsafelyUnwrapped($0, &error) }
|
|
||||||
guard let pointer else { throw PlaydateError(cString: error) }
|
|
||||||
self.init(pointer: pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
gfx.pointee.freeBitmapTable.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces the table's contents with the image table at `path`.
|
|
||||||
public func load(path: String) throws(PlaydateError) {
|
|
||||||
var error: UnsafePointer<CChar>?
|
|
||||||
path.withPlaydateCString { gfx.pointee.loadIntoBitmapTable.unsafelyUnwrapped($0, pointer, &error) }
|
|
||||||
if let error { throw PlaydateError(cString: error) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bitmap at `index`, or `nil` if out of range. The bitmap
|
|
||||||
/// references storage owned by the table; keep the table alive while
|
|
||||||
/// using it.
|
|
||||||
public func bitmap(at index: Int) -> Bitmap? {
|
|
||||||
guard let bitmap = gfx.pointee.getTableBitmap.unsafelyUnwrapped(pointer, Int32(index)) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Bitmap(pointer: bitmap, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of bitmaps in the table and the number of cells per row
|
|
||||||
/// of the source image.
|
|
||||||
public var info: (count: Int, cellsWide: Int) {
|
|
||||||
var count: Int32 = 0, width: Int32 = 0
|
|
||||||
gfx.pointee.getBitmapTableInfo.unsafelyUnwrapped(pointer, &count, &width)
|
|
||||||
return (Int(count), Int(width))
|
|
||||||
}
|
|
||||||
|
|
||||||
public var count: Int { info.count }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// A collection of bitmaps loaded from an image table. Wraps `LCDBitmapTable`.
|
||||||
|
public final class BitmapTable {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer) {
|
||||||
|
self.pointer = pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocates a table with room for `count` bitmaps of the given size.
|
||||||
|
public convenience init(count: Int, width: Int, height: Int) {
|
||||||
|
let pointer = gfx.pointee.newBitmapTable.unsafelyUnwrapped(Int32(count), Int32(width), Int32(height))
|
||||||
|
self.init(pointer: pointer.unsafelyUnwrapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads an image table from a file.
|
||||||
|
public convenience init(path: String) throws(PlaydateError) {
|
||||||
|
var error: UnsafePointer<CChar>?
|
||||||
|
let pointer = path.withPlaydateCString { gfx.pointee.loadBitmapTable.unsafelyUnwrapped($0, &error) }
|
||||||
|
guard let pointer else { throw PlaydateError(cString: error) }
|
||||||
|
self.init(pointer: pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
gfx.pointee.freeBitmapTable.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the table's contents with the image table at `path`.
|
||||||
|
public func load(path: String) throws(PlaydateError) {
|
||||||
|
var error: UnsafePointer<CChar>?
|
||||||
|
path.withPlaydateCString { gfx.pointee.loadIntoBitmapTable.unsafelyUnwrapped($0, pointer, &error) }
|
||||||
|
if let error { throw PlaydateError(cString: error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bitmap at `index`, or `nil` if out of range. The bitmap
|
||||||
|
/// references storage owned by the table; keep the table alive while
|
||||||
|
/// using it.
|
||||||
|
public func bitmap(at index: Int) -> Bitmap? {
|
||||||
|
guard let bitmap = gfx.pointee.getTableBitmap.unsafelyUnwrapped(pointer, Int32(index)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Bitmap(pointer: bitmap, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of bitmaps in the table and the number of cells per row
|
||||||
|
/// of the source image.
|
||||||
|
public var info: (count: Int, cellsWide: Int) {
|
||||||
|
var count: Int32 = 0, width: Int32 = 0
|
||||||
|
gfx.pointee.getBitmapTableInfo.unsafelyUnwrapped(pointer, &count, &width)
|
||||||
|
return (Int(count), Int(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var count: Int { info.count }
|
||||||
|
}
|
||||||
|
}
|
||||||
-36
@@ -1,8 +1,3 @@
|
|||||||
//
|
|
||||||
// GraphicsFont.swift
|
|
||||||
// Font, FontPage, and Glyph wrappers around LCDFont / LCDFontPage / LCDFontGlyph.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
extension Graphics {
|
extension Graphics {
|
||||||
@@ -89,35 +84,4 @@ extension Graphics {
|
|||||||
Int(advance))
|
Int(advance))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A page of glyphs within a font. Wraps `LCDFontPage`.
|
|
||||||
/// Keep the font alive while using its pages.
|
|
||||||
public struct FontPage {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let font: Font
|
|
||||||
|
|
||||||
/// The glyph for `codepoint` within this page, with its bitmap and advance.
|
|
||||||
public func glyph(for codepoint: UInt32) -> (glyph: Glyph, bitmap: Bitmap?, advance: Int)? {
|
|
||||||
var bitmap: OpaquePointer?
|
|
||||||
var advance: Int32 = 0
|
|
||||||
guard let glyph = gfx.pointee.getPageGlyph.unsafelyUnwrapped(pointer, codepoint, &bitmap, &advance) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return (Glyph(pointer: glyph, font: font),
|
|
||||||
bitmap.map { Bitmap(pointer: $0, isOwned: false) },
|
|
||||||
Int(advance))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A single glyph within a font. Wraps `LCDFontGlyph`.
|
|
||||||
/// Keep the font alive while using its glyphs.
|
|
||||||
public struct Glyph {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let font: Font
|
|
||||||
|
|
||||||
/// The kerning adjustment between this glyph and the next character.
|
|
||||||
public func kerning(glyphCode: UInt32, nextCode: UInt32) -> Int {
|
|
||||||
Int(gfx.pointee.getGlyphKerning.unsafelyUnwrapped(pointer, glyphCode, nextCode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
private var streamAPI: UnsafePointer<playdate_videostream> { Playdate.videoStreamAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// Streams video (and audio) from a file or network connection.
|
||||||
|
/// Wraps `LCDStreamPlayer`.
|
||||||
|
public final class StreamPlayer {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
/// Retains the active source so it outlives the stream.
|
||||||
|
private var retainedSource: AnyObject?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
pointer = streamAPI.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
streamAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the sizes of the stream's video and audio buffers, in bytes.
|
||||||
|
public func setBufferSize(video: Int, audio: Int) {
|
||||||
|
streamAPI.pointee.setBufferSize.unsafelyUnwrapped(pointer, Int32(video), Int32(audio))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streams from an open file.
|
||||||
|
public func setFile(_ file: File.Handle) {
|
||||||
|
retainedSource = file
|
||||||
|
streamAPI.pointee.setFile.unsafelyUnwrapped(pointer, file.pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streams from an HTTP connection.
|
||||||
|
public func setHTTPConnection(_ connection: Network.HTTPConnection) {
|
||||||
|
retainedSource = connection
|
||||||
|
streamAPI.pointee.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streams from a TCP connection.
|
||||||
|
public func setTCPConnection(_ connection: Network.TCPConnection) {
|
||||||
|
retainedSource = connection
|
||||||
|
streamAPI.pointee.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The player used for the stream's audio track. Owned by the stream.
|
||||||
|
/// The same wrapper is returned on every access, so callbacks
|
||||||
|
/// registered on it stay valid for the stream's lifetime.
|
||||||
|
public var filePlayer: Sound.FilePlayer? {
|
||||||
|
guard let player = streamAPI.pointee.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
|
||||||
|
if let cached = cachedFilePlayer, cached.pointer == player {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
let wrapper = Sound.FilePlayer(pointer: player, isOwned: false)
|
||||||
|
cachedFilePlayer = wrapper
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cachedFilePlayer: Sound.FilePlayer?
|
||||||
|
|
||||||
|
/// The player used for the stream's video track. Owned by the stream.
|
||||||
|
public var videoPlayer: VideoPlayer? {
|
||||||
|
guard let player = streamAPI.pointee.getVideoPlayer.unsafelyUnwrapped(pointer) else { return nil }
|
||||||
|
return VideoPlayer(pointer: player, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances the stream. Returns `true` if a frame was drawn.
|
||||||
|
@discardableResult
|
||||||
|
public func update() -> Bool {
|
||||||
|
streamAPI.pointee.update.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of video frames currently buffered.
|
||||||
|
public var bufferedFrameCount: Int {
|
||||||
|
Int(streamAPI.pointee.getBufferedFrameCount.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The total number of bytes read from the source.
|
||||||
|
public var bytesRead: UInt32 {
|
||||||
|
streamAPI.pointee.getBytesRead.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-5
@@ -1,8 +1,3 @@
|
|||||||
//
|
|
||||||
// GraphicsTileMap.swift
|
|
||||||
// TileMap wrapper around LCDTileMap (playdate->graphics->tilemap).
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
private var tilemapAPI: UnsafePointer<playdate_tilemap> { Playdate.tilemapAPI.unsafelyUnwrapped }
|
private var tilemapAPI: UnsafePointer<playdate_tilemap> { Playdate.tilemapAPI.unsafelyUnwrapped }
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
private var videoAPI: UnsafePointer<playdate_video> { Playdate.videoAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// Plays .pdv video files. Wraps `LCDVideoPlayer`.
|
||||||
|
public final class VideoPlayer {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let isOwned: Bool
|
||||||
|
/// Retains the render context bitmap while the player uses it.
|
||||||
|
private var retainedContext: Bitmap?
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.isOwned = isOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens the .pdv file at `path`.
|
||||||
|
public convenience init(path: String) throws(PlaydateError) {
|
||||||
|
let pointer = path.withPlaydateCString { videoAPI.pointee.loadVideo.unsafelyUnwrapped($0) }
|
||||||
|
guard let pointer else {
|
||||||
|
throw PlaydateError(message: "unable to load video: \(path)")
|
||||||
|
}
|
||||||
|
self.init(pointer: pointer, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
videoAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the bitmap the video renders into.
|
||||||
|
public func setContext(_ context: Bitmap) throws(PlaydateError) {
|
||||||
|
guard videoAPI.pointee.setContext.unsafelyUnwrapped(pointer, context.pointer) != 0 else {
|
||||||
|
throw PlaydateError(message: error ?? "unable to set video context")
|
||||||
|
}
|
||||||
|
retainedContext = context
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The bitmap the video renders into.
|
||||||
|
public var context: Bitmap? {
|
||||||
|
guard let context = videoAPI.pointee.getContext.unsafelyUnwrapped(pointer) else { return nil }
|
||||||
|
return Bitmap(pointer: context, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders directly into the display framebuffer.
|
||||||
|
public func useScreenContext() {
|
||||||
|
retainedContext = nil
|
||||||
|
videoAPI.pointee.useScreenContext.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders frame `frame` into the current context.
|
||||||
|
public func renderFrame(_ frame: Int) throws(PlaydateError) {
|
||||||
|
guard videoAPI.pointee.renderFrame.unsafelyUnwrapped(pointer, Int32(frame)) != 0 else {
|
||||||
|
// Static message: the caller knows the frame it passed, and
|
||||||
|
// interpolating it would pull integer formatting machinery
|
||||||
|
// into the device binary.
|
||||||
|
throw PlaydateError(message: error ?? "unable to render frame")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The most recent error message, if any.
|
||||||
|
public var error: String? {
|
||||||
|
String(playdateCString: videoAPI.pointee.getError.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The video's dimensions, frame rate, frame count, and current frame.
|
||||||
|
public var info: (width: Int, height: Int, frameRate: Float, frameCount: Int, currentFrame: Int) {
|
||||||
|
var width: Int32 = 0, height: Int32 = 0, frameCount: Int32 = 0, currentFrame: Int32 = 0
|
||||||
|
var frameRate: Float = 0
|
||||||
|
videoAPI.pointee.getInfo.unsafelyUnwrapped(pointer, &width, &height, &frameRate,
|
||||||
|
&frameCount, ¤tFrame)
|
||||||
|
return (Int(width), Int(height), frameRate, Int(frameCount), Int(currentFrame))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// Mirroring applied when drawing a bitmap.
|
||||||
|
public enum BitmapFlip: UInt32, Sendable {
|
||||||
|
case unflipped = 0
|
||||||
|
case flippedX = 1
|
||||||
|
case flippedY = 2
|
||||||
|
case flippedXY = 3
|
||||||
|
|
||||||
|
init(_ flip: LCDBitmapFlip) { self = BitmapFlip(rawValue: UInt32(flip.rawValue)) ?? .unflipped }
|
||||||
|
var cValue: LCDBitmapFlip { LCDBitmapFlip(LCDBitmapFlip.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// A drawing color: solid or an 8×8 pattern.
|
||||||
|
public enum Color: Sendable {
|
||||||
|
case black
|
||||||
|
case white
|
||||||
|
case clear
|
||||||
|
case xor
|
||||||
|
case pattern(Pattern)
|
||||||
|
|
||||||
|
/// Materializes the `LCDColor` for the duration of `body`. Pattern
|
||||||
|
/// colors pass a pointer to a temporary, so the value must not be
|
||||||
|
/// stored beyond the call.
|
||||||
|
func withLCDColor<Result>(_ body: (LCDColor) -> Result) -> Result {
|
||||||
|
switch self {
|
||||||
|
case .black: return body(LCDColor(kColorBlack.rawValue))
|
||||||
|
case .white: return body(LCDColor(kColorWhite.rawValue))
|
||||||
|
case .clear: return body(LCDColor(kColorClear.rawValue))
|
||||||
|
case .xor: return body(LCDColor(kColorXOR.rawValue))
|
||||||
|
case .pattern(let pattern):
|
||||||
|
return withUnsafeBytes(of: pattern.bytes) { buffer in
|
||||||
|
body(LCDColor(UInt(bitPattern: buffer.baseAddress)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// How source pixels combine with the destination when drawing.
|
||||||
|
public enum DrawMode: UInt32, Sendable {
|
||||||
|
case copy = 0
|
||||||
|
case whiteTransparent = 1
|
||||||
|
case blackTransparent = 2
|
||||||
|
case fillWhite = 3
|
||||||
|
case fillBlack = 4
|
||||||
|
case xor = 5
|
||||||
|
case nxor = 6
|
||||||
|
case inverted = 7
|
||||||
|
|
||||||
|
init(_ mode: LCDBitmapDrawMode) { self = DrawMode(rawValue: UInt32(mode.rawValue)) ?? .copy }
|
||||||
|
var cValue: LCDBitmapDrawMode { LCDBitmapDrawMode(LCDBitmapDrawMode.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// The end cap style used when drawing lines.
|
||||||
|
public enum LineCapStyle: UInt32, Sendable {
|
||||||
|
case butt = 0
|
||||||
|
case square = 1
|
||||||
|
case round = 2
|
||||||
|
|
||||||
|
var cValue: LCDLineCapStyle { LCDLineCapStyle(LCDLineCapStyle.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// The winding rule used by `fillPolygon`.
|
||||||
|
public enum PolygonFillRule: UInt32, Sendable {
|
||||||
|
case nonZero = 0
|
||||||
|
case evenOdd = 1
|
||||||
|
|
||||||
|
var cValue: LCDPolygonFillRule { LCDPolygonFillRule(LCDPolygonFillRule.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// A solid color, for APIs that cannot take a pattern.
|
||||||
|
public enum SolidColor: UInt32, Sendable {
|
||||||
|
case black = 0
|
||||||
|
case white = 1
|
||||||
|
case clear = 2
|
||||||
|
case xor = 3
|
||||||
|
|
||||||
|
init(_ color: LCDSolidColor) { self = SolidColor(rawValue: UInt32(color.rawValue)) ?? .clear }
|
||||||
|
var cValue: LCDSolidColor { LCDSolidColor(LCDSolidColor.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// The encoding of text passed to the text functions.
|
||||||
|
public enum StringEncoding: UInt32, Sendable {
|
||||||
|
case ascii = 0
|
||||||
|
case utf8 = 1
|
||||||
|
case utf16LittleEndian = 2
|
||||||
|
|
||||||
|
var cValue: PDStringEncoding { PDStringEncoding(PDStringEncoding.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// Horizontal alignment for `drawText(in:)`.
|
||||||
|
public enum TextAlignment: UInt32, Sendable {
|
||||||
|
case left = 0
|
||||||
|
case center = 1
|
||||||
|
case right = 2
|
||||||
|
|
||||||
|
var cValue: PDTextAlignment { PDTextAlignment(PDTextAlignment.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// How text wraps in `drawText(in:)`.
|
||||||
|
public enum TextWrappingMode: UInt32, Sendable {
|
||||||
|
case clip = 0
|
||||||
|
case character = 1
|
||||||
|
case word = 2
|
||||||
|
|
||||||
|
var cValue: PDTextWrappingMode { PDTextWrappingMode(PDTextWrappingMode.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
//
|
|
||||||
// Graphics.swift
|
|
||||||
// Wraps `playdate->graphics` (pd_api_gfx.h): drawing state, shapes, text,
|
|
||||||
// and raw framebuffer access. Bitmap, font, tilemap, and video wrappers live
|
|
||||||
// in their own files.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
/// The graphics API: drawing, bitmaps, fonts, tilemaps, and video.
|
/// The graphics API: drawing, bitmaps, fonts, tilemaps, and video.
|
||||||
@@ -22,165 +15,6 @@ extension Graphics {
|
|||||||
/// The stride of a framebuffer row in bytes (`LCD_ROWSIZE`).
|
/// The stride of a framebuffer row in bytes (`LCD_ROWSIZE`).
|
||||||
public static let rowSize = 52
|
public static let rowSize = 52
|
||||||
|
|
||||||
// MARK: - Types
|
|
||||||
|
|
||||||
/// An 8×8 two-color pattern: 8 rows of image data followed by 8 rows of mask.
|
|
||||||
public struct Pattern: Sendable {
|
|
||||||
public var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
|
||||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
|
|
||||||
|
|
||||||
public init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
|
||||||
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
|
|
||||||
self.bytes = bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an opaque pattern from 8 rows of image data.
|
|
||||||
public init(rows r: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
|
|
||||||
bytes = (r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7,
|
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A drawing color: solid or an 8×8 pattern.
|
|
||||||
public enum Color: Sendable {
|
|
||||||
case black
|
|
||||||
case white
|
|
||||||
case clear
|
|
||||||
case xor
|
|
||||||
case pattern(Pattern)
|
|
||||||
|
|
||||||
/// Materializes the `LCDColor` for the duration of `body`. Pattern
|
|
||||||
/// colors pass a pointer to a temporary, so the value must not be
|
|
||||||
/// stored beyond the call.
|
|
||||||
func withLCDColor<Result>(_ body: (LCDColor) -> Result) -> Result {
|
|
||||||
switch self {
|
|
||||||
case .black: return body(LCDColor(kColorBlack.rawValue))
|
|
||||||
case .white: return body(LCDColor(kColorWhite.rawValue))
|
|
||||||
case .clear: return body(LCDColor(kColorClear.rawValue))
|
|
||||||
case .xor: return body(LCDColor(kColorXOR.rawValue))
|
|
||||||
case .pattern(let pattern):
|
|
||||||
return withUnsafeBytes(of: pattern.bytes) { buffer in
|
|
||||||
body(LCDColor(UInt(bitPattern: buffer.baseAddress)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A solid color, for APIs that cannot take a pattern.
|
|
||||||
public enum SolidColor: UInt32, Sendable {
|
|
||||||
case black = 0
|
|
||||||
case white = 1
|
|
||||||
case clear = 2
|
|
||||||
case xor = 3
|
|
||||||
|
|
||||||
init(_ color: LCDSolidColor) { self = SolidColor(rawValue: UInt32(color.rawValue)) ?? .clear }
|
|
||||||
var cValue: LCDSolidColor { LCDSolidColor(LCDSolidColor.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How source pixels combine with the destination when drawing.
|
|
||||||
public enum DrawMode: UInt32, Sendable {
|
|
||||||
case copy = 0
|
|
||||||
case whiteTransparent = 1
|
|
||||||
case blackTransparent = 2
|
|
||||||
case fillWhite = 3
|
|
||||||
case fillBlack = 4
|
|
||||||
case xor = 5
|
|
||||||
case nxor = 6
|
|
||||||
case inverted = 7
|
|
||||||
|
|
||||||
init(_ mode: LCDBitmapDrawMode) { self = DrawMode(rawValue: UInt32(mode.rawValue)) ?? .copy }
|
|
||||||
var cValue: LCDBitmapDrawMode { LCDBitmapDrawMode(LCDBitmapDrawMode.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mirroring applied when drawing a bitmap.
|
|
||||||
public enum BitmapFlip: UInt32, Sendable {
|
|
||||||
case unflipped = 0
|
|
||||||
case flippedX = 1
|
|
||||||
case flippedY = 2
|
|
||||||
case flippedXY = 3
|
|
||||||
|
|
||||||
init(_ flip: LCDBitmapFlip) { self = BitmapFlip(rawValue: UInt32(flip.rawValue)) ?? .unflipped }
|
|
||||||
var cValue: LCDBitmapFlip { LCDBitmapFlip(LCDBitmapFlip.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The end cap style used when drawing lines.
|
|
||||||
public enum LineCapStyle: UInt32, Sendable {
|
|
||||||
case butt = 0
|
|
||||||
case square = 1
|
|
||||||
case round = 2
|
|
||||||
|
|
||||||
var cValue: LCDLineCapStyle { LCDLineCapStyle(LCDLineCapStyle.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The encoding of text passed to the text functions.
|
|
||||||
public enum StringEncoding: UInt32, Sendable {
|
|
||||||
case ascii = 0
|
|
||||||
case utf8 = 1
|
|
||||||
case utf16LittleEndian = 2
|
|
||||||
|
|
||||||
var cValue: PDStringEncoding { PDStringEncoding(PDStringEncoding.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The winding rule used by `fillPolygon`.
|
|
||||||
public enum PolygonFillRule: UInt32, Sendable {
|
|
||||||
case nonZero = 0
|
|
||||||
case evenOdd = 1
|
|
||||||
|
|
||||||
var cValue: LCDPolygonFillRule { LCDPolygonFillRule(LCDPolygonFillRule.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How text wraps in `drawText(in:)`.
|
|
||||||
public enum TextWrappingMode: UInt32, Sendable {
|
|
||||||
case clip = 0
|
|
||||||
case character = 1
|
|
||||||
case word = 2
|
|
||||||
|
|
||||||
var cValue: PDTextWrappingMode { PDTextWrappingMode(PDTextWrappingMode.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Horizontal alignment for `drawText(in:)`.
|
|
||||||
public enum TextAlignment: UInt32, Sendable {
|
|
||||||
case left = 0
|
|
||||||
case center = 1
|
|
||||||
case right = 2
|
|
||||||
|
|
||||||
var cValue: PDTextAlignment { PDTextAlignment(PDTextAlignment.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An integer rectangle mirroring `LCDRect`. `right` and `bottom` are
|
|
||||||
/// not inclusive.
|
|
||||||
public struct Rect: Sendable {
|
|
||||||
public var left: Int
|
|
||||||
public var right: Int
|
|
||||||
public var top: Int
|
|
||||||
public var bottom: Int
|
|
||||||
|
|
||||||
public init(left: Int, right: Int, top: Int, bottom: Int) {
|
|
||||||
self.left = left
|
|
||||||
self.right = right
|
|
||||||
self.top = top
|
|
||||||
self.bottom = bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(x: Int, y: Int, width: Int, height: Int) {
|
|
||||||
self.init(left: x, right: x + width, top: y, bottom: y + height)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ rect: LCDRect) {
|
|
||||||
self.init(left: Int(rect.left), right: Int(rect.right),
|
|
||||||
top: Int(rect.top), bottom: Int(rect.bottom))
|
|
||||||
}
|
|
||||||
|
|
||||||
var cValue: LCDRect {
|
|
||||||
LCDRect(left: Int32(left), right: Int32(right),
|
|
||||||
top: Int32(top), bottom: Int32(bottom))
|
|
||||||
}
|
|
||||||
|
|
||||||
public func translated(dx: Int, dy: Int) -> Rect {
|
|
||||||
Rect(left: left + dx, right: right + dx, top: top + dy, bottom: bottom + dy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Drawing state
|
// MARK: - Drawing state
|
||||||
|
|
||||||
/// Clears the entire display, filling it with `color`.
|
/// Clears the entire display, filling it with `color`.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
extension Graphics.Bitmap {
|
||||||
|
/// The bitmap's dimensions, row stride, and raw pixel/mask storage.
|
||||||
|
/// The pointers are owned by the bitmap.
|
||||||
|
public struct Data {
|
||||||
|
public let width: Int
|
||||||
|
public let height: Int
|
||||||
|
public let rowBytes: Int
|
||||||
|
public let mask: UnsafeMutablePointer<UInt8>?
|
||||||
|
public let data: UnsafeMutablePointer<UInt8>?
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// A page of glyphs within a font. Wraps `LCDFontPage`.
|
||||||
|
/// Keep the font alive while using its pages.
|
||||||
|
public struct FontPage {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let font: Font
|
||||||
|
|
||||||
|
/// The glyph for `codepoint` within this page, with its bitmap and advance.
|
||||||
|
public func glyph(for codepoint: UInt32) -> (glyph: Glyph, bitmap: Bitmap?, advance: Int)? {
|
||||||
|
var bitmap: OpaquePointer?
|
||||||
|
var advance: Int32 = 0
|
||||||
|
guard let glyph = gfx.pointee.getPageGlyph.unsafelyUnwrapped(pointer, codepoint, &bitmap, &advance) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (Glyph(pointer: glyph, font: font),
|
||||||
|
bitmap.map { Bitmap(pointer: $0, isOwned: false) },
|
||||||
|
Int(advance))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// A single glyph within a font. Wraps `LCDFontGlyph`.
|
||||||
|
/// Keep the font alive while using its glyphs.
|
||||||
|
public struct Glyph {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let font: Font
|
||||||
|
|
||||||
|
/// The kerning adjustment between this glyph and the next character.
|
||||||
|
public func kerning(glyphCode: UInt32, nextCode: UInt32) -> Int {
|
||||||
|
Int(gfx.pointee.getGlyphKerning.unsafelyUnwrapped(pointer, glyphCode, nextCode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Graphics {
|
||||||
|
/// An integer rectangle mirroring `LCDRect`. `right` and `bottom` are
|
||||||
|
/// not inclusive.
|
||||||
|
public struct Rect: Sendable {
|
||||||
|
public var left: Int
|
||||||
|
public var right: Int
|
||||||
|
public var top: Int
|
||||||
|
public var bottom: Int
|
||||||
|
|
||||||
|
public init(left: Int, right: Int, top: Int, bottom: Int) {
|
||||||
|
self.left = left
|
||||||
|
self.right = right
|
||||||
|
self.top = top
|
||||||
|
self.bottom = bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(x: Int, y: Int, width: Int, height: Int) {
|
||||||
|
self.init(left: x, right: x + width, top: y, bottom: y + height)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ rect: LCDRect) {
|
||||||
|
self.init(left: Int(rect.left), right: Int(rect.right),
|
||||||
|
top: Int(rect.top), bottom: Int(rect.bottom))
|
||||||
|
}
|
||||||
|
|
||||||
|
var cValue: LCDRect {
|
||||||
|
LCDRect(left: Int32(left), right: Int32(right),
|
||||||
|
top: Int32(top), bottom: Int32(bottom))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func translated(dx: Int, dy: Int) -> Rect {
|
||||||
|
Rect(left: left + dx, right: right + dx, top: top + dy, bottom: bottom + dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
extension Graphics {
|
||||||
|
/// An 8×8 two-color pattern: 8 rows of image data followed by 8 rows of mask.
|
||||||
|
public struct Pattern: Sendable {
|
||||||
|
public var bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||||
|
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)
|
||||||
|
|
||||||
|
public init(bytes: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
|
||||||
|
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
|
||||||
|
self.bytes = bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an opaque pattern from 8 rows of image data.
|
||||||
|
public init(rows r: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
|
||||||
|
bytes = (r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7,
|
||||||
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
//
|
|
||||||
// GraphicsVideo.swift
|
|
||||||
// VideoPlayer and StreamPlayer wrappers around LCDVideoPlayer /
|
|
||||||
// LCDStreamPlayer (playdate->graphics->video / ->videostream).
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
|
||||||
|
|
||||||
private var videoAPI: UnsafePointer<playdate_video> { Playdate.videoAPI.unsafelyUnwrapped }
|
|
||||||
private var streamAPI: UnsafePointer<playdate_videostream> { Playdate.videoStreamAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
extension Graphics {
|
|
||||||
/// Plays .pdv video files. Wraps `LCDVideoPlayer`.
|
|
||||||
public final class VideoPlayer {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let isOwned: Bool
|
|
||||||
/// Retains the render context bitmap while the player uses it.
|
|
||||||
private var retainedContext: Bitmap?
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
self.pointer = pointer
|
|
||||||
self.isOwned = isOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens the .pdv file at `path`.
|
|
||||||
public convenience init(path: String) throws(PlaydateError) {
|
|
||||||
let pointer = path.withPlaydateCString { videoAPI.pointee.loadVideo.unsafelyUnwrapped($0) }
|
|
||||||
guard let pointer else {
|
|
||||||
throw PlaydateError(message: "unable to load video: \(path)")
|
|
||||||
}
|
|
||||||
self.init(pointer: pointer, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
videoAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the bitmap the video renders into.
|
|
||||||
public func setContext(_ context: Bitmap) throws(PlaydateError) {
|
|
||||||
guard videoAPI.pointee.setContext.unsafelyUnwrapped(pointer, context.pointer) != 0 else {
|
|
||||||
throw PlaydateError(message: error ?? "unable to set video context")
|
|
||||||
}
|
|
||||||
retainedContext = context
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The bitmap the video renders into.
|
|
||||||
public var context: Bitmap? {
|
|
||||||
guard let context = videoAPI.pointee.getContext.unsafelyUnwrapped(pointer) else { return nil }
|
|
||||||
return Bitmap(pointer: context, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders directly into the display framebuffer.
|
|
||||||
public func useScreenContext() {
|
|
||||||
retainedContext = nil
|
|
||||||
videoAPI.pointee.useScreenContext.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders frame `frame` into the current context.
|
|
||||||
public func renderFrame(_ frame: Int) throws(PlaydateError) {
|
|
||||||
guard videoAPI.pointee.renderFrame.unsafelyUnwrapped(pointer, Int32(frame)) != 0 else {
|
|
||||||
// Static message: the caller knows the frame it passed, and
|
|
||||||
// interpolating it would pull integer formatting machinery
|
|
||||||
// into the device binary.
|
|
||||||
throw PlaydateError(message: error ?? "unable to render frame")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The most recent error message, if any.
|
|
||||||
public var error: String? {
|
|
||||||
String(playdateCString: videoAPI.pointee.getError.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The video's dimensions, frame rate, frame count, and current frame.
|
|
||||||
public var info: (width: Int, height: Int, frameRate: Float, frameCount: Int, currentFrame: Int) {
|
|
||||||
var width: Int32 = 0, height: Int32 = 0, frameCount: Int32 = 0, currentFrame: Int32 = 0
|
|
||||||
var frameRate: Float = 0
|
|
||||||
videoAPI.pointee.getInfo.unsafelyUnwrapped(pointer, &width, &height, &frameRate,
|
|
||||||
&frameCount, ¤tFrame)
|
|
||||||
return (Int(width), Int(height), frameRate, Int(frameCount), Int(currentFrame))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Streams video (and audio) from a file or network connection.
|
|
||||||
/// Wraps `LCDStreamPlayer`.
|
|
||||||
public final class StreamPlayer {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
/// Retains the active source so it outlives the stream.
|
|
||||||
private var retainedSource: AnyObject?
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
pointer = streamAPI.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
streamAPI.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the sizes of the stream's video and audio buffers, in bytes.
|
|
||||||
public func setBufferSize(video: Int, audio: Int) {
|
|
||||||
streamAPI.pointee.setBufferSize.unsafelyUnwrapped(pointer, Int32(video), Int32(audio))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Streams from an open file.
|
|
||||||
public func setFile(_ file: File.Handle) {
|
|
||||||
retainedSource = file
|
|
||||||
streamAPI.pointee.setFile.unsafelyUnwrapped(pointer, file.pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Streams from an HTTP connection.
|
|
||||||
public func setHTTPConnection(_ connection: Network.HTTPConnection) {
|
|
||||||
retainedSource = connection
|
|
||||||
streamAPI.pointee.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Streams from a TCP connection.
|
|
||||||
public func setTCPConnection(_ connection: Network.TCPConnection) {
|
|
||||||
retainedSource = connection
|
|
||||||
streamAPI.pointee.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The player used for the stream's audio track. Owned by the stream.
|
|
||||||
/// The same wrapper is returned on every access, so callbacks
|
|
||||||
/// registered on it stay valid for the stream's lifetime.
|
|
||||||
public var filePlayer: Sound.FilePlayer? {
|
|
||||||
guard let player = streamAPI.pointee.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
|
|
||||||
if let cached = cachedFilePlayer, cached.pointer == player {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
let wrapper = Sound.FilePlayer(pointer: player, isOwned: false)
|
|
||||||
cachedFilePlayer = wrapper
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cachedFilePlayer: Sound.FilePlayer?
|
|
||||||
|
|
||||||
/// The player used for the stream's video track. Owned by the stream.
|
|
||||||
public var videoPlayer: VideoPlayer? {
|
|
||||||
guard let player = streamAPI.pointee.getVideoPlayer.unsafelyUnwrapped(pointer) else { return nil }
|
|
||||||
return VideoPlayer(pointer: player, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advances the stream. Returns `true` if a frame was drawn.
|
|
||||||
@discardableResult
|
|
||||||
public func update() -> Bool {
|
|
||||||
streamAPI.pointee.update.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of video frames currently buffered.
|
|
||||||
public var bufferedFrameCount: Int {
|
|
||||||
Int(streamAPI.pointee.getBufferedFrameCount.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The total number of bytes read from the source.
|
|
||||||
public var bytesRead: UInt32 {
|
|
||||||
streamAPI.pointee.getBytesRead.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension JSON {
|
||||||
|
/// A streaming JSON encoder writing into a string. Wraps `json_encoder`.
|
||||||
|
public final class Encoder {
|
||||||
|
private final class Output {
|
||||||
|
var text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private var encoder = json_encoder()
|
||||||
|
private let output = Output()
|
||||||
|
|
||||||
|
public init(pretty: Bool = false) {
|
||||||
|
jsonAPI.pointee.initEncoder.unsafelyUnwrapped(&encoder, { userdata, string, length in
|
||||||
|
guard let userdata, let string else { return }
|
||||||
|
let output = Unmanaged<Output>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
let bytes = UnsafeRawBufferPointer(start: string, count: Int(length))
|
||||||
|
output.text += String(decoding: bytes, as: UTF8.self)
|
||||||
|
}, Unmanaged.passUnretained(output).toOpaque(), pretty ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The JSON produced so far.
|
||||||
|
public var json: String { output.text }
|
||||||
|
|
||||||
|
public func startArray() {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.startArray.unsafelyUnwrapped($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call before writing each array element.
|
||||||
|
public func addArrayMember() {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.addArrayMember.unsafelyUnwrapped($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func endArray() {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.endArray.unsafelyUnwrapped($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startTable() {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.startTable.unsafelyUnwrapped($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call before writing each table value.
|
||||||
|
public func addTableMember(name: String) {
|
||||||
|
name.withPlaydateCString { cName in
|
||||||
|
withUnsafeMutablePointer(to: &encoder) {
|
||||||
|
$0.pointee.addTableMember.unsafelyUnwrapped($0, cName, Int32(name.utf8.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func endTable() {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.endTable.unsafelyUnwrapped($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeNull() {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeNull.unsafelyUnwrapped($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeBool(_ value: Bool) {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) {
|
||||||
|
(value ? $0.pointee.writeTrue : $0.pointee.writeFalse).unsafelyUnwrapped($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeInt(_ value: Int) {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeInt.unsafelyUnwrapped($0, Int32(value)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeDouble(_ value: Double) {
|
||||||
|
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeDouble.unsafelyUnwrapped($0, value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func writeString(_ value: String) {
|
||||||
|
value.withPlaydateCString { cString in
|
||||||
|
withUnsafeMutablePointer(to: &encoder) {
|
||||||
|
$0.pointee.writeString.unsafelyUnwrapped($0, cString, Int32(value.utf8.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a complete `Value` tree.
|
||||||
|
public func write(_ value: Value) {
|
||||||
|
switch value {
|
||||||
|
case .null:
|
||||||
|
writeNull()
|
||||||
|
case .bool(let bool):
|
||||||
|
writeBool(bool)
|
||||||
|
case .int(let int):
|
||||||
|
writeInt(int)
|
||||||
|
case .float(let float):
|
||||||
|
writeDouble(Double(float))
|
||||||
|
case .string(let string):
|
||||||
|
writeString(string)
|
||||||
|
case .array(let items):
|
||||||
|
startArray()
|
||||||
|
for item in items {
|
||||||
|
addArrayMember()
|
||||||
|
write(item)
|
||||||
|
}
|
||||||
|
endArray()
|
||||||
|
case .table(let entries):
|
||||||
|
startTable()
|
||||||
|
for (key, entry) in entries {
|
||||||
|
addTableMember(name: key)
|
||||||
|
write(entry)
|
||||||
|
}
|
||||||
|
endTable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
extension JSON {
|
||||||
|
/// A decoded JSON value.
|
||||||
|
public indirect enum Value {
|
||||||
|
case null
|
||||||
|
case bool(Bool)
|
||||||
|
case int(Int)
|
||||||
|
case float(Float)
|
||||||
|
case string(String)
|
||||||
|
case array([Value])
|
||||||
|
case table([String: Value])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,11 @@
|
|||||||
//
|
|
||||||
// JSON.swift
|
|
||||||
// Wraps `playdate->json` (pd_api_json.h).
|
|
||||||
//
|
|
||||||
// The C decoder is callback-based; this wrapper drives it to build a
|
|
||||||
// complete `Value` tree. The encoder is exposed both as a streaming
|
|
||||||
// `Encoder` and as a one-shot `encode(_:)` of a `Value`.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
private var jsonAPI: UnsafePointer<playdate_json> { Playdate.jsonAPI.unsafelyUnwrapped }
|
var jsonAPI: UnsafePointer<playdate_json> { Playdate.jsonAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
/// The JSON API: decoding to and encoding from a `Value` tree.
|
/// The JSON API: decoding to and encoding from a `Value` tree.
|
||||||
public enum JSON {}
|
public enum JSON {}
|
||||||
|
|
||||||
extension JSON {
|
extension JSON {
|
||||||
/// A decoded JSON value.
|
|
||||||
public indirect enum Value {
|
|
||||||
case null
|
|
||||||
case bool(Bool)
|
|
||||||
case int(Int)
|
|
||||||
case float(Float)
|
|
||||||
case string(String)
|
|
||||||
case array([Value])
|
|
||||||
case table([String: Value])
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Decoding
|
// MARK: - Decoding
|
||||||
|
|
||||||
private final class ValueBox {
|
private final class ValueBox {
|
||||||
@@ -178,114 +158,6 @@ extension JSON {
|
|||||||
|
|
||||||
// MARK: - Encoding
|
// MARK: - Encoding
|
||||||
|
|
||||||
/// A streaming JSON encoder writing into a string. Wraps `json_encoder`.
|
|
||||||
public final class Encoder {
|
|
||||||
private final class Output {
|
|
||||||
var text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
private var encoder = json_encoder()
|
|
||||||
private let output = Output()
|
|
||||||
|
|
||||||
public init(pretty: Bool = false) {
|
|
||||||
jsonAPI.pointee.initEncoder.unsafelyUnwrapped(&encoder, { userdata, string, length in
|
|
||||||
guard let userdata, let string else { return }
|
|
||||||
let output = Unmanaged<Output>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
let bytes = UnsafeRawBufferPointer(start: string, count: Int(length))
|
|
||||||
output.text += String(decoding: bytes, as: UTF8.self)
|
|
||||||
}, Unmanaged.passUnretained(output).toOpaque(), pretty ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The JSON produced so far.
|
|
||||||
public var json: String { output.text }
|
|
||||||
|
|
||||||
public func startArray() {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.startArray.unsafelyUnwrapped($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call before writing each array element.
|
|
||||||
public func addArrayMember() {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.addArrayMember.unsafelyUnwrapped($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public func endArray() {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.endArray.unsafelyUnwrapped($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public func startTable() {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.startTable.unsafelyUnwrapped($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call before writing each table value.
|
|
||||||
public func addTableMember(name: String) {
|
|
||||||
name.withPlaydateCString { cName in
|
|
||||||
withUnsafeMutablePointer(to: &encoder) {
|
|
||||||
$0.pointee.addTableMember.unsafelyUnwrapped($0, cName, Int32(name.utf8.count))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func endTable() {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.endTable.unsafelyUnwrapped($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeNull() {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeNull.unsafelyUnwrapped($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeBool(_ value: Bool) {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) {
|
|
||||||
(value ? $0.pointee.writeTrue : $0.pointee.writeFalse).unsafelyUnwrapped($0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeInt(_ value: Int) {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeInt.unsafelyUnwrapped($0, Int32(value)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeDouble(_ value: Double) {
|
|
||||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeDouble.unsafelyUnwrapped($0, value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
public func writeString(_ value: String) {
|
|
||||||
value.withPlaydateCString { cString in
|
|
||||||
withUnsafeMutablePointer(to: &encoder) {
|
|
||||||
$0.pointee.writeString.unsafelyUnwrapped($0, cString, Int32(value.utf8.count))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a complete `Value` tree.
|
|
||||||
public func write(_ value: Value) {
|
|
||||||
switch value {
|
|
||||||
case .null:
|
|
||||||
writeNull()
|
|
||||||
case .bool(let bool):
|
|
||||||
writeBool(bool)
|
|
||||||
case .int(let int):
|
|
||||||
writeInt(int)
|
|
||||||
case .float(let float):
|
|
||||||
writeDouble(Double(float))
|
|
||||||
case .string(let string):
|
|
||||||
writeString(string)
|
|
||||||
case .array(let items):
|
|
||||||
startArray()
|
|
||||||
for item in items {
|
|
||||||
addArrayMember()
|
|
||||||
write(item)
|
|
||||||
}
|
|
||||||
endArray()
|
|
||||||
case .table(let entries):
|
|
||||||
startTable()
|
|
||||||
for (key, entry) in entries {
|
|
||||||
addTableMember(name: key)
|
|
||||||
write(entry)
|
|
||||||
}
|
|
||||||
endTable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encodes a `Value` tree as a JSON string.
|
/// Encodes a `Value` tree as a JSON string.
|
||||||
public static func encode(_ value: Value, pretty: Bool = false) -> String {
|
public static func encode(_ value: Value, pretty: Bool = false) -> String {
|
||||||
let encoder = Encoder(pretty: pretty)
|
let encoder = Encoder(pretty: pretty)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
public import CPlaydate
|
||||||
|
|
||||||
|
extension Lua {
|
||||||
|
/// A function callable from Lua. Returns the number of values it pushed
|
||||||
|
/// onto the stack.
|
||||||
|
public typealias CFunction = lua_CFunction
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extension Lua {
|
||||||
|
/// A constant published on a registered class.
|
||||||
|
public enum ClassValue {
|
||||||
|
case int(name: String, value: UInt32)
|
||||||
|
case float(name: String, value: Float)
|
||||||
|
case string(name: String, value: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Lua {
|
||||||
|
/// The type of a value on the Lua stack.
|
||||||
|
public enum Kind: UInt32, Sendable {
|
||||||
|
case `nil` = 0
|
||||||
|
case bool = 1
|
||||||
|
case int = 2
|
||||||
|
case float = 3
|
||||||
|
case string = 4
|
||||||
|
case table = 5
|
||||||
|
case function = 6
|
||||||
|
case thread = 7
|
||||||
|
case object = 8
|
||||||
|
|
||||||
|
init(_ type: LuaType) { self = Kind(rawValue: UInt32(type.rawValue)) ?? .nil }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +1,12 @@
|
|||||||
//
|
internal import CPlaydate
|
||||||
// Lua.swift
|
|
||||||
// Wraps `playdate->lua` (pd_api_lua.h).
|
|
||||||
//
|
|
||||||
// Lua callbacks are C function pointers without userdata, so functions
|
|
||||||
// registered here must be `@convention(c)` (the `CFunction` typealias), not
|
|
||||||
// capturing closures.
|
|
||||||
//
|
|
||||||
|
|
||||||
public import CPlaydate
|
var luaAPI: UnsafePointer<playdate_lua> { Playdate.luaAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
private var luaAPI: UnsafePointer<playdate_lua> { Playdate.luaAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The Lua bridge: registering C functions and classes, and exchanging
|
/// The Lua bridge: registering C functions and classes, and exchanging
|
||||||
/// values with Lua code.
|
/// values with Lua code.
|
||||||
public enum Lua {}
|
public enum Lua {}
|
||||||
|
|
||||||
extension Lua {
|
extension Lua {
|
||||||
/// A function callable from Lua. Returns the number of values it pushed
|
|
||||||
/// onto the stack.
|
|
||||||
public typealias CFunction = lua_CFunction
|
|
||||||
|
|
||||||
/// The type of a value on the Lua stack.
|
|
||||||
public enum Kind: UInt32, Sendable {
|
|
||||||
case `nil` = 0
|
|
||||||
case bool = 1
|
|
||||||
case int = 2
|
|
||||||
case float = 3
|
|
||||||
case string = 4
|
|
||||||
case table = 5
|
|
||||||
case function = 6
|
|
||||||
case thread = 7
|
|
||||||
case object = 8
|
|
||||||
|
|
||||||
init(_ type: LuaType) { self = Kind(rawValue: UInt32(type.rawValue)) ?? .nil }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A constant published on a registered class.
|
|
||||||
public enum ClassValue {
|
|
||||||
case int(name: String, value: UInt32)
|
|
||||||
case float(name: String, value: Float)
|
|
||||||
case string(name: String, value: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Buffers passed to `registerClass`/`addFunction`; the OS may keep
|
/// Buffers passed to `registerClass`/`addFunction`; the OS may keep
|
||||||
/// referencing them, so they are retained for the life of the game.
|
/// referencing them, so they are retained for the life of the game.
|
||||||
nonisolated(unsafe) private static var retainedBuffers: [UnsafeMutableRawPointer] = []
|
nonisolated(unsafe) private static var retainedBuffers: [UnsafeMutableRawPointer] = []
|
||||||
@@ -253,35 +218,6 @@ extension Lua {
|
|||||||
return UDObject(pointer: pointer)
|
return UDObject(pointer: pointer)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle to a Lua-owned object. Wraps `LuaUDObject`.
|
|
||||||
public struct UDObject {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
|
|
||||||
/// Prevents the object from being garbage-collected until `release()`.
|
|
||||||
@discardableResult
|
|
||||||
public func retain() -> UDObject {
|
|
||||||
UDObject(pointer: luaAPI.pointee.retainObject.unsafelyUnwrapped(pointer).unsafelyUnwrapped)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func release() {
|
|
||||||
luaAPI.pointee.releaseObject.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pops the value on top of the stack and stores it in the object's
|
|
||||||
/// user-value `slot` (1-based).
|
|
||||||
public func setUserValue(slot: UInt32) {
|
|
||||||
luaAPI.pointee.setUserValue.unsafelyUnwrapped(pointer, slot)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pushes the value in user-value `slot` onto the stack and returns
|
|
||||||
/// its stack position, or `nil` if there is none.
|
|
||||||
@discardableResult
|
|
||||||
public func getUserValue(slot: UInt32) -> Int? {
|
|
||||||
let position = luaAPI.pointee.getUserValue.unsafelyUnwrapped(pointer, slot)
|
|
||||||
return position == 0 ? nil : Int(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Calling Lua
|
// MARK: - Calling Lua
|
||||||
|
|
||||||
/// Calls the Lua function `name`. Push the arguments onto the stack
|
/// Calls the Lua function `name`. Push the arguments onto the stack
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Lua {
|
||||||
|
/// A handle to a Lua-owned object. Wraps `LuaUDObject`.
|
||||||
|
public struct UDObject {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
|
||||||
|
/// Prevents the object from being garbage-collected until `release()`.
|
||||||
|
@discardableResult
|
||||||
|
public func retain() -> UDObject {
|
||||||
|
UDObject(pointer: luaAPI.pointee.retainObject.unsafelyUnwrapped(pointer).unsafelyUnwrapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func release() {
|
||||||
|
luaAPI.pointee.releaseObject.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pops the value on top of the stack and stores it in the object's
|
||||||
|
/// user-value `slot` (1-based).
|
||||||
|
public func setUserValue(slot: UInt32) {
|
||||||
|
luaAPI.pointee.setUserValue.unsafelyUnwrapped(pointer, slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes the value in user-value `slot` onto the stack and returns
|
||||||
|
/// its stack position, or `nil` if there is none.
|
||||||
|
@discardableResult
|
||||||
|
public func getUserValue(slot: UInt32) -> Int? {
|
||||||
|
let position = luaAPI.pointee.getUserValue.unsafelyUnwrapped(pointer, slot)
|
||||||
|
return position == 0 ? nil : Int(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,515 +0,0 @@
|
|||||||
//
|
|
||||||
// Network.swift
|
|
||||||
// Wraps `playdate->network` (pd_api_network.h): HTTP and TCP connections.
|
|
||||||
//
|
|
||||||
// The binding stores a back-reference to each connection wrapper in the
|
|
||||||
// underlying object's userdata slot so callbacks can recover the wrapper;
|
|
||||||
// the C userdata slot is therefore reserved by the binding.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
|
||||||
|
|
||||||
private var networkAPI: UnsafePointer<playdate_network> { Playdate.networkAPI.unsafelyUnwrapped }
|
|
||||||
private var httpAPI: UnsafePointer<playdate_http> { Playdate.httpAPI.unsafelyUnwrapped }
|
|
||||||
private var tcpAPI: UnsafePointer<playdate_tcp> { Playdate.tcpAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The network API: wifi status, HTTP, and TCP.
|
|
||||||
public enum Network {}
|
|
||||||
|
|
||||||
extension Network {
|
|
||||||
/// A network error code (`PDNetErr`).
|
|
||||||
public enum NetError: Int32, Swift.Error, Sendable {
|
|
||||||
case noDevice = -1
|
|
||||||
case busy = -2
|
|
||||||
case writeError = -3
|
|
||||||
case writeBusy = -4
|
|
||||||
case writeTimeout = -5
|
|
||||||
case readError = -6
|
|
||||||
case readBusy = -7
|
|
||||||
case readTimeout = -8
|
|
||||||
case readOverflow = -9
|
|
||||||
case frameError = -10
|
|
||||||
case badResponse = -11
|
|
||||||
case errorResponse = -12
|
|
||||||
case resetTimeout = -13
|
|
||||||
case bufferTooSmall = -14
|
|
||||||
case unexpectedResponse = -15
|
|
||||||
case notConnectedToAP = -16
|
|
||||||
case notImplemented = -17
|
|
||||||
case connectionClosed = -18
|
|
||||||
case unknown = 1
|
|
||||||
|
|
||||||
init(_ error: PDNetErr) {
|
|
||||||
self = NetError(rawValue: Int32(error.rawValue)) ?? .unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Throws unless `error` is `NET_OK`.
|
|
||||||
static func check(_ error: PDNetErr) throws(NetError) {
|
|
||||||
if error != NET_OK {
|
|
||||||
throw NetError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts an error code to `nil` (OK) or a `NetError`.
|
|
||||||
static func optionalError(_ error: PDNetErr) -> NetError? {
|
|
||||||
error == NET_OK ? nil : NetError(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The device's wifi status.
|
|
||||||
public enum WifiStatus: UInt32, Sendable {
|
|
||||||
case notConnected = 0
|
|
||||||
case connected = 1
|
|
||||||
/// A connection was attempted but no configured access point was
|
|
||||||
/// available.
|
|
||||||
case notAvailable = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var status: WifiStatus {
|
|
||||||
WifiStatus(rawValue: UInt32(networkAPI.pointee.getStatus.unsafelyUnwrapped().rawValue)) ?? .notConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turns the wifi radio on or off. The completion receives `nil` on
|
|
||||||
/// success. Completions of overlapping calls are delivered in call order.
|
|
||||||
public static func setEnabled(_ enabled: Bool, completion: ((NetError?) -> Void)? = nil) {
|
|
||||||
if let completion {
|
|
||||||
setEnabledCompletions.append(completion)
|
|
||||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, { error in
|
|
||||||
guard !Network.setEnabledCompletions.isEmpty else { return }
|
|
||||||
let completion = Network.setEnabledCompletions.removeFirst()
|
|
||||||
completion(Network.optionalError(error))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated(unsafe) private static var setEnabledCompletions: [(NetError?) -> Void] = []
|
|
||||||
|
|
||||||
/// Requests permission to connect to `server`. Shared by HTTP and TCP.
|
|
||||||
fileprivate static func requestAccess(
|
|
||||||
rawRequest: (UnsafePointer<CChar>?, Int32, Bool, UnsafePointer<CChar>?,
|
|
||||||
(@convention(c) (Bool, UnsafeMutableRawPointer?) -> Void)?,
|
|
||||||
UnsafeMutableRawPointer?) -> accessReply,
|
|
||||||
server: String, port: Int, useSSL: Bool, purpose: String?,
|
|
||||||
completion: @escaping (Bool) -> Void) -> AccessReply {
|
|
||||||
final class Box {
|
|
||||||
let body: (Bool) -> Void
|
|
||||||
init(_ body: @escaping (Bool) -> Void) { self.body = body }
|
|
||||||
}
|
|
||||||
let box = Unmanaged.passRetained(Box(completion))
|
|
||||||
let trampoline: @convention(c) (Bool, UnsafeMutableRawPointer?) -> Void = { allowed, userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
Unmanaged<Box>.fromOpaque(userdata).takeRetainedValue().body(allowed)
|
|
||||||
}
|
|
||||||
let reply = server.withPlaydateCString { cServer in
|
|
||||||
if let purpose {
|
|
||||||
return purpose.withPlaydateCString { cPurpose in
|
|
||||||
rawRequest(cServer, Int32(port), useSSL, cPurpose, trampoline, box.toOpaque())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return rawRequest(cServer, Int32(port), useSSL, nil, trampoline, box.toOpaque())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if reply != kAccessAsk {
|
|
||||||
// The callback will not be invoked; balance the retain.
|
|
||||||
box.release()
|
|
||||||
}
|
|
||||||
return AccessReply(rawValue: UInt32(reply.rawValue)) ?? .ask
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - HTTP
|
|
||||||
|
|
||||||
/// An HTTP connection to a server. Wraps `HTTPConnection`.
|
|
||||||
public final class HTTPConnection {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
|
|
||||||
var headerReceivedCallback: ((HTTPConnection, _ key: String, _ value: String) -> Void)?
|
|
||||||
var headersReadCallback: ((HTTPConnection) -> Void)?
|
|
||||||
var responseCallback: ((HTTPConnection) -> Void)?
|
|
||||||
var requestCompleteCallback: ((HTTPConnection) -> Void)?
|
|
||||||
var connectionClosedCallback: ((HTTPConnection) -> Void)?
|
|
||||||
|
|
||||||
/// Requests permission to connect to `server`. If the reply is
|
|
||||||
/// `.ask`, the completion is called later with the user's answer.
|
|
||||||
@discardableResult
|
|
||||||
public static func requestAccess(server: String, port: Int = 443, useSSL: Bool = true,
|
|
||||||
purpose: String? = nil,
|
|
||||||
completion: @escaping (Bool) -> Void) -> AccessReply {
|
|
||||||
Network.requestAccess(
|
|
||||||
rawRequest: { httpAPI.pointee.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
|
|
||||||
server: server, port: port, useSSL: useSSL, purpose: purpose,
|
|
||||||
completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens a connection to `server`. Fails if access has not been
|
|
||||||
/// granted.
|
|
||||||
public init?(server: String, port: Int = 443, useSSL: Bool = true) {
|
|
||||||
let pointer = server.withPlaydateCString {
|
|
||||||
httpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
|
|
||||||
}
|
|
||||||
guard let pointer else { return nil }
|
|
||||||
self.pointer = pointer
|
|
||||||
httpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
httpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, nil)
|
|
||||||
httpAPI.pointee.release.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func wrapper(for pointer: OpaquePointer?) -> HTTPConnection? {
|
|
||||||
guard let pointer,
|
|
||||||
let userdata = httpAPI.pointee.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
|
|
||||||
return Unmanaged<HTTPConnection>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Configuration
|
|
||||||
|
|
||||||
/// The time to wait for the connection to open, in milliseconds.
|
|
||||||
public func setConnectTimeout(milliseconds: Int) {
|
|
||||||
httpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether to keep the connection open after a request completes.
|
|
||||||
public func setKeepAlive(_ keepAlive: Bool) {
|
|
||||||
httpAPI.pointee.setKeepAlive.unsafelyUnwrapped(pointer, keepAlive)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a `Range: bytes=start-end` header to future requests.
|
|
||||||
public func setByteRange(start: Int, end: Int) {
|
|
||||||
httpAPI.pointee.setByteRange.unsafelyUnwrapped(pointer, Int32(start), Int32(end))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The time to wait for incoming data, in milliseconds.
|
|
||||||
public func setReadTimeout(milliseconds: Int) {
|
|
||||||
httpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The size of the connection's read buffer, in bytes.
|
|
||||||
public func setReadBufferSize(bytes: Int) {
|
|
||||||
httpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Requests
|
|
||||||
|
|
||||||
/// Sends a GET request for `path`. `headers` are raw header lines
|
|
||||||
/// (e.g. "Accept: text/html\r\n").
|
|
||||||
public func get(path: String, headers: String = "") throws(NetError) {
|
|
||||||
let error = path.withPlaydateCString { cPath in
|
|
||||||
headers.withPlaydateCString { cHeaders in
|
|
||||||
httpAPI.pointee.get.unsafelyUnwrapped(pointer, cPath, cHeaders, headers.utf8.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try Network.check(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a POST request for `path` with the given body.
|
|
||||||
public func post(path: String, headers: String = "", body: [UInt8]) throws(NetError) {
|
|
||||||
let error = path.withPlaydateCString { cPath in
|
|
||||||
headers.withPlaydateCString { cHeaders in
|
|
||||||
body.withUnsafeBytes { bodyBuffer in
|
|
||||||
httpAPI.pointee.post.unsafelyUnwrapped(
|
|
||||||
pointer, cPath, cHeaders, headers.utf8.count,
|
|
||||||
bodyBuffer.baseAddress?.assumingMemoryBound(to: CChar.self),
|
|
||||||
bodyBuffer.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try Network.check(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a request with an arbitrary HTTP method.
|
|
||||||
public func query(method: String, path: String, headers: String = "",
|
|
||||||
body: [UInt8] = []) throws(NetError) {
|
|
||||||
let error = method.withPlaydateCString { cMethod in
|
|
||||||
path.withPlaydateCString { cPath in
|
|
||||||
headers.withPlaydateCString { cHeaders in
|
|
||||||
body.withUnsafeBytes { bodyBuffer in
|
|
||||||
httpAPI.pointee.query.unsafelyUnwrapped(
|
|
||||||
pointer, cMethod, cPath, cHeaders, headers.utf8.count,
|
|
||||||
bodyBuffer.baseAddress?.assumingMemoryBound(to: CChar.self),
|
|
||||||
bodyBuffer.count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try Network.check(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Response
|
|
||||||
|
|
||||||
/// The last error on the connection, if any.
|
|
||||||
public var error: NetError? {
|
|
||||||
Network.optionalError(httpAPI.pointee.getError.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of bytes read of the current response, and the total
|
|
||||||
/// expected (0 if the response has no Content-Length).
|
|
||||||
public var progress: (read: Int, total: Int) {
|
|
||||||
var read: Int32 = 0, total: Int32 = 0
|
|
||||||
httpAPI.pointee.getProgress.unsafelyUnwrapped(pointer, &read, &total)
|
|
||||||
return (Int(read), Int(total))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The HTTP status code of the response.
|
|
||||||
public var responseStatus: Int {
|
|
||||||
Int(httpAPI.pointee.getResponseStatus.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of response bytes available to read.
|
|
||||||
public var bytesAvailable: Int {
|
|
||||||
Int(httpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads up to `buffer.count` response bytes. Returns the number of
|
|
||||||
/// bytes read.
|
|
||||||
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
|
|
||||||
let result = httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress,
|
|
||||||
UInt32(buffer.count))
|
|
||||||
if result < 0 {
|
|
||||||
throw NetError(rawValue: result) ?? .unknown
|
|
||||||
}
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads up to `length` available response bytes.
|
|
||||||
public func read(length: Int) throws(NetError) -> [UInt8] {
|
|
||||||
var bytes = [UInt8](repeating: 0, count: length)
|
|
||||||
let result = bytes.withUnsafeMutableBytes { buffer in
|
|
||||||
httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
|
||||||
}
|
|
||||||
if result < 0 {
|
|
||||||
throw NetError(rawValue: result) ?? .unknown
|
|
||||||
}
|
|
||||||
bytes.removeLast(length - Int(result))
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Closes the connection.
|
|
||||||
public func close() {
|
|
||||||
httpAPI.pointee.close.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Callbacks
|
|
||||||
|
|
||||||
/// Called for each header line as it arrives.
|
|
||||||
public func setHeaderReceivedCallback(_ callback: ((HTTPConnection, _ key: String, _ value: String) -> Void)?) {
|
|
||||||
headerReceivedCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
httpAPI.pointee.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, { connection, key, value in
|
|
||||||
guard let wrapper = HTTPConnection.wrapper(for: connection),
|
|
||||||
let key = String(playdateCString: key),
|
|
||||||
let value = String(playdateCString: value) else { return }
|
|
||||||
wrapper.headerReceivedCallback?(wrapper, key, value)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
httpAPI.pointee.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when all headers have been read.
|
|
||||||
public func setHeadersReadCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
|
||||||
headersReadCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
httpAPI.pointee.setHeadersReadCallback.unsafelyUnwrapped(pointer, { connection in
|
|
||||||
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
|
||||||
wrapper.headersReadCallback?(wrapper)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
httpAPI.pointee.setHeadersReadCallback.unsafelyUnwrapped(pointer, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when response data is available to read.
|
|
||||||
public func setResponseCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
|
||||||
responseCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
httpAPI.pointee.setResponseCallback.unsafelyUnwrapped(pointer, { connection in
|
|
||||||
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
|
||||||
wrapper.responseCallback?(wrapper)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
httpAPI.pointee.setResponseCallback.unsafelyUnwrapped(pointer, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when the request finishes.
|
|
||||||
public func setRequestCompleteCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
|
||||||
requestCompleteCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
httpAPI.pointee.setRequestCompleteCallback.unsafelyUnwrapped(pointer, { connection in
|
|
||||||
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
|
||||||
wrapper.requestCompleteCallback?(wrapper)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
httpAPI.pointee.setRequestCompleteCallback.unsafelyUnwrapped(pointer, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when the connection closes.
|
|
||||||
public func setConnectionClosedCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
|
||||||
connectionClosedCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
httpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection in
|
|
||||||
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
|
||||||
wrapper.connectionClosedCallback?(wrapper)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
httpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - TCP
|
|
||||||
|
|
||||||
/// A TCP connection to a server. Wraps `TCPConnection`.
|
|
||||||
public final class TCPConnection {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
|
|
||||||
var openCompletion: ((TCPConnection, NetError?) -> Void)?
|
|
||||||
var connectionClosedCallback: ((TCPConnection, NetError?) -> Void)?
|
|
||||||
|
|
||||||
/// Requests permission to connect to `server`. If the reply is
|
|
||||||
/// `.ask`, the completion is called later with the user's answer.
|
|
||||||
@discardableResult
|
|
||||||
public static func requestAccess(server: String, port: Int, useSSL: Bool = true,
|
|
||||||
purpose: String? = nil,
|
|
||||||
completion: @escaping (Bool) -> Void) -> AccessReply {
|
|
||||||
Network.requestAccess(
|
|
||||||
rawRequest: { tcpAPI.pointee.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
|
|
||||||
server: server, port: port, useSSL: useSSL, purpose: purpose,
|
|
||||||
completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a connection to `server`. Fails if access has not been
|
|
||||||
/// granted. Call `open(_:)` to connect.
|
|
||||||
public init?(server: String, port: Int, useSSL: Bool = true) {
|
|
||||||
let pointer = server.withPlaydateCString {
|
|
||||||
tcpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
|
|
||||||
}
|
|
||||||
guard let pointer else { return nil }
|
|
||||||
self.pointer = pointer
|
|
||||||
tcpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
tcpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, nil)
|
|
||||||
tcpAPI.pointee.release.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func wrapper(for pointer: OpaquePointer?) -> TCPConnection? {
|
|
||||||
guard let pointer,
|
|
||||||
let userdata = tcpAPI.pointee.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
|
|
||||||
return Unmanaged<TCPConnection>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The last error on the connection, if any.
|
|
||||||
public var error: NetError? {
|
|
||||||
Network.optionalError(tcpAPI.pointee.getError.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The time to wait for the connection to open, in milliseconds.
|
|
||||||
public func setConnectTimeout(milliseconds: Int) {
|
|
||||||
tcpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens the connection. The completion receives `nil` on success.
|
|
||||||
public func open(_ completion: @escaping (TCPConnection, NetError?) -> Void) throws(NetError) {
|
|
||||||
openCompletion = completion
|
|
||||||
let error = tcpAPI.pointee.open.unsafelyUnwrapped(pointer, { connection, error, _ in
|
|
||||||
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
|
|
||||||
let completion = wrapper.openCompletion
|
|
||||||
wrapper.openCompletion = nil
|
|
||||||
completion?(wrapper, Network.optionalError(error))
|
|
||||||
}, nil)
|
|
||||||
try Network.check(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Closes the connection.
|
|
||||||
public func close() throws(NetError) {
|
|
||||||
try Network.check(tcpAPI.pointee.close.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when the connection closes, with the reason if it closed
|
|
||||||
/// due to an error.
|
|
||||||
public func setConnectionClosedCallback(_ callback: ((TCPConnection, NetError?) -> Void)?) {
|
|
||||||
connectionClosedCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
tcpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection, error in
|
|
||||||
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
|
|
||||||
wrapper.connectionClosedCallback?(wrapper, Network.optionalError(error))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tcpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The time to wait for incoming data, in milliseconds.
|
|
||||||
public func setReadTimeout(milliseconds: Int) {
|
|
||||||
tcpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The size of the connection's read buffer, in bytes.
|
|
||||||
public func setReadBufferSize(bytes: Int) {
|
|
||||||
tcpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of bytes available to read.
|
|
||||||
public var bytesAvailable: Int {
|
|
||||||
Int(tcpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of written bytes not yet sent on the wire.
|
|
||||||
public var sentBytesPending: Int {
|
|
||||||
Int(tcpAPI.pointee.getSentBytesPending.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads up to `buffer.count` bytes, waiting up to the read timeout.
|
|
||||||
/// Returns the number of bytes read.
|
|
||||||
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
|
|
||||||
let result = tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
|
||||||
if result < 0 {
|
|
||||||
throw NetError(rawValue: result) ?? .unknown
|
|
||||||
}
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads up to `length` bytes, waiting up to the read timeout.
|
|
||||||
public func read(length: Int) throws(NetError) -> [UInt8] {
|
|
||||||
var bytes = [UInt8](repeating: 0, count: length)
|
|
||||||
let result = bytes.withUnsafeMutableBytes { buffer in
|
|
||||||
tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
|
||||||
}
|
|
||||||
if result < 0 {
|
|
||||||
throw NetError(rawValue: result) ?? .unknown
|
|
||||||
}
|
|
||||||
bytes.removeLast(length - Int(result))
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes the buffer to the connection. Returns the number of bytes
|
|
||||||
/// accepted.
|
|
||||||
@discardableResult
|
|
||||||
public func write(_ buffer: UnsafeRawBufferPointer) throws(NetError) -> Int {
|
|
||||||
let result = tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
|
||||||
if result < 0 {
|
|
||||||
throw NetError(rawValue: result) ?? .unknown
|
|
||||||
}
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes the bytes to the connection. Returns the number of bytes
|
|
||||||
/// accepted.
|
|
||||||
@discardableResult
|
|
||||||
public func write(_ bytes: [UInt8]) throws(NetError) -> Int {
|
|
||||||
let result = bytes.withUnsafeBytes { buffer in
|
|
||||||
tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
|
||||||
}
|
|
||||||
if result < 0 {
|
|
||||||
throw NetError(rawValue: result) ?? .unknown
|
|
||||||
}
|
|
||||||
return Int(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
private var httpAPI: UnsafePointer<playdate_http> { Playdate.httpAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
extension Network {
|
||||||
|
/// An HTTP connection to a server. Wraps `HTTPConnection`.
|
||||||
|
public final class HTTPConnection {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
|
||||||
|
var headerReceivedCallback: ((HTTPConnection, _ key: String, _ value: String) -> Void)?
|
||||||
|
var headersReadCallback: ((HTTPConnection) -> Void)?
|
||||||
|
var responseCallback: ((HTTPConnection) -> Void)?
|
||||||
|
var requestCompleteCallback: ((HTTPConnection) -> Void)?
|
||||||
|
var connectionClosedCallback: ((HTTPConnection) -> Void)?
|
||||||
|
|
||||||
|
/// Requests permission to connect to `server`. If the reply is
|
||||||
|
/// `.ask`, the completion is called later with the user's answer.
|
||||||
|
@discardableResult
|
||||||
|
public static func requestAccess(server: String, port: Int = 443, useSSL: Bool = true,
|
||||||
|
purpose: String? = nil,
|
||||||
|
completion: @escaping (Bool) -> Void) -> AccessReply {
|
||||||
|
Network.requestAccess(
|
||||||
|
rawRequest: { httpAPI.pointee.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
|
||||||
|
server: server, port: port, useSSL: useSSL, purpose: purpose,
|
||||||
|
completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a connection to `server`. Fails if access has not been
|
||||||
|
/// granted.
|
||||||
|
public init?(server: String, port: Int = 443, useSSL: Bool = true) {
|
||||||
|
let pointer = server.withPlaydateCString {
|
||||||
|
httpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
|
||||||
|
}
|
||||||
|
guard let pointer else { return nil }
|
||||||
|
self.pointer = pointer
|
||||||
|
httpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
httpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, nil)
|
||||||
|
httpAPI.pointee.release.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func wrapper(for pointer: OpaquePointer?) -> HTTPConnection? {
|
||||||
|
guard let pointer,
|
||||||
|
let userdata = httpAPI.pointee.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
|
||||||
|
return Unmanaged<HTTPConnection>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Configuration
|
||||||
|
|
||||||
|
/// The time to wait for the connection to open, in milliseconds.
|
||||||
|
public func setConnectTimeout(milliseconds: Int) {
|
||||||
|
httpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to keep the connection open after a request completes.
|
||||||
|
public func setKeepAlive(_ keepAlive: Bool) {
|
||||||
|
httpAPI.pointee.setKeepAlive.unsafelyUnwrapped(pointer, keepAlive)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a `Range: bytes=start-end` header to future requests.
|
||||||
|
public func setByteRange(start: Int, end: Int) {
|
||||||
|
httpAPI.pointee.setByteRange.unsafelyUnwrapped(pointer, Int32(start), Int32(end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time to wait for incoming data, in milliseconds.
|
||||||
|
public func setReadTimeout(milliseconds: Int) {
|
||||||
|
httpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The size of the connection's read buffer, in bytes.
|
||||||
|
public func setReadBufferSize(bytes: Int) {
|
||||||
|
httpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Requests
|
||||||
|
|
||||||
|
/// Sends a GET request for `path`. `headers` are raw header lines
|
||||||
|
/// (e.g. "Accept: text/html\r\n").
|
||||||
|
public func get(path: String, headers: String = "") throws(NetError) {
|
||||||
|
let error = path.withPlaydateCString { cPath in
|
||||||
|
headers.withPlaydateCString { cHeaders in
|
||||||
|
httpAPI.pointee.get.unsafelyUnwrapped(pointer, cPath, cHeaders, headers.utf8.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try Network.check(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a POST request for `path` with the given body.
|
||||||
|
public func post(path: String, headers: String = "", body: [UInt8]) throws(NetError) {
|
||||||
|
let error = path.withPlaydateCString { cPath in
|
||||||
|
headers.withPlaydateCString { cHeaders in
|
||||||
|
body.withUnsafeBytes { bodyBuffer in
|
||||||
|
httpAPI.pointee.post.unsafelyUnwrapped(
|
||||||
|
pointer, cPath, cHeaders, headers.utf8.count,
|
||||||
|
bodyBuffer.baseAddress?.assumingMemoryBound(to: CChar.self),
|
||||||
|
bodyBuffer.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try Network.check(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a request with an arbitrary HTTP method.
|
||||||
|
public func query(method: String, path: String, headers: String = "",
|
||||||
|
body: [UInt8] = []) throws(NetError) {
|
||||||
|
let error = method.withPlaydateCString { cMethod in
|
||||||
|
path.withPlaydateCString { cPath in
|
||||||
|
headers.withPlaydateCString { cHeaders in
|
||||||
|
body.withUnsafeBytes { bodyBuffer in
|
||||||
|
httpAPI.pointee.query.unsafelyUnwrapped(
|
||||||
|
pointer, cMethod, cPath, cHeaders, headers.utf8.count,
|
||||||
|
bodyBuffer.baseAddress?.assumingMemoryBound(to: CChar.self),
|
||||||
|
bodyBuffer.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try Network.check(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Response
|
||||||
|
|
||||||
|
/// The last error on the connection, if any.
|
||||||
|
public var error: NetError? {
|
||||||
|
Network.optionalError(httpAPI.pointee.getError.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of bytes read of the current response, and the total
|
||||||
|
/// expected (0 if the response has no Content-Length).
|
||||||
|
public var progress: (read: Int, total: Int) {
|
||||||
|
var read: Int32 = 0, total: Int32 = 0
|
||||||
|
httpAPI.pointee.getProgress.unsafelyUnwrapped(pointer, &read, &total)
|
||||||
|
return (Int(read), Int(total))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The HTTP status code of the response.
|
||||||
|
public var responseStatus: Int {
|
||||||
|
Int(httpAPI.pointee.getResponseStatus.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of response bytes available to read.
|
||||||
|
public var bytesAvailable: Int {
|
||||||
|
Int(httpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads up to `buffer.count` response bytes. Returns the number of
|
||||||
|
/// bytes read.
|
||||||
|
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
|
||||||
|
let result = httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress,
|
||||||
|
UInt32(buffer.count))
|
||||||
|
if result < 0 {
|
||||||
|
throw NetError(rawValue: result) ?? .unknown
|
||||||
|
}
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads up to `length` available response bytes.
|
||||||
|
public func read(length: Int) throws(NetError) -> [UInt8] {
|
||||||
|
var bytes = [UInt8](repeating: 0, count: length)
|
||||||
|
let result = bytes.withUnsafeMutableBytes { buffer in
|
||||||
|
httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count))
|
||||||
|
}
|
||||||
|
if result < 0 {
|
||||||
|
throw NetError(rawValue: result) ?? .unknown
|
||||||
|
}
|
||||||
|
bytes.removeLast(length - Int(result))
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the connection.
|
||||||
|
public func close() {
|
||||||
|
httpAPI.pointee.close.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Callbacks
|
||||||
|
|
||||||
|
/// Called for each header line as it arrives.
|
||||||
|
public func setHeaderReceivedCallback(_ callback: ((HTTPConnection, _ key: String, _ value: String) -> Void)?) {
|
||||||
|
headerReceivedCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
httpAPI.pointee.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, { connection, key, value in
|
||||||
|
guard let wrapper = HTTPConnection.wrapper(for: connection),
|
||||||
|
let key = String(playdateCString: key),
|
||||||
|
let value = String(playdateCString: value) else { return }
|
||||||
|
wrapper.headerReceivedCallback?(wrapper, key, value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
httpAPI.pointee.setHeaderReceivedCallback.unsafelyUnwrapped(pointer, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when all headers have been read.
|
||||||
|
public func setHeadersReadCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
||||||
|
headersReadCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
httpAPI.pointee.setHeadersReadCallback.unsafelyUnwrapped(pointer, { connection in
|
||||||
|
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
||||||
|
wrapper.headersReadCallback?(wrapper)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
httpAPI.pointee.setHeadersReadCallback.unsafelyUnwrapped(pointer, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when response data is available to read.
|
||||||
|
public func setResponseCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
||||||
|
responseCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
httpAPI.pointee.setResponseCallback.unsafelyUnwrapped(pointer, { connection in
|
||||||
|
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
||||||
|
wrapper.responseCallback?(wrapper)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
httpAPI.pointee.setResponseCallback.unsafelyUnwrapped(pointer, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the request finishes.
|
||||||
|
public func setRequestCompleteCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
||||||
|
requestCompleteCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
httpAPI.pointee.setRequestCompleteCallback.unsafelyUnwrapped(pointer, { connection in
|
||||||
|
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
||||||
|
wrapper.requestCompleteCallback?(wrapper)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
httpAPI.pointee.setRequestCompleteCallback.unsafelyUnwrapped(pointer, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the connection closes.
|
||||||
|
public func setConnectionClosedCallback(_ callback: ((HTTPConnection) -> Void)?) {
|
||||||
|
connectionClosedCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
httpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection in
|
||||||
|
guard let wrapper = HTTPConnection.wrapper(for: connection) else { return }
|
||||||
|
wrapper.connectionClosedCallback?(wrapper)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
httpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
private var tcpAPI: UnsafePointer<playdate_tcp> { Playdate.tcpAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
extension Network {
|
||||||
|
/// A TCP connection to a server. Wraps `TCPConnection`.
|
||||||
|
public final class TCPConnection {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
|
||||||
|
var openCompletion: ((TCPConnection, NetError?) -> Void)?
|
||||||
|
var connectionClosedCallback: ((TCPConnection, NetError?) -> Void)?
|
||||||
|
|
||||||
|
/// Requests permission to connect to `server`. If the reply is
|
||||||
|
/// `.ask`, the completion is called later with the user's answer.
|
||||||
|
@discardableResult
|
||||||
|
public static func requestAccess(server: String, port: Int, useSSL: Bool = true,
|
||||||
|
purpose: String? = nil,
|
||||||
|
completion: @escaping (Bool) -> Void) -> AccessReply {
|
||||||
|
Network.requestAccess(
|
||||||
|
rawRequest: { tcpAPI.pointee.requestAccess.unsafelyUnwrapped($0, $1, $2, $3, $4, $5) },
|
||||||
|
server: server, port: port, useSSL: useSSL, purpose: purpose,
|
||||||
|
completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a connection to `server`. Fails if access has not been
|
||||||
|
/// granted. Call `open(_:)` to connect.
|
||||||
|
public init?(server: String, port: Int, useSSL: Bool = true) {
|
||||||
|
let pointer = server.withPlaydateCString {
|
||||||
|
tcpAPI.pointee.newConnection.unsafelyUnwrapped($0, Int32(port), useSSL)
|
||||||
|
}
|
||||||
|
guard let pointer else { return nil }
|
||||||
|
self.pointer = pointer
|
||||||
|
tcpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
tcpAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, nil)
|
||||||
|
tcpAPI.pointee.release.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func wrapper(for pointer: OpaquePointer?) -> TCPConnection? {
|
||||||
|
guard let pointer,
|
||||||
|
let userdata = tcpAPI.pointee.getUserdata.unsafelyUnwrapped(pointer) else { return nil }
|
||||||
|
return Unmanaged<TCPConnection>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last error on the connection, if any.
|
||||||
|
public var error: NetError? {
|
||||||
|
Network.optionalError(tcpAPI.pointee.getError.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time to wait for the connection to open, in milliseconds.
|
||||||
|
public func setConnectTimeout(milliseconds: Int) {
|
||||||
|
tcpAPI.pointee.setConnectTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens the connection. The completion receives `nil` on success.
|
||||||
|
public func open(_ completion: @escaping (TCPConnection, NetError?) -> Void) throws(NetError) {
|
||||||
|
openCompletion = completion
|
||||||
|
let error = tcpAPI.pointee.open.unsafelyUnwrapped(pointer, { connection, error, _ in
|
||||||
|
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
|
||||||
|
let completion = wrapper.openCompletion
|
||||||
|
wrapper.openCompletion = nil
|
||||||
|
completion?(wrapper, Network.optionalError(error))
|
||||||
|
}, nil)
|
||||||
|
try Network.check(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the connection.
|
||||||
|
public func close() throws(NetError) {
|
||||||
|
try Network.check(tcpAPI.pointee.close.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the connection closes, with the reason if it closed
|
||||||
|
/// due to an error.
|
||||||
|
public func setConnectionClosedCallback(_ callback: ((TCPConnection, NetError?) -> Void)?) {
|
||||||
|
connectionClosedCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
tcpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, { connection, error in
|
||||||
|
guard let wrapper = TCPConnection.wrapper(for: connection) else { return }
|
||||||
|
wrapper.connectionClosedCallback?(wrapper, Network.optionalError(error))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
tcpAPI.pointee.setConnectionClosedCallback.unsafelyUnwrapped(pointer, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The time to wait for incoming data, in milliseconds.
|
||||||
|
public func setReadTimeout(milliseconds: Int) {
|
||||||
|
tcpAPI.pointee.setReadTimeout.unsafelyUnwrapped(pointer, Int32(milliseconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The size of the connection's read buffer, in bytes.
|
||||||
|
public func setReadBufferSize(bytes: Int) {
|
||||||
|
tcpAPI.pointee.setReadBufferSize.unsafelyUnwrapped(pointer, Int32(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of bytes available to read.
|
||||||
|
public var bytesAvailable: Int {
|
||||||
|
Int(tcpAPI.pointee.getBytesAvailable.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of written bytes not yet sent on the wire.
|
||||||
|
public var sentBytesPending: Int {
|
||||||
|
Int(tcpAPI.pointee.getSentBytesPending.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads up to `buffer.count` bytes, waiting up to the read timeout.
|
||||||
|
/// Returns the number of bytes read.
|
||||||
|
public func read(into buffer: UnsafeMutableRawBufferPointer) throws(NetError) -> Int {
|
||||||
|
let result = tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
||||||
|
if result < 0 {
|
||||||
|
throw NetError(rawValue: result) ?? .unknown
|
||||||
|
}
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads up to `length` bytes, waiting up to the read timeout.
|
||||||
|
public func read(length: Int) throws(NetError) -> [UInt8] {
|
||||||
|
var bytes = [UInt8](repeating: 0, count: length)
|
||||||
|
let result = bytes.withUnsafeMutableBytes { buffer in
|
||||||
|
tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
||||||
|
}
|
||||||
|
if result < 0 {
|
||||||
|
throw NetError(rawValue: result) ?? .unknown
|
||||||
|
}
|
||||||
|
bytes.removeLast(length - Int(result))
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the buffer to the connection. Returns the number of bytes
|
||||||
|
/// accepted.
|
||||||
|
@discardableResult
|
||||||
|
public func write(_ buffer: UnsafeRawBufferPointer) throws(NetError) -> Int {
|
||||||
|
let result = tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
||||||
|
if result < 0 {
|
||||||
|
throw NetError(rawValue: result) ?? .unknown
|
||||||
|
}
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the bytes to the connection. Returns the number of bytes
|
||||||
|
/// accepted.
|
||||||
|
@discardableResult
|
||||||
|
public func write(_ bytes: [UInt8]) throws(NetError) -> Int {
|
||||||
|
let result = bytes.withUnsafeBytes { buffer in
|
||||||
|
tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count)
|
||||||
|
}
|
||||||
|
if result < 0 {
|
||||||
|
throw NetError(rawValue: result) ?? .unknown
|
||||||
|
}
|
||||||
|
return Int(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Network {
|
||||||
|
/// A network error code (`PDNetErr`).
|
||||||
|
public enum NetError: Int32, Swift.Error, Sendable {
|
||||||
|
case noDevice = -1
|
||||||
|
case busy = -2
|
||||||
|
case writeError = -3
|
||||||
|
case writeBusy = -4
|
||||||
|
case writeTimeout = -5
|
||||||
|
case readError = -6
|
||||||
|
case readBusy = -7
|
||||||
|
case readTimeout = -8
|
||||||
|
case readOverflow = -9
|
||||||
|
case frameError = -10
|
||||||
|
case badResponse = -11
|
||||||
|
case errorResponse = -12
|
||||||
|
case resetTimeout = -13
|
||||||
|
case bufferTooSmall = -14
|
||||||
|
case unexpectedResponse = -15
|
||||||
|
case notConnectedToAP = -16
|
||||||
|
case notImplemented = -17
|
||||||
|
case connectionClosed = -18
|
||||||
|
case unknown = 1
|
||||||
|
|
||||||
|
init(_ error: PDNetErr) {
|
||||||
|
self = NetError(rawValue: Int32(error.rawValue)) ?? .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extension Network {
|
||||||
|
/// The device's wifi status.
|
||||||
|
public enum WifiStatus: UInt32, Sendable {
|
||||||
|
case notConnected = 0
|
||||||
|
case connected = 1
|
||||||
|
/// A connection was attempted but no configured access point was
|
||||||
|
/// available.
|
||||||
|
case notAvailable = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
private var networkAPI: UnsafePointer<playdate_network> { Playdate.networkAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
/// The network API: wifi status, HTTP, and TCP.
|
||||||
|
public enum Network {}
|
||||||
|
|
||||||
|
extension Network {
|
||||||
|
/// Throws unless `error` is `NET_OK`.
|
||||||
|
static func check(_ error: PDNetErr) throws(NetError) {
|
||||||
|
if error != NET_OK {
|
||||||
|
throw NetError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an error code to `nil` (OK) or a `NetError`.
|
||||||
|
static func optionalError(_ error: PDNetErr) -> NetError? {
|
||||||
|
error == NET_OK ? nil : NetError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var status: WifiStatus {
|
||||||
|
WifiStatus(rawValue: UInt32(networkAPI.pointee.getStatus.unsafelyUnwrapped().rawValue)) ?? .notConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the wifi radio on or off. The completion receives `nil` on
|
||||||
|
/// success. Completions of overlapping calls are delivered in call order.
|
||||||
|
public static func setEnabled(_ enabled: Bool, completion: ((NetError?) -> Void)? = nil) {
|
||||||
|
if let completion {
|
||||||
|
setEnabledCompletions.append(completion)
|
||||||
|
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, { error in
|
||||||
|
guard !Network.setEnabledCompletions.isEmpty else { return }
|
||||||
|
let completion = Network.setEnabledCompletions.removeFirst()
|
||||||
|
completion(Network.optionalError(error))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated(unsafe) private static var setEnabledCompletions: [(NetError?) -> Void] = []
|
||||||
|
|
||||||
|
/// Requests permission to connect to `server`. Shared by HTTP and TCP.
|
||||||
|
static func requestAccess(
|
||||||
|
rawRequest: (UnsafePointer<CChar>?, Int32, Bool, UnsafePointer<CChar>?,
|
||||||
|
(@convention(c) (Bool, UnsafeMutableRawPointer?) -> Void)?,
|
||||||
|
UnsafeMutableRawPointer?) -> accessReply,
|
||||||
|
server: String, port: Int, useSSL: Bool, purpose: String?,
|
||||||
|
completion: @escaping (Bool) -> Void) -> AccessReply {
|
||||||
|
final class Box {
|
||||||
|
let body: (Bool) -> Void
|
||||||
|
init(_ body: @escaping (Bool) -> Void) { self.body = body }
|
||||||
|
}
|
||||||
|
let box = Unmanaged.passRetained(Box(completion))
|
||||||
|
let trampoline: @convention(c) (Bool, UnsafeMutableRawPointer?) -> Void = { allowed, userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
Unmanaged<Box>.fromOpaque(userdata).takeRetainedValue().body(allowed)
|
||||||
|
}
|
||||||
|
let reply = server.withPlaydateCString { cServer in
|
||||||
|
if let purpose {
|
||||||
|
return purpose.withPlaydateCString { cPurpose in
|
||||||
|
rawRequest(cServer, Int32(port), useSSL, cPurpose, trampoline, box.toOpaque())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return rawRequest(cServer, Int32(port), useSSL, nil, trampoline, box.toOpaque())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reply != kAccessAsk {
|
||||||
|
// The callback will not be invoked; balance the retain.
|
||||||
|
box.release()
|
||||||
|
}
|
||||||
|
return AccessReply(rawValue: UInt32(reply.rawValue)) ?? .ask
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/// The user's answer to a permission request (microphone, network).
|
||||||
|
public enum AccessReply: UInt32, Sendable {
|
||||||
|
case ask = 0
|
||||||
|
case deny = 1
|
||||||
|
case allow = 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
public import CPlaydate
|
||||||
|
|
||||||
|
/// A Swift view of `PDSystemEvent` with the key code folded into the
|
||||||
|
/// key events.
|
||||||
|
public enum SystemEvent {
|
||||||
|
case initialize
|
||||||
|
case initializeLua
|
||||||
|
case lock
|
||||||
|
case unlock
|
||||||
|
case pause
|
||||||
|
case resume
|
||||||
|
case terminate
|
||||||
|
case keyPressed(keyCode: UInt32)
|
||||||
|
case keyReleased(keyCode: UInt32)
|
||||||
|
case lowPower
|
||||||
|
case mirrorStarted
|
||||||
|
case mirrorEnded
|
||||||
|
|
||||||
|
public init?(event: PDSystemEvent, argument: UInt32) {
|
||||||
|
switch event {
|
||||||
|
case kEventInit: self = .initialize
|
||||||
|
case kEventInitLua: self = .initializeLua
|
||||||
|
case kEventLock: self = .lock
|
||||||
|
case kEventUnlock: self = .unlock
|
||||||
|
case kEventPause: self = .pause
|
||||||
|
case kEventResume: self = .resume
|
||||||
|
case kEventTerminate: self = .terminate
|
||||||
|
case kEventKeyPressed: self = .keyPressed(keyCode: argument)
|
||||||
|
case kEventKeyReleased: self = .keyReleased(keyCode: argument)
|
||||||
|
case kEventLowPower: self = .lowPower
|
||||||
|
case kEventMirrorStarted: self = .mirrorStarted
|
||||||
|
case kEventMirrorEnded: self = .mirrorEnded
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,3 @@
|
|||||||
//
|
|
||||||
// PlaydateKit.swift
|
|
||||||
// Swift bindings to the Playdate C API.
|
|
||||||
//
|
|
||||||
// The C API is delivered as a `PlaydateAPI` struct of function pointers that
|
|
||||||
// the firmware hands to the game's `eventHandler` entry point. Call
|
|
||||||
// `Playdate.initialize(with:)` from that entry point before using any other
|
|
||||||
// API in this module.
|
|
||||||
//
|
|
||||||
|
|
||||||
public import CPlaydate
|
public import CPlaydate
|
||||||
|
|
||||||
/// The raw C API bootstrap. Everything else in this module (System,
|
/// The raw C API bootstrap. Everything else in this module (System,
|
||||||
@@ -111,58 +101,3 @@ public enum Playdate {
|
|||||||
tcpAPI = networkAPI?.pointee.tcp
|
tcpAPI = networkAPI?.pointee.tcp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error reported by the Playdate OS.
|
|
||||||
public struct PlaydateError: Swift.Error, Sendable {
|
|
||||||
public let message: String
|
|
||||||
|
|
||||||
init(message: String) {
|
|
||||||
self.message = message
|
|
||||||
}
|
|
||||||
|
|
||||||
init(cString: UnsafePointer<CChar>?) {
|
|
||||||
self.init(message: String(playdateCString: cString) ?? "unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The user's answer to a permission request (microphone, network).
|
|
||||||
public enum AccessReply: UInt32, Sendable {
|
|
||||||
case ask = 0
|
|
||||||
case deny = 1
|
|
||||||
case allow = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Swift view of `PDSystemEvent` with the key code folded into the
|
|
||||||
/// key events.
|
|
||||||
public enum SystemEvent {
|
|
||||||
case initialize
|
|
||||||
case initializeLua
|
|
||||||
case lock
|
|
||||||
case unlock
|
|
||||||
case pause
|
|
||||||
case resume
|
|
||||||
case terminate
|
|
||||||
case keyPressed(keyCode: UInt32)
|
|
||||||
case keyReleased(keyCode: UInt32)
|
|
||||||
case lowPower
|
|
||||||
case mirrorStarted
|
|
||||||
case mirrorEnded
|
|
||||||
|
|
||||||
public init?(event: PDSystemEvent, argument: UInt32) {
|
|
||||||
switch event {
|
|
||||||
case kEventInit: self = .initialize
|
|
||||||
case kEventInitLua: self = .initializeLua
|
|
||||||
case kEventLock: self = .lock
|
|
||||||
case kEventUnlock: self = .unlock
|
|
||||||
case kEventPause: self = .pause
|
|
||||||
case kEventResume: self = .resume
|
|
||||||
case kEventTerminate: self = .terminate
|
|
||||||
case kEventKeyPressed: self = .keyPressed(keyCode: argument)
|
|
||||||
case kEventKeyReleased: self = .keyReleased(keyCode: argument)
|
|
||||||
case kEventLowPower: self = .lowPower
|
|
||||||
case kEventMirrorStarted: self = .mirrorStarted
|
|
||||||
case kEventMirrorEnded: self = .mirrorEnded
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/// An error reported by the Playdate OS.
|
||||||
|
public struct PlaydateError: Swift.Error, Sendable {
|
||||||
|
public let message: String
|
||||||
|
|
||||||
|
init(message: String) {
|
||||||
|
self.message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
init(cString: UnsafePointer<CChar>?) {
|
||||||
|
self.init(message: String(playdateCString: cString) ?? "unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-85
@@ -1,95 +1,11 @@
|
|||||||
//
|
|
||||||
// Scoreboards.swift
|
|
||||||
// Wraps `playdate->scoreboards` (pd_api_scoreboards.h).
|
|
||||||
//
|
|
||||||
// The C callbacks carry no userdata, so one completion per operation kind
|
|
||||||
// is tracked at a time; starting a second request of the same kind before
|
|
||||||
// the first completes replaces the stored completion.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
private var scoreboardsAPI: UnsafePointer<playdate_scoreboards> { Playdate.scoreboardsAPI.unsafelyUnwrapped }
|
var scoreboardsAPI: UnsafePointer<playdate_scoreboards> { Playdate.scoreboardsAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
/// The scoreboards API for games with online leaderboards.
|
/// The scoreboards API for games with online leaderboards.
|
||||||
public enum Scoreboards {}
|
public enum Scoreboards {}
|
||||||
|
|
||||||
extension Scoreboards {
|
extension Scoreboards {
|
||||||
/// A score on a board.
|
|
||||||
public struct Score {
|
|
||||||
public let rank: UInt32
|
|
||||||
public let value: UInt32
|
|
||||||
public let player: String
|
|
||||||
public let boardID: String?
|
|
||||||
|
|
||||||
init(_ score: PDScore) {
|
|
||||||
rank = score.rank
|
|
||||||
value = score.value
|
|
||||||
player = String(playdateCString: score.player) ?? ""
|
|
||||||
boardID = String(playdateCString: score.boardID)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ score: PDListScore, boardID: String?) {
|
|
||||||
rank = score.rank
|
|
||||||
value = score.value
|
|
||||||
player = String(playdateCString: score.player) ?? ""
|
|
||||||
self.boardID = boardID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The scores on a board.
|
|
||||||
public struct ScoresList {
|
|
||||||
public let boardID: String
|
|
||||||
public let lastUpdated: UInt32
|
|
||||||
public let playerIncluded: Bool
|
|
||||||
public let limit: UInt32
|
|
||||||
public let scores: [Score]
|
|
||||||
|
|
||||||
init(_ list: PDScoresList) {
|
|
||||||
boardID = String(playdateCString: list.boardID) ?? ""
|
|
||||||
lastUpdated = list.lastUpdated
|
|
||||||
playerIncluded = list.playerIncluded != 0
|
|
||||||
limit = list.limit
|
|
||||||
var scores = [Score]()
|
|
||||||
if let entries = list.scores {
|
|
||||||
scores.reserveCapacity(Int(list.count))
|
|
||||||
for index in 0..<Int(list.count) {
|
|
||||||
scores.append(Score(entries[index], boardID: boardID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.scores = scores
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A board belonging to the game.
|
|
||||||
public struct Board {
|
|
||||||
public let boardID: String
|
|
||||||
public let name: String
|
|
||||||
|
|
||||||
init(_ board: PDBoard) {
|
|
||||||
boardID = String(playdateCString: board.boardID) ?? ""
|
|
||||||
name = String(playdateCString: board.name) ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The game's boards.
|
|
||||||
public struct BoardsList {
|
|
||||||
public let lastUpdated: UInt32
|
|
||||||
public let boards: [Board]
|
|
||||||
|
|
||||||
init(_ list: PDBoardsList) {
|
|
||||||
lastUpdated = list.lastUpdated
|
|
||||||
var boards = [Board]()
|
|
||||||
if let entries = list.boards {
|
|
||||||
boards.reserveCapacity(Int(list.count))
|
|
||||||
for index in 0..<Int(list.count) {
|
|
||||||
boards.append(Board(entries[index]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.boards = boards
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated(unsafe) private static var addScoreCompletion: ((Result<Score, PlaydateError>) -> Void)?
|
nonisolated(unsafe) private static var addScoreCompletion: ((Result<Score, PlaydateError>) -> Void)?
|
||||||
nonisolated(unsafe) private static var personalBestCompletion: ((Result<Score, PlaydateError>) -> Void)?
|
nonisolated(unsafe) private static var personalBestCompletion: ((Result<Score, PlaydateError>) -> Void)?
|
||||||
nonisolated(unsafe) private static var boardsCompletion: ((Result<BoardsList, PlaydateError>) -> Void)?
|
nonisolated(unsafe) private static var boardsCompletion: ((Result<BoardsList, PlaydateError>) -> Void)?
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Scoreboards {
|
||||||
|
/// A board belonging to the game.
|
||||||
|
public struct Board {
|
||||||
|
public let boardID: String
|
||||||
|
public let name: String
|
||||||
|
|
||||||
|
init(_ board: PDBoard) {
|
||||||
|
boardID = String(playdateCString: board.boardID) ?? ""
|
||||||
|
name = String(playdateCString: board.name) ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Scoreboards {
|
||||||
|
/// The game's boards.
|
||||||
|
public struct BoardsList {
|
||||||
|
public let lastUpdated: UInt32
|
||||||
|
public let boards: [Board]
|
||||||
|
|
||||||
|
init(_ list: PDBoardsList) {
|
||||||
|
lastUpdated = list.lastUpdated
|
||||||
|
var boards = [Board]()
|
||||||
|
if let entries = list.boards {
|
||||||
|
boards.reserveCapacity(Int(list.count))
|
||||||
|
for index in 0..<Int(list.count) {
|
||||||
|
boards.append(Board(entries[index]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.boards = boards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Scoreboards {
|
||||||
|
/// A score on a board.
|
||||||
|
public struct Score {
|
||||||
|
public let rank: UInt32
|
||||||
|
public let value: UInt32
|
||||||
|
public let player: String
|
||||||
|
public let boardID: String?
|
||||||
|
|
||||||
|
init(_ score: PDScore) {
|
||||||
|
rank = score.rank
|
||||||
|
value = score.value
|
||||||
|
player = String(playdateCString: score.player) ?? ""
|
||||||
|
boardID = String(playdateCString: score.boardID)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ score: PDListScore, boardID: String?) {
|
||||||
|
rank = score.rank
|
||||||
|
value = score.value
|
||||||
|
player = String(playdateCString: score.player) ?? ""
|
||||||
|
self.boardID = boardID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Scoreboards {
|
||||||
|
/// The scores on a board.
|
||||||
|
public struct ScoresList {
|
||||||
|
public let boardID: String
|
||||||
|
public let lastUpdated: UInt32
|
||||||
|
public let playerIncluded: Bool
|
||||||
|
public let limit: UInt32
|
||||||
|
public let scores: [Score]
|
||||||
|
|
||||||
|
init(_ list: PDScoresList) {
|
||||||
|
boardID = String(playdateCString: list.boardID) ?? ""
|
||||||
|
lastUpdated = list.lastUpdated
|
||||||
|
playerIncluded = list.playerIncluded != 0
|
||||||
|
limit = list.limit
|
||||||
|
var scores = [Score]()
|
||||||
|
if let entries = list.scores {
|
||||||
|
scores.reserveCapacity(Int(list.count))
|
||||||
|
for index in 0..<Int(list.count) {
|
||||||
|
scores.append(Score(entries[index], boardID: boardID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.scores = scores
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
extension Sound {
|
||||||
|
/// A note as a MIDI note number, where 60 is middle C. Fractional values
|
||||||
|
/// are valid.
|
||||||
|
public typealias MIDINote = Float
|
||||||
|
}
|
||||||
@@ -1,179 +1,6 @@
|
|||||||
//
|
|
||||||
// Sound.swift
|
|
||||||
// Wraps `playdate->sound` (pd_api_sound.h): the namespace, top-level audio
|
|
||||||
// functions, and SoundChannel. Sources, signals, synths, and effects live in
|
|
||||||
// their own files.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
var snd: UnsafePointer<playdate_sound> { Playdate.soundAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The sound API: channels, players, synths, sequences, and effects.
|
|
||||||
public enum Sound {}
|
|
||||||
|
|
||||||
extension Sound {
|
extension Sound {
|
||||||
/// A note as a MIDI note number, where 60 is middle C. Fractional values
|
|
||||||
/// are valid.
|
|
||||||
public typealias MIDINote = Float
|
|
||||||
|
|
||||||
/// Middle C (`NOTE_C4`).
|
|
||||||
public static let noteC4: MIDINote = 60
|
|
||||||
|
|
||||||
/// The number of audio frames rendered per system audio cycle
|
|
||||||
/// (`AUDIO_FRAMES_PER_CYCLE`).
|
|
||||||
public static let audioFramesPerCycle = 512
|
|
||||||
|
|
||||||
/// Converts a MIDI note to a frequency in Hz.
|
|
||||||
public static func frequency(forNote note: MIDINote) -> Float {
|
|
||||||
pd_noteToFrequency(note)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a frequency in Hz to a MIDI note.
|
|
||||||
public static func note(forFrequency frequency: Float) -> MIDINote {
|
|
||||||
pd_frequencyToNote(frequency)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The format of sample data.
|
|
||||||
public enum Format: UInt32, Sendable {
|
|
||||||
case mono8bit = 0
|
|
||||||
case stereo8bit = 1
|
|
||||||
case mono16bit = 2
|
|
||||||
case stereo16bit = 3
|
|
||||||
case monoADPCM = 4
|
|
||||||
case stereoADPCM = 5
|
|
||||||
|
|
||||||
init(_ format: SoundFormat) { self = Format(rawValue: UInt32(format.rawValue)) ?? .mono16bit }
|
|
||||||
var cValue: SoundFormat { SoundFormat(SoundFormat.RawValue(rawValue)) }
|
|
||||||
|
|
||||||
public var isStereo: Bool { rawValue & 1 != 0 }
|
|
||||||
public var is16bit: Bool { rawValue >= 2 && rawValue < 4 }
|
|
||||||
public var bytesPerFrame: Int { Int(SoundFormat_bytesPerFrame(cValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The microphone used when recording.
|
|
||||||
public enum MicSource: UInt32, Sendable {
|
|
||||||
case autodetect = 0
|
|
||||||
case internalMic = 1
|
|
||||||
case headset = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The most recent sound error as a thrown error.
|
|
||||||
static func lastError() -> PlaydateError {
|
|
||||||
PlaydateError(cString: snd.pointee.getError.unsafelyUnwrapped())
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Top-level functions
|
|
||||||
|
|
||||||
/// The audio engine's current time, in frames (44,100 per second).
|
|
||||||
public static var currentTime: UInt32 {
|
|
||||||
snd.pointee.getCurrentTime.unsafelyUnwrapped()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The most recent audio error message, if any.
|
|
||||||
public static var error: String? {
|
|
||||||
String(playdateCString: snd.pointee.getError.unsafelyUnwrapped())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes a source from its channel.
|
|
||||||
@discardableResult
|
|
||||||
public static func removeSource(_ source: Source) -> Bool {
|
|
||||||
let removed = snd.pointee.removeSource.unsafelyUnwrapped(source.pointer) != 0
|
|
||||||
CallbackSource.release(source)
|
|
||||||
return removed
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a callback that records microphone input. Return `false` from the
|
|
||||||
/// callback to stop recording. Pass `nil` to stop recording immediately.
|
|
||||||
/// The buffer contains mono 16-bit samples.
|
|
||||||
@discardableResult
|
|
||||||
public static func setMicCallback(source: MicSource = .autodetect,
|
|
||||||
_ callback: ((UnsafeMutableBufferPointer<Int16>) -> Bool)?) -> Bool {
|
|
||||||
micCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
return snd.pointee.setMicCallback.unsafelyUnwrapped({ _, buffer, length in
|
|
||||||
let samples = UnsafeMutableBufferPointer(start: buffer, count: Int(length))
|
|
||||||
return Sound.micCallback?(samples) == true ? 1 : 0
|
|
||||||
}, nil, CPlaydate.MicSource(CPlaydate.MicSource.RawValue(source.rawValue))) != 0
|
|
||||||
} else {
|
|
||||||
return snd.pointee.setMicCallback.unsafelyUnwrapped(nil, nil, CPlaydate.MicSource(CPlaydate.MicSource.RawValue(source.rawValue))) != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated(unsafe) private static var micCallback: ((UnsafeMutableBufferPointer<Int16>) -> Bool)?
|
|
||||||
|
|
||||||
/// Asks the user for permission to record from the microphone. `purpose`
|
|
||||||
/// is shown in the permission prompt. The completion receives whether
|
|
||||||
/// access was granted; it is not called if the reply was already
|
|
||||||
/// determined (the returned value is `.deny` or `.allow`).
|
|
||||||
@discardableResult
|
|
||||||
public static func requestMicAccess(purpose: String? = nil,
|
|
||||||
_ completion: @escaping (Bool) -> Void) -> AccessReply {
|
|
||||||
final class Box { let body: (Bool) -> Void; init(_ body: @escaping (Bool) -> Void) { self.body = body } }
|
|
||||||
let box = Unmanaged.passRetained(Box(completion))
|
|
||||||
let trampoline: @convention(c) (Bool, UnsafeMutableRawPointer?) -> Void = { allowed, userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let box = Unmanaged<Box>.fromOpaque(userdata).takeRetainedValue()
|
|
||||||
box.body(allowed)
|
|
||||||
}
|
|
||||||
let reply: accessReply
|
|
||||||
if let purpose {
|
|
||||||
reply = purpose.withPlaydateCString {
|
|
||||||
snd.pointee.requestMicAccess.unsafelyUnwrapped($0, trampoline, box.toOpaque())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reply = snd.pointee.requestMicAccess.unsafelyUnwrapped(nil, trampoline, box.toOpaque())
|
|
||||||
}
|
|
||||||
if reply != kAccessAsk {
|
|
||||||
// The callback will not be invoked; balance the retain.
|
|
||||||
box.release()
|
|
||||||
}
|
|
||||||
return AccessReply(rawValue: UInt32(reply.rawValue)) ?? .ask
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current headphone and headset-microphone state.
|
|
||||||
public static var headphoneState: (headphone: Bool, headsetMic: Bool) {
|
|
||||||
var headphone: Int32 = 0, headsetMic: Int32 = 0
|
|
||||||
snd.pointee.getHeadphoneState.unsafelyUnwrapped(&headphone, &headsetMic, nil)
|
|
||||||
return (headphone != 0, headsetMic != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Installs a callback invoked when the headphone or headset-mic state
|
|
||||||
/// changes.
|
|
||||||
public static func setHeadphoneChangeCallback(_ callback: ((_ headphone: Bool, _ headsetMic: Bool) -> Void)?) {
|
|
||||||
headphoneChangeCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
snd.pointee.getHeadphoneState.unsafelyUnwrapped(nil, nil, { headphone, mic in
|
|
||||||
Sound.headphoneChangeCallback?(headphone != 0, mic != 0)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
snd.pointee.getHeadphoneState.unsafelyUnwrapped(nil, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated(unsafe) private static var headphoneChangeCallback: ((Bool, Bool) -> Void)?
|
|
||||||
|
|
||||||
/// Forces audio output to the headphone and/or speaker. When the
|
|
||||||
/// headphone jack drives output and `speaker` is also set, the speaker
|
|
||||||
/// plays too.
|
|
||||||
public static func setOutputsActive(headphone: Bool, speaker: Bool) {
|
|
||||||
snd.pointee.setOutputsActive.unsafelyUnwrapped(headphone ? 1 : 0, speaker ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a callback-based source to the default channel. The callback
|
|
||||||
/// fills the sample buffers and returns `true` if it produced output.
|
|
||||||
/// Buffers hold 16-bit samples; `right` is non-nil only when `stereo`.
|
|
||||||
public static func addSource(stereo: Bool,
|
|
||||||
_ callback: @escaping CallbackSource.Callback) -> CallbackSource {
|
|
||||||
let source = CallbackSource(callback: callback)
|
|
||||||
let pointer = snd.pointee.addSource.unsafelyUnwrapped(
|
|
||||||
CallbackSource.trampoline, source.contextPointer, stereo ? 1 : 0)
|
|
||||||
source.adopt(pointer: pointer.unsafelyUnwrapped)
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Channels
|
|
||||||
|
|
||||||
/// A mixer channel holding sources and effects. Wraps `SoundChannel`.
|
/// A mixer channel holding sources and effects. Wraps `SoundChannel`.
|
||||||
public final class Channel {
|
public final class Channel {
|
||||||
private static var api: UnsafePointer<playdate_sound_channel> { Playdate.channelAPI.unsafelyUnwrapped }
|
private static var api: UnsafePointer<playdate_sound_channel> { Playdate.channelAPI.unsafelyUnwrapped }
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extension Sound.Effect {
|
||||||
|
/// Processes up to `AUDIO_FRAMES_PER_CYCLE` sample frames in signed
|
||||||
|
/// Q8.24 format. `bufferActive` is `false` when the input buffer is
|
||||||
|
/// silent. Returns `true` if the effect produced output.
|
||||||
|
public typealias Processor = (_ left: UnsafeMutableBufferPointer<Int32>,
|
||||||
|
_ right: UnsafeMutableBufferPointer<Int32>?,
|
||||||
|
_ bufferActive: Bool) -> Bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A bit-crushing and downsampling effect. Wraps `BitCrusher`.
|
||||||
|
public final class BitCrusher: Effect {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_effect_bitcrusher> { Playdate.bitCrusherAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
private var retainedModulators: [SignalValue] = []
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
super.init(pointer: BitCrusher.api.pointee.newBitCrusher.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
BitCrusher.api.pointee.freeBitCrusher.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When `true`, `setDepth` values map exponentially to bit depth.
|
||||||
|
public func setExponential(_ flag: Bool) {
|
||||||
|
BitCrusher.api.pointee.setExponential.unsafelyUnwrapped(pointer, flag)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The amount of crushing, 0 (none) to 1 (quantized to 1 bit).
|
||||||
|
public func setDepth(_ depth: Float) {
|
||||||
|
BitCrusher.api.pointee.setDepth.unsafelyUnwrapped(pointer, depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var depthModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(BitCrusher.api.pointee.getDepthModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retain(newValue)
|
||||||
|
BitCrusher.api.pointee.setDepthModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The amount of downsampling, 0 (none) to 1 (every sample repeated).
|
||||||
|
public func setDownsampling(_ downsampling: Float) {
|
||||||
|
BitCrusher.api.pointee.setDownsampling.unsafelyUnwrapped(pointer, downsampling)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var downsamplingModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(BitCrusher.api.pointee.getDownsamplingModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retain(newValue)
|
||||||
|
BitCrusher.api.pointee.setDownsamplingModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func retain(_ modulator: SignalValue?) {
|
||||||
|
if let modulator { retainedModulators.append(modulator) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A delay line effect. Wraps `DelayLine`.
|
||||||
|
public final class DelayLine: Effect {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_effect_delayline> { Playdate.delayLineAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
/// Creates a delay line holding `length` frames.
|
||||||
|
public init(length: Int, stereo: Bool = false) {
|
||||||
|
super.init(pointer: DelayLine.api.pointee.newDelayLine.unsafelyUnwrapped(
|
||||||
|
Int32(length), stereo ? 1 : 0).unsafelyUnwrapped, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
DelayLine.api.pointee.freeDelayLine.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the delay length. Cannot be larger than the line's
|
||||||
|
/// original length.
|
||||||
|
public func setLength(frames: Int) {
|
||||||
|
DelayLine.api.pointee.setLength.unsafelyUnwrapped(pointer, Int32(frames))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The feedback level, 0...1.
|
||||||
|
public func setFeedback(_ feedback: Float) {
|
||||||
|
DelayLine.api.pointee.setFeedback.unsafelyUnwrapped(pointer, feedback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a tap `delay` frames behind the write head. The tap can be
|
||||||
|
/// added to a channel as a sound source.
|
||||||
|
public func addTap(delay: Int) -> DelayLineTap? {
|
||||||
|
guard let tap = DelayLine.api.pointee.addTap.unsafelyUnwrapped(pointer, Int32(delay)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return DelayLineTap(pointer: tap, delayLine: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A tap into a delay line; produces audio and can be added to a channel
|
||||||
|
/// as a source. Wraps `DelayLineTap`.
|
||||||
|
public final class DelayLineTap: Source {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_effect_delayline> { Playdate.delayLineAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
/// The delay line is retained so the tap stays valid.
|
||||||
|
private let delayLine: DelayLine
|
||||||
|
private var retainedDelayModulator: SignalValue?
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer, delayLine: DelayLine) {
|
||||||
|
self.delayLine = delayLine
|
||||||
|
super.init(pointer: pointer, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
DelayLineTap.api.pointee.freeTap.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tap's position in the delay line, in frames.
|
||||||
|
public func setDelay(frames: Int) {
|
||||||
|
DelayLineTap.api.pointee.setTapDelay.unsafelyUnwrapped(pointer, Int32(frames))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var delayModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(DelayLineTap.api.pointee.getTapDelayModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedDelayModulator = newValue
|
||||||
|
DelayLineTap.api.pointee.setTapDelayModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For stereo delay lines: swaps the left and right channels.
|
||||||
|
public func setChannelsFlipped(_ flipped: Bool) {
|
||||||
|
DelayLineTap.api.pointee.setTapChannelsFlipped.unsafelyUnwrapped(pointer, flipped ? 1 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
private var effectAPI: UnsafePointer<playdate_sound_effect> { Playdate.effectAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// An effect that processes a channel's audio: the base class of the
|
||||||
|
/// built-in effects. Wraps `SoundEffect`.
|
||||||
|
public class Effect {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let isOwned: Bool
|
||||||
|
private var retainedMixModulator: SignalValue?
|
||||||
|
private var processorBox: Unmanaged<ProcessorBox>?
|
||||||
|
|
||||||
|
final class ProcessorBox {
|
||||||
|
let processor: Processor
|
||||||
|
init(_ processor: @escaping Processor) { self.processor = processor }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.isOwned = isOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an effect that processes audio with a Swift callback.
|
||||||
|
public init(processor: @escaping Processor) {
|
||||||
|
let box = Unmanaged.passRetained(ProcessorBox(processor))
|
||||||
|
processorBox = box
|
||||||
|
pointer = effectAPI.pointee.newEffect.unsafelyUnwrapped({ effect, left, right, nsamples, bufactive in
|
||||||
|
guard let effect, let left,
|
||||||
|
let userdata = effectAPI.pointee.getUserdata.unsafelyUnwrapped(effect) else { return 0 }
|
||||||
|
let box = Unmanaged<ProcessorBox>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
let leftBuffer = UnsafeMutableBufferPointer(start: left, count: Int(nsamples))
|
||||||
|
let rightBuffer = right.map { UnsafeMutableBufferPointer(start: $0, count: Int(nsamples)) }
|
||||||
|
return box.processor(leftBuffer, rightBuffer, bufactive != 0) ? 1 : 0
|
||||||
|
}, box.toOpaque()).unsafelyUnwrapped
|
||||||
|
isOwned = true
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
effectAPI.pointee.freeEffect.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
processorBox?.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The wet/dry mix: 1 is fully processed, 0 fully dry.
|
||||||
|
public func setMix(_ level: Float) {
|
||||||
|
effectAPI.pointee.setMix.unsafelyUnwrapped(pointer, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var mixModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(effectAPI.pointee.getMixModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedMixModulator = newValue
|
||||||
|
effectAPI.pointee.setMixModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A one-pole low/high-pass filter. Wraps `OnePoleFilter`.
|
||||||
|
public final class OnePoleFilter: Effect {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_effect_onepolefilter> { Playdate.onePoleFilterAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
private var retainedParameterModulator: SignalValue?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
super.init(pointer: OnePoleFilter.api.pointee.newFilter.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
OnePoleFilter.api.pointee.freeFilter.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The filter's cutoff: -1 to 1, where values above 0 are low-pass
|
||||||
|
/// and values below 0 high-pass.
|
||||||
|
public func setParameter(_ parameter: Float) {
|
||||||
|
OnePoleFilter.api.pointee.setParameter.unsafelyUnwrapped(pointer, parameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var parameterModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(OnePoleFilter.api.pointee.getParameterModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedParameterModulator = newValue
|
||||||
|
OnePoleFilter.api.pointee.setParameterModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// An overdrive/distortion effect. Wraps `Overdrive`.
|
||||||
|
public final class Overdrive: Effect {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_effect_overdrive> { Playdate.overdriveAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
private var retainedModulators: [SignalValue] = []
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
super.init(pointer: Overdrive.api.pointee.newOverdrive.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
Overdrive.api.pointee.freeOverdrive.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The input gain applied before clipping.
|
||||||
|
public func setGain(_ gain: Float) {
|
||||||
|
Overdrive.api.pointee.setGain.unsafelyUnwrapped(pointer, gain)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level where the amplified input clips.
|
||||||
|
public func setLimit(_ limit: Float) {
|
||||||
|
Overdrive.api.pointee.setLimit.unsafelyUnwrapped(pointer, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var limitModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(Overdrive.api.pointee.getLimitModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retain(newValue)
|
||||||
|
Overdrive.api.pointee.setLimitModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A DC offset applied to the input, making the clipping asymmetric.
|
||||||
|
public func setOffset(_ offset: Float) {
|
||||||
|
Overdrive.api.pointee.setOffset.unsafelyUnwrapped(pointer, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var offsetModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(Overdrive.api.pointee.getOffsetModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retain(newValue)
|
||||||
|
Overdrive.api.pointee.setOffsetModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func retain(_ modulator: SignalValue?) {
|
||||||
|
if let modulator { retainedModulators.append(modulator) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A ring modulator effect. Wraps `RingModulator`.
|
||||||
|
public final class RingModulator: Effect {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_effect_ringmodulator> { Playdate.ringModulatorAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
private var retainedFrequencyModulator: SignalValue?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
super.init(pointer: RingModulator.api.pointee.newRingmod.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
RingModulator.api.pointee.freeRingmod.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The modulation frequency, in Hz.
|
||||||
|
public func setFrequency(_ frequency: Float) {
|
||||||
|
RingModulator.api.pointee.setFrequency.unsafelyUnwrapped(pointer, frequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var frequencyModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(RingModulator.api.pointee.getFrequencyModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedFrequencyModulator = newValue
|
||||||
|
RingModulator.api.pointee.setFrequencyModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A two-pole IIR filter. Wraps `TwoPoleFilter`.
|
||||||
|
public final class TwoPoleFilter: Effect {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_effect_twopolefilter> { Playdate.twoPoleFilterAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
private var retainedFrequencyModulator: SignalValue?
|
||||||
|
private var retainedResonanceModulator: SignalValue?
|
||||||
|
|
||||||
|
public init(kind: Kind = .lowPass) {
|
||||||
|
super.init(pointer: TwoPoleFilter.api.pointee.newFilter.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
setKind(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
TwoPoleFilter.api.pointee.freeFilter.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setKind(_ kind: Kind) {
|
||||||
|
TwoPoleFilter.api.pointee.setType.unsafelyUnwrapped(pointer, kind.cValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The center/corner frequency, in Hz.
|
||||||
|
public func setFrequency(_ frequency: Float) {
|
||||||
|
TwoPoleFilter.api.pointee.setFrequency.unsafelyUnwrapped(pointer, frequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var frequencyModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(TwoPoleFilter.api.pointee.getFrequencyModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedFrequencyModulator = newValue
|
||||||
|
TwoPoleFilter.api.pointee.setFrequencyModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The gain, used by PEQ and shelf filters.
|
||||||
|
public func setGain(_ gain: Float) {
|
||||||
|
TwoPoleFilter.api.pointee.setGain.unsafelyUnwrapped(pointer, gain)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setResonance(_ resonance: Float) {
|
||||||
|
TwoPoleFilter.api.pointee.setResonance.unsafelyUnwrapped(pointer, resonance)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var resonanceModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(TwoPoleFilter.api.pointee.getResonanceModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedResonanceModulator = newValue
|
||||||
|
TwoPoleFilter.api.pointee.setResonanceModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound.TwoPoleFilter {
|
||||||
|
public enum Kind: UInt32, Sendable {
|
||||||
|
case lowPass = 0
|
||||||
|
case highPass = 1
|
||||||
|
case bandPass = 2
|
||||||
|
case notch = 3
|
||||||
|
case peq = 4
|
||||||
|
case lowShelf = 5
|
||||||
|
case highShelf = 6
|
||||||
|
|
||||||
|
var cValue: TwoPoleFilterType { TwoPoleFilterType(TwoPoleFilterType.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// The format of sample data.
|
||||||
|
public enum Format: UInt32, Sendable {
|
||||||
|
case mono8bit = 0
|
||||||
|
case stereo8bit = 1
|
||||||
|
case mono16bit = 2
|
||||||
|
case stereo16bit = 3
|
||||||
|
case monoADPCM = 4
|
||||||
|
case stereoADPCM = 5
|
||||||
|
|
||||||
|
init(_ format: SoundFormat) { self = Format(rawValue: UInt32(format.rawValue)) ?? .mono16bit }
|
||||||
|
var cValue: SoundFormat { SoundFormat(SoundFormat.RawValue(rawValue)) }
|
||||||
|
|
||||||
|
public var isStereo: Bool { rawValue & 1 != 0 }
|
||||||
|
public var is16bit: Bool { rawValue >= 2 && rawValue < 4 }
|
||||||
|
public var bytesPerFrame: Int { Int(SoundFormat_bytesPerFrame(cValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extension Sound {
|
||||||
|
/// The microphone used when recording.
|
||||||
|
public enum MicSource: UInt32, Sendable {
|
||||||
|
case autodetect = 0
|
||||||
|
case internalMic = 1
|
||||||
|
case headset = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A signal whose values are set on a sequence timeline. Wraps
|
||||||
|
/// `ControlSignal`.
|
||||||
|
public final class ControlSignal: SignalValue {
|
||||||
|
private static var api: UnsafePointer<playdate_control_signal> { Playdate.controlSignalAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
let pointer = ControlSignal.api.pointee.newSignal.unsafelyUnwrapped()
|
||||||
|
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
super.init(pointer: pointer, isOwned: isOwned)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
ControlSignal.api.pointee.freeSignal.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func clearEvents() {
|
||||||
|
ControlSignal.api.pointee.clearEvents.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a value at `step` in the signal's timeline. If `interpolate`
|
||||||
|
/// is `true`, the value ramps from the previous event.
|
||||||
|
public func addEvent(step: Int, value: Float, interpolate: Bool = false) {
|
||||||
|
ControlSignal.api.pointee.addEvent.unsafelyUnwrapped(pointer, Int32(step), value,
|
||||||
|
interpolate ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func removeEvent(step: Int) {
|
||||||
|
ControlSignal.api.pointee.removeEvent.unsafelyUnwrapped(pointer, Int32(step))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The MIDI controller number for signals loaded from a MIDI file.
|
||||||
|
public var midiControllerNumber: Int {
|
||||||
|
Int(ControlSignal.api.pointee.getMIDIControllerNumber.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// An ADSR envelope signal. Wraps `PDSynthEnvelope`.
|
||||||
|
public final class Envelope: SignalValue {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_envelope> { Playdate.envelopeAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
/// Creates an envelope with the given attack and decay times
|
||||||
|
/// (seconds), sustain level (0...1), and release time (seconds).
|
||||||
|
public init(attack: Float = 0, decay: Float = 0, sustain: Float = 1, release: Float = 0) {
|
||||||
|
let pointer = Envelope.api.pointee.newEnvelope.unsafelyUnwrapped(attack, decay, sustain, release)
|
||||||
|
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
super.init(pointer: pointer, isOwned: isOwned)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
Envelope.api.pointee.freeEnvelope.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setAttack(_ attack: Float) {
|
||||||
|
Envelope.api.pointee.setAttack.unsafelyUnwrapped(pointer, attack)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setDecay(_ decay: Float) {
|
||||||
|
Envelope.api.pointee.setDecay.unsafelyUnwrapped(pointer, decay)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setSustain(_ sustain: Float) {
|
||||||
|
Envelope.api.pointee.setSustain.unsafelyUnwrapped(pointer, sustain)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setRelease(_ release: Float) {
|
||||||
|
Envelope.api.pointee.setRelease.unsafelyUnwrapped(pointer, release)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When `true`, a new note while a note is playing does not restart
|
||||||
|
/// the envelope.
|
||||||
|
public func setLegato(_ flag: Bool) {
|
||||||
|
Envelope.api.pointee.setLegato.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When `true`, a new note restarts the envelope from zero instead of
|
||||||
|
/// its current value.
|
||||||
|
public func setRetrigger(_ flag: Bool) {
|
||||||
|
Envelope.api.pointee.setRetrigger.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bends the envelope's segments: 0 is linear, 1 is maximum curvature.
|
||||||
|
public func setCurvature(_ amount: Float) {
|
||||||
|
Envelope.api.pointee.setCurvature.unsafelyUnwrapped(pointer, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much note velocity scales the envelope's output.
|
||||||
|
public func setVelocitySensitivity(_ sensitivity: Float) {
|
||||||
|
Envelope.api.pointee.setVelocitySensitivity.unsafelyUnwrapped(pointer, sensitivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scales the envelope's rate by note: notes above `start` play the
|
||||||
|
/// envelope faster (up to `scaling` at `end` and beyond).
|
||||||
|
public func setRateScaling(_ scaling: Float, start: MIDINote, end: MIDINote) {
|
||||||
|
Envelope.api.pointee.setRateScaling.unsafelyUnwrapped(pointer, scaling, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var value: Float {
|
||||||
|
Envelope.api.pointee.getValue.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A low-frequency oscillator signal. Wraps `PDSynthLFO`.
|
||||||
|
public final class LFO: SignalValue {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_lfo> { Playdate.lfoAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
var function: ((LFO) -> Float)?
|
||||||
|
|
||||||
|
public init(shape: Shape = .sine) {
|
||||||
|
let pointer = LFO.api.pointee.newLFO.unsafelyUnwrapped(shape.cValue)
|
||||||
|
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
LFO.api.pointee.freeLFO.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setShape(_ shape: Shape) {
|
||||||
|
LFO.api.pointee.setType.unsafelyUnwrapped(pointer, shape.cValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The LFO rate, in cycles per second.
|
||||||
|
public func setRate(_ rate: Float) {
|
||||||
|
LFO.api.pointee.setRate.unsafelyUnwrapped(pointer, rate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current phase, 0...1.
|
||||||
|
public func setPhase(_ phase: Float) {
|
||||||
|
LFO.api.pointee.setPhase.unsafelyUnwrapped(pointer, phase)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The phase the LFO starts at when a note starts, 0...1.
|
||||||
|
public func setStartPhase(_ phase: Float) {
|
||||||
|
LFO.api.pointee.setStartPhase.unsafelyUnwrapped(pointer, phase)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The center value of the LFO output.
|
||||||
|
public func setCenter(_ center: Float) {
|
||||||
|
LFO.api.pointee.setCenter.unsafelyUnwrapped(pointer, center)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The amplitude of the LFO around its center.
|
||||||
|
public func setDepth(_ depth: Float) {
|
||||||
|
LFO.api.pointee.setDepth.unsafelyUnwrapped(pointer, depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For `.arpeggiator` LFOs: the sequence of values (in half-steps)
|
||||||
|
/// to step through.
|
||||||
|
public func setArpeggiation(_ steps: [Float]) {
|
||||||
|
var steps = steps
|
||||||
|
steps.withUnsafeMutableBufferPointer { buffer in
|
||||||
|
LFO.api.pointee.setArpeggiation.unsafelyUnwrapped(pointer, Int32(buffer.count),
|
||||||
|
buffer.baseAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For `.function` LFOs: the Swift function providing the value. If
|
||||||
|
/// `interpolate` is `true`, values are interpolated between calls.
|
||||||
|
public func setFunction(interpolate: Bool = false, _ function: @escaping (LFO) -> Float) {
|
||||||
|
self.function = function
|
||||||
|
LFO.api.pointee.setFunction.unsafelyUnwrapped(pointer, { _, userdata in
|
||||||
|
guard let userdata else { return 0 }
|
||||||
|
let lfo = Unmanaged<LFO>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
return lfo.function?(lfo) ?? 0
|
||||||
|
}, Unmanaged.passUnretained(self).toOpaque(), interpolate ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits `holdoff` seconds after a note starts, then ramps the LFO
|
||||||
|
/// depth up over `rampTime` seconds.
|
||||||
|
public func setDelay(holdoff: Float, rampTime: Float) {
|
||||||
|
LFO.api.pointee.setDelay.unsafelyUnwrapped(pointer, holdoff, rampTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the LFO phase restarts on every new note.
|
||||||
|
public func setRetrigger(_ flag: Bool) {
|
||||||
|
LFO.api.pointee.setRetrigger.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When `true`, the LFO runs globally instead of per-note.
|
||||||
|
public func setGlobal(_ global: Bool) {
|
||||||
|
LFO.api.pointee.setGlobal.unsafelyUnwrapped(pointer, global ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seeds the random number generator used by `.sampleAndHold` LFOs.
|
||||||
|
public func setRandomSeed(_ seed: UInt16) {
|
||||||
|
LFO.api.pointee.setRandomSeed.unsafelyUnwrapped(pointer, seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var value: Float {
|
||||||
|
LFO.api.pointee.getValue.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A signal object; also provides custom signals driven by Swift
|
||||||
|
/// callbacks. Wraps `PDSynthSignal`.
|
||||||
|
public final class Signal: SignalValue {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_signal> { Playdate.signalAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
private final class Box {
|
||||||
|
let callbacks: Callbacks
|
||||||
|
init(_ callbacks: Callbacks) { self.callbacks = callbacks }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a signal driven by the given callbacks.
|
||||||
|
public init(callbacks: Callbacks) {
|
||||||
|
let box = Unmanaged.passRetained(Box(callbacks))
|
||||||
|
let pointer = Signal.api.pointee.newSignal.unsafelyUnwrapped(
|
||||||
|
{ userdata, ioFrames, interpolationValue in
|
||||||
|
guard let userdata else { return 0 }
|
||||||
|
let box = Unmanaged<Box>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
return box.callbacks.step(ioFrames, interpolationValue)
|
||||||
|
},
|
||||||
|
{ userdata, note, velocity, length in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let box = Unmanaged<Box>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
box.callbacks.noteOn?(note, velocity, length)
|
||||||
|
},
|
||||||
|
{ userdata, stopped, offset in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let box = Unmanaged<Box>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
box.callbacks.noteOff?(stopped != 0, Int(offset))
|
||||||
|
},
|
||||||
|
{ userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
Unmanaged<Box>.fromOpaque(userdata).release()
|
||||||
|
},
|
||||||
|
box.toOpaque())
|
||||||
|
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a plain signal object wrapping an existing signal value,
|
||||||
|
/// so it can be scaled and offset.
|
||||||
|
public init(value: SignalValue) {
|
||||||
|
let pointer = Signal.api.pointee.newSignalForValue.unsafelyUnwrapped(value.pointer)
|
||||||
|
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
super.init(pointer: pointer, isOwned: isOwned)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
Signal.api.pointee.freeSignal.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The signal's current value.
|
||||||
|
public var value: Float {
|
||||||
|
Signal.api.pointee.getValue.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scales the signal's output.
|
||||||
|
public func setValueScale(_ scale: Float) {
|
||||||
|
Signal.api.pointee.setValueScale.unsafelyUnwrapped(pointer, scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offsets the signal's output.
|
||||||
|
public func setValueOffset(_ offset: Float) {
|
||||||
|
Signal.api.pointee.setValueOffset.unsafelyUnwrapped(pointer, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
extension Sound {
|
||||||
|
/// A value that can modulate a parameter. The base class of `Signal`,
|
||||||
|
/// `LFO`, `Envelope`, and `ControlSignal`. Wraps `PDSynthSignalValue`.
|
||||||
|
public class SignalValue {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let isOwned: Bool
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.isOwned = isOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a signal value pointer returned by the OS (not owned).
|
||||||
|
static func wrap(_ pointer: OpaquePointer?) -> SignalValue? {
|
||||||
|
guard let pointer else { return nil }
|
||||||
|
return SignalValue(pointer: pointer, isOwned: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound.LFO {
|
||||||
|
/// The oscillator's waveform.
|
||||||
|
public enum Shape: UInt32, Sendable {
|
||||||
|
case square = 0
|
||||||
|
case triangle = 1
|
||||||
|
case sine = 2
|
||||||
|
case sampleAndHold = 3
|
||||||
|
case sawtoothUp = 4
|
||||||
|
case sawtoothDown = 5
|
||||||
|
case arpeggiator = 6
|
||||||
|
case function = 7
|
||||||
|
|
||||||
|
var cValue: LFOType { LFOType(LFOType.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
extension Sound.Signal {
|
||||||
|
/// Custom signal callbacks.
|
||||||
|
public struct Callbacks {
|
||||||
|
/// Returns the signal's value at the end of the current cycle.
|
||||||
|
/// `ioFrames` is the number of frames until the cycle ends and
|
||||||
|
/// may be lowered to interpolate toward `interpolationValue`.
|
||||||
|
public var step: (_ ioFrames: UnsafeMutablePointer<Int32>?,
|
||||||
|
_ interpolationValue: UnsafeMutablePointer<Float>?) -> Float
|
||||||
|
/// Called on note-on events. `length` is -1 for indefinite notes.
|
||||||
|
public var noteOn: ((_ note: Sound.MIDINote, _ velocity: Float, _ length: Float) -> Void)?
|
||||||
|
/// Called on note-off events. `stopped` is `false` when the note
|
||||||
|
/// is released and `true` when it actually stops playing;
|
||||||
|
/// `offset` is the frame offset within the current cycle.
|
||||||
|
public var noteOff: ((_ stopped: Bool, _ offset: Int) -> Void)?
|
||||||
|
|
||||||
|
public init(step: @escaping (_ ioFrames: UnsafeMutablePointer<Int32>?,
|
||||||
|
_ interpolationValue: UnsafeMutablePointer<Float>?) -> Float,
|
||||||
|
noteOn: ((_ note: Sound.MIDINote, _ velocity: Float, _ length: Float) -> Void)? = nil,
|
||||||
|
noteOff: ((_ stopped: Bool, _ offset: Int) -> Void)? = nil) {
|
||||||
|
self.step = step
|
||||||
|
self.noteOn = noteOn
|
||||||
|
self.noteOff = noteOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
var snd: UnsafePointer<playdate_sound> { Playdate.soundAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
/// The sound API: channels, players, synths, sequences, and effects.
|
||||||
|
public enum Sound {}
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// Middle C (`NOTE_C4`).
|
||||||
|
public static let noteC4: MIDINote = 60
|
||||||
|
|
||||||
|
/// The number of audio frames rendered per system audio cycle
|
||||||
|
/// (`AUDIO_FRAMES_PER_CYCLE`).
|
||||||
|
public static let audioFramesPerCycle = 512
|
||||||
|
|
||||||
|
/// Converts a MIDI note to a frequency in Hz.
|
||||||
|
public static func frequency(forNote note: MIDINote) -> Float {
|
||||||
|
pd_noteToFrequency(note)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a frequency in Hz to a MIDI note.
|
||||||
|
public static func note(forFrequency frequency: Float) -> MIDINote {
|
||||||
|
pd_frequencyToNote(frequency)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The most recent sound error as a thrown error.
|
||||||
|
static func lastError() -> PlaydateError {
|
||||||
|
PlaydateError(cString: snd.pointee.getError.unsafelyUnwrapped())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Top-level functions
|
||||||
|
|
||||||
|
/// The audio engine's current time, in frames (44,100 per second).
|
||||||
|
public static var currentTime: UInt32 {
|
||||||
|
snd.pointee.getCurrentTime.unsafelyUnwrapped()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The most recent audio error message, if any.
|
||||||
|
public static var error: String? {
|
||||||
|
String(playdateCString: snd.pointee.getError.unsafelyUnwrapped())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a source from its channel.
|
||||||
|
@discardableResult
|
||||||
|
public static func removeSource(_ source: Source) -> Bool {
|
||||||
|
let removed = snd.pointee.removeSource.unsafelyUnwrapped(source.pointer) != 0
|
||||||
|
CallbackSource.release(source)
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a callback that records microphone input. Return `false` from the
|
||||||
|
/// callback to stop recording. Pass `nil` to stop recording immediately.
|
||||||
|
/// The buffer contains mono 16-bit samples.
|
||||||
|
@discardableResult
|
||||||
|
public static func setMicCallback(source: MicSource = .autodetect,
|
||||||
|
_ callback: ((UnsafeMutableBufferPointer<Int16>) -> Bool)?) -> Bool {
|
||||||
|
micCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
return snd.pointee.setMicCallback.unsafelyUnwrapped({ _, buffer, length in
|
||||||
|
let samples = UnsafeMutableBufferPointer(start: buffer, count: Int(length))
|
||||||
|
return Sound.micCallback?(samples) == true ? 1 : 0
|
||||||
|
}, nil, CPlaydate.MicSource(CPlaydate.MicSource.RawValue(source.rawValue))) != 0
|
||||||
|
} else {
|
||||||
|
return snd.pointee.setMicCallback.unsafelyUnwrapped(nil, nil, CPlaydate.MicSource(CPlaydate.MicSource.RawValue(source.rawValue))) != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated(unsafe) private static var micCallback: ((UnsafeMutableBufferPointer<Int16>) -> Bool)?
|
||||||
|
|
||||||
|
/// Asks the user for permission to record from the microphone. `purpose`
|
||||||
|
/// is shown in the permission prompt. The completion receives whether
|
||||||
|
/// access was granted; it is not called if the reply was already
|
||||||
|
/// determined (the returned value is `.deny` or `.allow`).
|
||||||
|
@discardableResult
|
||||||
|
public static func requestMicAccess(purpose: String? = nil,
|
||||||
|
_ completion: @escaping (Bool) -> Void) -> AccessReply {
|
||||||
|
final class Box { let body: (Bool) -> Void; init(_ body: @escaping (Bool) -> Void) { self.body = body } }
|
||||||
|
let box = Unmanaged.passRetained(Box(completion))
|
||||||
|
let trampoline: @convention(c) (Bool, UnsafeMutableRawPointer?) -> Void = { allowed, userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let box = Unmanaged<Box>.fromOpaque(userdata).takeRetainedValue()
|
||||||
|
box.body(allowed)
|
||||||
|
}
|
||||||
|
let reply: accessReply
|
||||||
|
if let purpose {
|
||||||
|
reply = purpose.withPlaydateCString {
|
||||||
|
snd.pointee.requestMicAccess.unsafelyUnwrapped($0, trampoline, box.toOpaque())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reply = snd.pointee.requestMicAccess.unsafelyUnwrapped(nil, trampoline, box.toOpaque())
|
||||||
|
}
|
||||||
|
if reply != kAccessAsk {
|
||||||
|
// The callback will not be invoked; balance the retain.
|
||||||
|
box.release()
|
||||||
|
}
|
||||||
|
return AccessReply(rawValue: UInt32(reply.rawValue)) ?? .ask
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current headphone and headset-microphone state.
|
||||||
|
public static var headphoneState: (headphone: Bool, headsetMic: Bool) {
|
||||||
|
var headphone: Int32 = 0, headsetMic: Int32 = 0
|
||||||
|
snd.pointee.getHeadphoneState.unsafelyUnwrapped(&headphone, &headsetMic, nil)
|
||||||
|
return (headphone != 0, headsetMic != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs a callback invoked when the headphone or headset-mic state
|
||||||
|
/// changes.
|
||||||
|
public static func setHeadphoneChangeCallback(_ callback: ((_ headphone: Bool, _ headsetMic: Bool) -> Void)?) {
|
||||||
|
headphoneChangeCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
snd.pointee.getHeadphoneState.unsafelyUnwrapped(nil, nil, { headphone, mic in
|
||||||
|
Sound.headphoneChangeCallback?(headphone != 0, mic != 0)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
snd.pointee.getHeadphoneState.unsafelyUnwrapped(nil, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonisolated(unsafe) private static var headphoneChangeCallback: ((Bool, Bool) -> Void)?
|
||||||
|
|
||||||
|
/// Forces audio output to the headphone and/or speaker. When the
|
||||||
|
/// headphone jack drives output and `speaker` is also set, the speaker
|
||||||
|
/// plays too.
|
||||||
|
public static func setOutputsActive(headphone: Bool, speaker: Bool) {
|
||||||
|
snd.pointee.setOutputsActive.unsafelyUnwrapped(headphone ? 1 : 0, speaker ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a callback-based source to the default channel. The callback
|
||||||
|
/// fills the sample buffers and returns `true` if it produced output.
|
||||||
|
/// Buffers hold 16-bit samples; `right` is non-nil only when `stereo`.
|
||||||
|
public static func addSource(stereo: Bool,
|
||||||
|
_ callback: @escaping CallbackSource.Callback) -> CallbackSource {
|
||||||
|
let source = CallbackSource(callback: callback)
|
||||||
|
let pointer = snd.pointee.addSource.unsafelyUnwrapped(
|
||||||
|
CallbackSource.trampoline, source.contextPointer, stereo ? 1 : 0)
|
||||||
|
source.adopt(pointer: pointer.unsafelyUnwrapped)
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
extension Sound.CallbackSource {
|
||||||
|
/// Fills the sample buffers and returns `true` if output was
|
||||||
|
/// produced. `right` is non-nil only for stereo sources.
|
||||||
|
public typealias Callback = (_ left: UnsafeMutableBufferPointer<Int16>,
|
||||||
|
_ right: UnsafeMutableBufferPointer<Int16>?) -> Bool
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// Audio data loaded into memory. Wraps `AudioSample`.
|
||||||
|
public final class AudioSample {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_sample> { Playdate.sampleAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let isOwned: Bool
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.isOwned = isOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocates a sample buffer with room for `byteCount` bytes.
|
||||||
|
public convenience init(byteCount: Int) {
|
||||||
|
self.init(pointer: AudioSample.api.pointee.newSampleBuffer.unsafelyUnwrapped(
|
||||||
|
Int32(byteCount)).unsafelyUnwrapped, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the wav or aiff file at `path`.
|
||||||
|
public convenience init(path: String) throws(PlaydateError) {
|
||||||
|
let pointer = path.withPlaydateCString { AudioSample.api.pointee.load.unsafelyUnwrapped($0) }
|
||||||
|
guard let pointer else {
|
||||||
|
throw PlaydateError(message: "unable to load sample: \(path)")
|
||||||
|
}
|
||||||
|
self.init(pointer: pointer, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a sample referencing existing sample data. If
|
||||||
|
/// `freeWhenDone` is `true`, the OS frees `data` when the sample is
|
||||||
|
/// freed; otherwise the caller must keep `data` valid for the
|
||||||
|
/// sample's lifetime.
|
||||||
|
public convenience init?(data: UnsafeMutablePointer<UInt8>, format: Format,
|
||||||
|
sampleRate: UInt32, byteCount: Int, freeWhenDone: Bool) {
|
||||||
|
guard let pointer = AudioSample.api.pointee.newSampleFromData.unsafelyUnwrapped(
|
||||||
|
data, format.cValue, sampleRate, Int32(byteCount), freeWhenDone ? 1 : 0) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.init(pointer: pointer, isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
AudioSample.api.pointee.freeSample.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the file at `path` into this sample's buffer.
|
||||||
|
public func load(path: String) throws(PlaydateError) {
|
||||||
|
let loaded = path.withPlaydateCString {
|
||||||
|
AudioSample.api.pointee.loadIntoSample.unsafelyUnwrapped(pointer, $0) != 0
|
||||||
|
}
|
||||||
|
if !loaded {
|
||||||
|
throw PlaydateError(message: "unable to load sample: \(path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sample's raw data, format, and rate.
|
||||||
|
public var data: (data: UnsafeMutablePointer<UInt8>?, format: Format,
|
||||||
|
sampleRate: UInt32, byteLength: UInt32) {
|
||||||
|
var data: UnsafeMutablePointer<UInt8>?
|
||||||
|
var format = kSound16bitMono
|
||||||
|
var sampleRate: UInt32 = 0, byteLength: UInt32 = 0
|
||||||
|
AudioSample.api.pointee.getData.unsafelyUnwrapped(pointer, &data, &format, &sampleRate, &byteLength)
|
||||||
|
return (data, Format(format), sampleRate, byteLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sample's length in seconds.
|
||||||
|
public var length: Float {
|
||||||
|
AudioSample.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decompresses an ADPCM sample to 16-bit PCM so it can be used in a
|
||||||
|
/// synth. Returns `false` if there is not enough memory.
|
||||||
|
@discardableResult
|
||||||
|
public func decompress() -> Bool {
|
||||||
|
AudioSample.api.pointee.decompress.unsafelyUnwrapped(pointer) != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
extension Sound {
|
||||||
|
/// A source that produces audio by calling back into Swift.
|
||||||
|
public final class CallbackSource: Source {
|
||||||
|
let callback: Callback
|
||||||
|
|
||||||
|
/// Every callback source is kept alive here while the C side may
|
||||||
|
/// still invoke its trampoline: from creation until it is removed
|
||||||
|
/// with `Sound.removeSource`/`Channel.removeSource`, or until its
|
||||||
|
/// owning channel is freed.
|
||||||
|
nonisolated(unsafe) static var live: [CallbackSource] = []
|
||||||
|
|
||||||
|
/// Releases the registration added by `adopt(pointer:)`.
|
||||||
|
static func release(_ source: Source) {
|
||||||
|
live.removeAll { $0 === source }
|
||||||
|
}
|
||||||
|
|
||||||
|
init(callback: @escaping Callback) {
|
||||||
|
self.callback = callback
|
||||||
|
super.init(pointer: nil, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextPointer: UnsafeMutableRawPointer {
|
||||||
|
Unmanaged.passUnretained(self).toOpaque()
|
||||||
|
}
|
||||||
|
|
||||||
|
static let trampoline: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutablePointer<Int16>?,
|
||||||
|
UnsafeMutablePointer<Int16>?, Int32) -> Int32 = { context, left, right, length in
|
||||||
|
guard let context, let left else { return 0 }
|
||||||
|
let source = Unmanaged<CallbackSource>.fromOpaque(context).takeUnretainedValue()
|
||||||
|
let leftBuffer = UnsafeMutableBufferPointer(start: left, count: Int(length))
|
||||||
|
let rightBuffer = right.map { UnsafeMutableBufferPointer(start: $0, count: Int(length)) }
|
||||||
|
return source.callback(leftBuffer, rightBuffer) ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches the C object created for this source.
|
||||||
|
func adopt(pointer: OpaquePointer) {
|
||||||
|
self.pointer = pointer
|
||||||
|
CallbackSource.live.append(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// Streams audio from a file. Wraps `FilePlayer`.
|
||||||
|
public final class FilePlayer: Source {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_fileplayer> { Playdate.filePlayerAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
var loopCallback: ((FilePlayer) -> Void)?
|
||||||
|
var fadeCallback: ((FilePlayer) -> Void)?
|
||||||
|
var mp3DataSource: ((UnsafeMutableBufferPointer<UInt8>) -> Int)?
|
||||||
|
private var retainedRateModulator: SignalValue?
|
||||||
|
|
||||||
|
override init(pointer: OpaquePointer?, isOwned: Bool) {
|
||||||
|
super.init(pointer: pointer, isOwned: isOwned)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init() {
|
||||||
|
self.init(pointer: FilePlayer.api.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a player and loads the audio file at `path`.
|
||||||
|
public convenience init(path: String) throws(PlaydateError) {
|
||||||
|
self.init()
|
||||||
|
try load(path: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
FilePlayer.api.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepares the player to stream the file at `path`.
|
||||||
|
public func load(path: String) throws(PlaydateError) {
|
||||||
|
let loaded = path.withPlaydateCString {
|
||||||
|
FilePlayer.api.pointee.loadIntoPlayer.unsafelyUnwrapped(pointer, $0) != 0
|
||||||
|
}
|
||||||
|
if !loaded {
|
||||||
|
throw PlaydateError(message: "unable to load audio file: \(path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the length of the stream buffer, in seconds. Default 0.25.
|
||||||
|
public func setBufferLength(_ seconds: Float) {
|
||||||
|
FilePlayer.api.pointee.setBufferLength.unsafelyUnwrapped(pointer, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts playback, looping `repeat` times; 0 loops endlessly.
|
||||||
|
@discardableResult
|
||||||
|
public func play(repeat repeatCount: Int = 1) -> Bool {
|
||||||
|
FilePlayer.api.pointee.play.unsafelyUnwrapped(pointer, Int32(repeatCount)) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func pause() {
|
||||||
|
FilePlayer.api.pointee.pause.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stop() {
|
||||||
|
FilePlayer.api.pointee.stop.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The file's length in seconds.
|
||||||
|
public var length: Float {
|
||||||
|
FilePlayer.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The playback position in seconds.
|
||||||
|
public var offset: Float {
|
||||||
|
get { FilePlayer.api.pointee.getOffset.unsafelyUnwrapped(pointer) }
|
||||||
|
set { FilePlayer.api.pointee.setOffset.unsafelyUnwrapped(pointer, newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The playback rate; 1 is normal speed, negative values are not
|
||||||
|
/// supported.
|
||||||
|
public var rate: Float {
|
||||||
|
get { FilePlayer.api.pointee.getRate.unsafelyUnwrapped(pointer) }
|
||||||
|
set { FilePlayer.api.pointee.setRate.unsafelyUnwrapped(pointer, newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loops playback between `start` and `end` (seconds) while playing
|
||||||
|
/// with `repeat` 0. An `end` of 0 means the end of the file.
|
||||||
|
public func setLoopRange(start: Float, end: Float) {
|
||||||
|
FilePlayer.api.pointee.setLoopRange.unsafelyUnwrapped(pointer, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether playback underran because the file could not be read fast
|
||||||
|
/// enough.
|
||||||
|
public var didUnderrun: Bool {
|
||||||
|
FilePlayer.api.pointee.didUnderrun.unsafelyUnwrapped(pointer) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops playback (instead of looping the buffer) on underrun.
|
||||||
|
public func setStopOnUnderrun(_ flag: Bool) {
|
||||||
|
FilePlayer.api.pointee.setStopOnUnderrun.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a function called every time playback loops.
|
||||||
|
public func setLoopCallback(_ callback: ((FilePlayer) -> Void)?) {
|
||||||
|
loopCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
FilePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, { _, userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let player = Unmanaged<FilePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
player.loopCallback?(player)
|
||||||
|
}, Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
} else {
|
||||||
|
FilePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fades the volume to the given levels over `length` sample frames,
|
||||||
|
/// then calls `completion`.
|
||||||
|
public func fadeVolume(left: Float, right: Float, length: Int32,
|
||||||
|
completion: ((FilePlayer) -> Void)? = nil) {
|
||||||
|
fadeCallback = completion
|
||||||
|
if completion != nil {
|
||||||
|
FilePlayer.api.pointee.fadeVolume.unsafelyUnwrapped(pointer, left, right, length, { _, userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let player = Unmanaged<FilePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
player.fadeCallback?(player)
|
||||||
|
}, Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
} else {
|
||||||
|
FilePlayer.api.pointee.fadeVolume.unsafelyUnwrapped(pointer, left, right, length, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streams MP3 data from a callback instead of a file. The callback
|
||||||
|
/// fills the buffer and returns the number of bytes written; return 0
|
||||||
|
/// to signal the end of the stream.
|
||||||
|
public func setMP3StreamSource(bufferLength: Float,
|
||||||
|
_ dataSource: @escaping (UnsafeMutableBufferPointer<UInt8>) -> Int) {
|
||||||
|
mp3DataSource = dataSource
|
||||||
|
FilePlayer.api.pointee.setMP3StreamSource.unsafelyUnwrapped(pointer, { data, bytes, userdata in
|
||||||
|
guard let userdata, let data else { return 0 }
|
||||||
|
let player = Unmanaged<FilePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
let buffer = UnsafeMutableBufferPointer(start: data, count: Int(bytes))
|
||||||
|
return Int32(player.mp3DataSource?(buffer) ?? 0)
|
||||||
|
}, Unmanaged.passUnretained(self).toOpaque(), bufferLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modulates the playback rate.
|
||||||
|
public var rateModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(FilePlayer.api.pointee.getRateModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedRateModulator = newValue
|
||||||
|
FilePlayer.api.pointee.setRateModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// Plays an `AudioSample` from memory. Wraps `SamplePlayer`.
|
||||||
|
public final class SamplePlayer: Source {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_sampleplayer> { Playdate.samplePlayerAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
var loopCallback: ((SamplePlayer) -> Void)?
|
||||||
|
private var retainedSample: AudioSample?
|
||||||
|
private var retainedRateModulator: SignalValue?
|
||||||
|
|
||||||
|
override init(pointer: OpaquePointer?, isOwned: Bool) {
|
||||||
|
super.init(pointer: pointer, isOwned: isOwned)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init() {
|
||||||
|
self.init(pointer: SamplePlayer.api.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a player for the sample at `path`.
|
||||||
|
public convenience init(path: String) throws(PlaydateError) {
|
||||||
|
self.init()
|
||||||
|
sample = try AudioSample(path: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
SamplePlayer.api.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sample to play.
|
||||||
|
public var sample: AudioSample? {
|
||||||
|
get { retainedSample }
|
||||||
|
set {
|
||||||
|
retainedSample = newValue
|
||||||
|
SamplePlayer.api.pointee.setSample.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts playback at `rate`, looping `repeat` times; 0 loops
|
||||||
|
/// endlessly, -1 loops ping-pong.
|
||||||
|
@discardableResult
|
||||||
|
public func play(repeat repeatCount: Int = 1, rate: Float = 1) -> Bool {
|
||||||
|
SamplePlayer.api.pointee.play.unsafelyUnwrapped(pointer, Int32(repeatCount), rate) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stop() {
|
||||||
|
SamplePlayer.api.pointee.stop.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setPaused(_ paused: Bool) {
|
||||||
|
SamplePlayer.api.pointee.setPaused.unsafelyUnwrapped(pointer, paused ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sample's length in seconds.
|
||||||
|
public var length: Float {
|
||||||
|
SamplePlayer.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The playback position in seconds.
|
||||||
|
public var offset: Float {
|
||||||
|
get { SamplePlayer.api.pointee.getOffset.unsafelyUnwrapped(pointer) }
|
||||||
|
set { SamplePlayer.api.pointee.setOffset.unsafelyUnwrapped(pointer, newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The playback rate; 1 is normal speed, negative plays backward.
|
||||||
|
public var rate: Float {
|
||||||
|
get { SamplePlayer.api.pointee.getRate.unsafelyUnwrapped(pointer) }
|
||||||
|
set { SamplePlayer.api.pointee.setRate.unsafelyUnwrapped(pointer, newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restricts playback to the given range of sample frames.
|
||||||
|
public func setPlayRange(start: Int, end: Int) {
|
||||||
|
SamplePlayer.api.pointee.setPlayRange.unsafelyUnwrapped(pointer, Int32(start), Int32(end))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a function called every time playback loops.
|
||||||
|
public func setLoopCallback(_ callback: ((SamplePlayer) -> Void)?) {
|
||||||
|
loopCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
SamplePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, { _, userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let player = Unmanaged<SamplePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
player.loopCallback?(player)
|
||||||
|
}, Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
} else {
|
||||||
|
SamplePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modulates the playback rate.
|
||||||
|
public var rateModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(SamplePlayer.api.pointee.getRateModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retainedRateModulator = newValue
|
||||||
|
SamplePlayer.api.pointee.setRateModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A source of audio: the base class of `FilePlayer`, `SamplePlayer`,
|
||||||
|
/// `Synth`, `DelayLineTap`, and `CallbackSource`. Wraps `SoundSource`.
|
||||||
|
public class Source {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_source> { Playdate.sourceAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
/// The underlying C object. Set once, immediately after creation.
|
||||||
|
var pointer: OpaquePointer!
|
||||||
|
let isOwned: Bool
|
||||||
|
var finishCallback: ((Source) -> Void)?
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer?, isOwned: Bool) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.isOwned = isOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the playback volume for the left and right channels, 0...1.
|
||||||
|
public func setVolume(left: Float, right: Float) {
|
||||||
|
Source.api.pointee.setVolume.unsafelyUnwrapped(pointer, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the playback volume of both channels.
|
||||||
|
public func setVolume(_ volume: Float) {
|
||||||
|
setVolume(left: volume, right: volume)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The playback volume of the left and right channels.
|
||||||
|
public var volume: (left: Float, right: Float) {
|
||||||
|
var left: Float = 0, right: Float = 0
|
||||||
|
Source.api.pointee.getVolume.unsafelyUnwrapped(pointer, &left, &right)
|
||||||
|
return (left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isPlaying: Bool {
|
||||||
|
Source.api.pointee.isPlaying.unsafelyUnwrapped(pointer) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a function called when the source finishes playing.
|
||||||
|
public func setFinishCallback(_ callback: ((Source) -> Void)?) {
|
||||||
|
finishCallback = callback
|
||||||
|
if callback != nil {
|
||||||
|
Source.api.pointee.setFinishCallback.unsafelyUnwrapped(pointer, { _, userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let source = Unmanaged<Source>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
source.finishCallback?(source)
|
||||||
|
}, Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
} else {
|
||||||
|
Source.api.pointee.setFinishCallback.unsafelyUnwrapped(pointer, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A bank of synth voices for playing a sequence track. Wraps
|
||||||
|
/// `PDSynthInstrument`.
|
||||||
|
public final class Instrument {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_instrument> { Playdate.instrumentAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let isOwned: Bool
|
||||||
|
private var retainedVoices: [Synth] = []
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.isOwned = isOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init() {
|
||||||
|
self.init(pointer: Instrument.api.pointee.newInstrument.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
Instrument.api.pointee.freeInstrument.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a voice to the instrument, handling notes in
|
||||||
|
/// `rangeStart...rangeEnd` (0...127 handles all notes), transposed by
|
||||||
|
/// `transpose` half-steps.
|
||||||
|
@discardableResult
|
||||||
|
public func addVoice(_ synth: Synth, rangeStart: MIDINote = 0, rangeEnd: MIDINote = 127,
|
||||||
|
transpose: Float = 0) -> Bool {
|
||||||
|
let added = Instrument.api.pointee.addVoice.unsafelyUnwrapped(
|
||||||
|
pointer, synth.pointer, rangeStart, rangeEnd, transpose) != 0
|
||||||
|
if added, !retainedVoices.contains(where: { $0 === synth }) {
|
||||||
|
retainedVoices.append(synth)
|
||||||
|
}
|
||||||
|
return added
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a note at `frequency` Hz on an available voice. Returns the
|
||||||
|
/// synth used, if any.
|
||||||
|
@discardableResult
|
||||||
|
public func playNote(frequency: Float, velocity: Float = 1,
|
||||||
|
length: Float? = nil, when: UInt32 = 0) -> Synth? {
|
||||||
|
let synth = Instrument.api.pointee.playNote.unsafelyUnwrapped(
|
||||||
|
pointer, frequency, velocity, length ?? -1, when)
|
||||||
|
return voice(for: synth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a MIDI note on an available voice. Returns the synth used.
|
||||||
|
@discardableResult
|
||||||
|
public func playMIDINote(_ note: MIDINote, velocity: Float = 1,
|
||||||
|
length: Float? = nil, when: UInt32 = 0) -> Synth? {
|
||||||
|
let synth = Instrument.api.pointee.playMIDINote.unsafelyUnwrapped(
|
||||||
|
pointer, note, velocity, length ?? -1, when)
|
||||||
|
return voice(for: synth)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func voice(for pointer: OpaquePointer?) -> Synth? {
|
||||||
|
guard let pointer else { return nil }
|
||||||
|
if let voice = retainedVoices.first(where: { $0.pointer == pointer }) {
|
||||||
|
return voice
|
||||||
|
}
|
||||||
|
return Synth(pointer: pointer, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bends played notes by `bend` × the pitch bend range.
|
||||||
|
public func setPitchBend(_ bend: Float) {
|
||||||
|
Instrument.api.pointee.setPitchBend.unsafelyUnwrapped(pointer, bend)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setPitchBendRange(halfSteps: Float) {
|
||||||
|
Instrument.api.pointee.setPitchBendRange.unsafelyUnwrapped(pointer, halfSteps)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setTranspose(halfSteps: Float) {
|
||||||
|
Instrument.api.pointee.setTranspose.unsafelyUnwrapped(pointer, halfSteps)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases the voice playing `note` at time `when` (0 = now).
|
||||||
|
public func noteOff(_ note: MIDINote, when: UInt32 = 0) {
|
||||||
|
Instrument.api.pointee.noteOff.unsafelyUnwrapped(pointer, note, when)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func allNotesOff(when: UInt32 = 0) {
|
||||||
|
Instrument.api.pointee.allNotesOff.unsafelyUnwrapped(pointer, when)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setVolume(left: Float, right: Float) {
|
||||||
|
Instrument.api.pointee.setVolume.unsafelyUnwrapped(pointer, left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var volume: (left: Float, right: Float) {
|
||||||
|
var left: Float = 0, right: Float = 0
|
||||||
|
Instrument.api.pointee.getVolume.unsafelyUnwrapped(pointer, &left, &right)
|
||||||
|
return (left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var activeVoiceCount: Int {
|
||||||
|
Int(Instrument.api.pointee.activeVoiceCount.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A collection of tracks with tempo and loop control, playable from a
|
||||||
|
/// MIDI file. Wraps `SoundSequence`.
|
||||||
|
public final class Sequence {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_sequence> { Playdate.sequenceAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
private var retainedTracks: [SequenceTrack] = []
|
||||||
|
var finishCallback: ((Sequence) -> Void)?
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
pointer = Sequence.api.pointee.newSequence.unsafelyUnwrapped().unsafelyUnwrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a sequence and loads the MIDI file at `path`.
|
||||||
|
public convenience init(midiFilePath: String) throws(PlaydateError) {
|
||||||
|
self.init()
|
||||||
|
try loadMIDIFile(path: midiFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
Sequence.api.pointee.freeSequence.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadMIDIFile(path: String) throws(PlaydateError) {
|
||||||
|
let loaded = path.withPlaydateCString {
|
||||||
|
Sequence.api.pointee.loadMIDIFile.unsafelyUnwrapped(pointer, $0) != 0
|
||||||
|
}
|
||||||
|
if !loaded {
|
||||||
|
throw PlaydateError(message: "unable to load MIDI file: \(path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts playback. `completion` is called when the sequence finishes.
|
||||||
|
public func play(completion: ((Sequence) -> Void)? = nil) {
|
||||||
|
finishCallback = completion
|
||||||
|
if completion != nil {
|
||||||
|
Sequence.api.pointee.play.unsafelyUnwrapped(pointer, { _, userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let sequence = Unmanaged<Sequence>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
sequence.finishCallback?(sequence)
|
||||||
|
}, Unmanaged.passUnretained(self).toOpaque())
|
||||||
|
} else {
|
||||||
|
Sequence.api.pointee.play.unsafelyUnwrapped(pointer, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stop() {
|
||||||
|
Sequence.api.pointee.stop.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isPlaying: Bool {
|
||||||
|
Sequence.api.pointee.isPlaying.unsafelyUnwrapped(pointer) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The playback position, in samples.
|
||||||
|
public var time: UInt32 {
|
||||||
|
get { Sequence.api.pointee.getTime.unsafelyUnwrapped(pointer) }
|
||||||
|
set { Sequence.api.pointee.setTime.unsafelyUnwrapped(pointer, newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tempo, in steps per second.
|
||||||
|
public var tempo: Float {
|
||||||
|
get { Sequence.api.pointee.getTempo.unsafelyUnwrapped(pointer) }
|
||||||
|
set { Sequence.api.pointee.setTempo.unsafelyUnwrapped(pointer, newValue) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sequence's length in steps, including the tail of the last note.
|
||||||
|
public var length: UInt32 {
|
||||||
|
Sequence.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loops the range `loopStart..<loopEnd` (steps) `loops` times while
|
||||||
|
/// playing; 0 loops endlessly.
|
||||||
|
public func setLoops(start: Int, end: Int, count: Int = 0) {
|
||||||
|
Sequence.api.pointee.setLoops.unsafelyUnwrapped(pointer, Int32(start), Int32(end), Int32(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current step, and the time offset (in samples) into that step.
|
||||||
|
public var currentStep: (step: Int, timeOffset: Int) {
|
||||||
|
var timeOffset: Int32 = 0
|
||||||
|
let step = Sequence.api.pointee.getCurrentStep.unsafelyUnwrapped(pointer, &timeOffset)
|
||||||
|
return (Int(step), Int(timeOffset))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves playback to the given step. If `playNotes` is `true`, notes
|
||||||
|
/// at the position (that started before it) are played.
|
||||||
|
public func setCurrentStep(_ step: Int, timeOffset: Int = 0, playNotes: Bool = false) {
|
||||||
|
Sequence.api.pointee.setCurrentStep.unsafelyUnwrapped(pointer, Int32(step),
|
||||||
|
Int32(timeOffset), playNotes ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Tracks
|
||||||
|
|
||||||
|
public var trackCount: Int {
|
||||||
|
Int(Sequence.api.pointee.getTrackCount.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new track to the sequence. The track is owned by the
|
||||||
|
/// sequence.
|
||||||
|
@discardableResult
|
||||||
|
public func addTrack() -> SequenceTrack {
|
||||||
|
let track = SequenceTrack(
|
||||||
|
pointer: Sequence.api.pointee.addTrack.unsafelyUnwrapped(pointer).unsafelyUnwrapped,
|
||||||
|
isOwned: false)
|
||||||
|
retainedTracks.append(track)
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The track at `index`. Owned by the sequence.
|
||||||
|
public func track(at index: Int) -> SequenceTrack? {
|
||||||
|
guard let track = Sequence.api.pointee.getTrackAtIndex.unsafelyUnwrapped(
|
||||||
|
pointer, UInt32(index)) else { return nil }
|
||||||
|
return SequenceTrack(pointer: track, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs `track` at `index`.
|
||||||
|
public func setTrack(_ track: SequenceTrack, at index: Int) {
|
||||||
|
if !retainedTracks.contains(where: { $0 === track }) {
|
||||||
|
retainedTracks.append(track)
|
||||||
|
}
|
||||||
|
Sequence.api.pointee.setTrackAtIndex.unsafelyUnwrapped(pointer, track.pointer, UInt32(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases every playing note in the sequence.
|
||||||
|
public func allNotesOff() {
|
||||||
|
Sequence.api.pointee.allNotesOff.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A track of notes played by an instrument. Wraps `SequenceTrack`.
|
||||||
|
public final class SequenceTrack {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_track> { Playdate.trackAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
let isOwned: Bool
|
||||||
|
private var retainedInstrument: Instrument?
|
||||||
|
|
||||||
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.isOwned = isOwned
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init() {
|
||||||
|
self.init(pointer: SequenceTrack.api.pointee.newTrack.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
SequenceTrack.api.pointee.freeTrack.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The instrument that plays this track's notes.
|
||||||
|
public var instrument: Instrument? {
|
||||||
|
get {
|
||||||
|
if let retainedInstrument { return retainedInstrument }
|
||||||
|
guard let instrument = SequenceTrack.api.pointee.getInstrument.unsafelyUnwrapped(pointer) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Instrument(pointer: instrument, isOwned: false)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
retainedInstrument = newValue
|
||||||
|
SequenceTrack.api.pointee.setInstrument.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a note starting at `step`, lasting `length` steps.
|
||||||
|
public func addNote(step: UInt32, length: UInt32, note: MIDINote, velocity: Float = 1) {
|
||||||
|
SequenceTrack.api.pointee.addNoteEvent.unsafelyUnwrapped(pointer, step, length, note, velocity)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func removeNote(step: UInt32, note: MIDINote) {
|
||||||
|
SequenceTrack.api.pointee.removeNoteEvent.unsafelyUnwrapped(pointer, step, note)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func clearNotes() {
|
||||||
|
SequenceTrack.api.pointee.clearNotes.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The track's length in steps, including the tail of the last note.
|
||||||
|
public var length: UInt32 {
|
||||||
|
SequenceTrack.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The index of the first note at or after `step`.
|
||||||
|
public func indexForStep(_ step: UInt32) -> Int {
|
||||||
|
Int(SequenceTrack.api.pointee.getIndexForStep.unsafelyUnwrapped(pointer, step))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The note at `index`, or `nil` if the index is out of range.
|
||||||
|
public func note(at index: Int) -> (step: UInt32, length: UInt32,
|
||||||
|
note: MIDINote, velocity: Float)? {
|
||||||
|
var step: UInt32 = 0, length: UInt32 = 0
|
||||||
|
var note: MIDINote = 0
|
||||||
|
var velocity: Float = 0
|
||||||
|
guard SequenceTrack.api.pointee.getNoteAtIndex.unsafelyUnwrapped(
|
||||||
|
pointer, Int32(index), &step, &length, ¬e, &velocity) != 0 else { return nil }
|
||||||
|
return (step, length, note, velocity)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of control signals on the track.
|
||||||
|
public var controlSignalCount: Int {
|
||||||
|
Int(SequenceTrack.api.pointee.getControlSignalCount.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The control signal at `index`. Owned by the track.
|
||||||
|
public func controlSignal(at index: Int) -> ControlSignal? {
|
||||||
|
guard let signal = SequenceTrack.api.pointee.getControlSignal.unsafelyUnwrapped(
|
||||||
|
pointer, Int32(index)) else { return nil }
|
||||||
|
return ControlSignal(pointer: signal, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The control signal for MIDI controller `controller`, optionally
|
||||||
|
/// creating it. Owned by the track.
|
||||||
|
public func signalForController(_ controller: Int, create: Bool = false) -> ControlSignal? {
|
||||||
|
guard let signal = SequenceTrack.api.pointee.getSignalForController.unsafelyUnwrapped(
|
||||||
|
pointer, Int32(controller), create ? 1 : 0) else { return nil }
|
||||||
|
return ControlSignal(pointer: signal, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func clearControlEvents() {
|
||||||
|
SequenceTrack.api.pointee.clearControlEvents.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The maximum number of simultaneous notes in the track.
|
||||||
|
public var polyphony: Int {
|
||||||
|
Int(SequenceTrack.api.pointee.getPolyphony.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var activeVoiceCount: Int {
|
||||||
|
Int(SequenceTrack.api.pointee.activeVoiceCount.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setMuted(_ muted: Bool) {
|
||||||
|
SequenceTrack.api.pointee.setMuted.unsafelyUnwrapped(pointer, muted ? 1 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound {
|
||||||
|
/// A synthesizer voice. Wraps `PDSynth`.
|
||||||
|
public final class Synth: Source {
|
||||||
|
private static var api: UnsafePointer<playdate_sound_synth> { Playdate.synthAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
|
private final class GeneratorBox {
|
||||||
|
let generator: Generator
|
||||||
|
let stereo: Bool
|
||||||
|
init(_ generator: Generator, stereo: Bool) {
|
||||||
|
self.generator = generator
|
||||||
|
self.stereo = stereo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var retainedSample: AudioSample?
|
||||||
|
private var retainedModulators: [SignalValue] = []
|
||||||
|
|
||||||
|
override init(pointer: OpaquePointer?, isOwned: Bool) {
|
||||||
|
super.init(pointer: pointer, isOwned: isOwned)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init() {
|
||||||
|
self.init(pointer: Synth.api.pointee.newSynth.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public convenience init(waveform: Waveform) {
|
||||||
|
self.init()
|
||||||
|
setWaveform(waveform)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
if isOwned {
|
||||||
|
Synth.api.pointee.freeSynth.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies the synth (and its generator, if any).
|
||||||
|
public func copy() -> Synth {
|
||||||
|
Synth(pointer: Synth.api.pointee.copy.unsafelyUnwrapped(pointer).unsafelyUnwrapped,
|
||||||
|
isOwned: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Sound generation
|
||||||
|
|
||||||
|
public func setWaveform(_ waveform: Waveform) {
|
||||||
|
Synth.api.pointee.setWaveform.unsafelyUnwrapped(pointer, waveform.cValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a sample instead of a waveform. A nonzero sustain range
|
||||||
|
/// loops that part of the sample while the note is held.
|
||||||
|
public func setSample(_ sample: AudioSample, sustainStart: UInt32 = 0, sustainEnd: UInt32 = 0) {
|
||||||
|
retainedSample = sample
|
||||||
|
Synth.api.pointee.setSample.unsafelyUnwrapped(pointer, sample.pointer, sustainStart, sustainEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses a wavetable for the synth. `log2size` is the base-2 log of
|
||||||
|
/// each waveform's size (e.g. 8 for 256 samples).
|
||||||
|
public func setWavetable(_ sample: AudioSample, log2size: Int,
|
||||||
|
columns: Int, rows: Int) throws(PlaydateError) {
|
||||||
|
retainedSample = sample
|
||||||
|
guard Synth.api.pointee.setWavetable.unsafelyUnwrapped(
|
||||||
|
pointer, sample.pointer, Int32(log2size), Int32(columns), Int32(rows)) != 0 else {
|
||||||
|
throw PlaydateError(message: "invalid wavetable dimensions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides audio via custom Swift callbacks.
|
||||||
|
public func setGenerator(stereo: Bool, _ generator: Generator) {
|
||||||
|
let box = Unmanaged.passRetained(GeneratorBox(generator, stereo: stereo))
|
||||||
|
Synth.api.pointee.setGenerator.unsafelyUnwrapped(
|
||||||
|
pointer, stereo ? 1 : 0,
|
||||||
|
{ userdata, left, right, nsamples, rate, drate in
|
||||||
|
guard let userdata, let left else { return 0 }
|
||||||
|
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
let leftBuffer = UnsafeMutableBufferPointer(start: left, count: Int(nsamples))
|
||||||
|
let rightBuffer = right.map { UnsafeMutableBufferPointer(start: $0, count: Int(nsamples)) }
|
||||||
|
return Int32(box.generator.render(leftBuffer, rightBuffer, rate, drate))
|
||||||
|
},
|
||||||
|
{ userdata, note, velocity, length in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
box.generator.noteOn?(note, velocity, length)
|
||||||
|
},
|
||||||
|
{ userdata, stop in
|
||||||
|
guard let userdata else { return }
|
||||||
|
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
box.generator.release?(stop != 0)
|
||||||
|
},
|
||||||
|
{ userdata, parameter, value in
|
||||||
|
guard let userdata else { return 0 }
|
||||||
|
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
return box.generator.setParameter?(Int(parameter), value) == true ? 1 : 0
|
||||||
|
},
|
||||||
|
{ userdata in
|
||||||
|
guard let userdata else { return }
|
||||||
|
Unmanaged<GeneratorBox>.fromOpaque(userdata).release()
|
||||||
|
},
|
||||||
|
{ userdata in
|
||||||
|
guard let userdata else { return nil }
|
||||||
|
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
|
return Unmanaged.passRetained(GeneratorBox(box.generator, stereo: box.stereo)).toOpaque()
|
||||||
|
},
|
||||||
|
box.toOpaque())
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Envelope
|
||||||
|
|
||||||
|
public func setAttackTime(_ attack: Float) {
|
||||||
|
Synth.api.pointee.setAttackTime.unsafelyUnwrapped(pointer, attack)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setDecayTime(_ decay: Float) {
|
||||||
|
Synth.api.pointee.setDecayTime.unsafelyUnwrapped(pointer, decay)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setSustainLevel(_ sustain: Float) {
|
||||||
|
Synth.api.pointee.setSustainLevel.unsafelyUnwrapped(pointer, sustain)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setReleaseTime(_ release: Float) {
|
||||||
|
Synth.api.pointee.setReleaseTime.unsafelyUnwrapped(pointer, release)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The synth's amplitude envelope. Owned by the synth.
|
||||||
|
public var envelope: Envelope? {
|
||||||
|
guard let envelope = Synth.api.pointee.getEnvelope.unsafelyUnwrapped(pointer) else { return nil }
|
||||||
|
return Envelope(pointer: envelope, isOwned: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the synth's envelope so it plays at constant volume.
|
||||||
|
public func clearEnvelope() {
|
||||||
|
Synth.api.pointee.clearEnvelope.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Modulation
|
||||||
|
|
||||||
|
/// Transposes played notes by `halfSteps` (fractional values allowed).
|
||||||
|
public func setTranspose(_ halfSteps: Float) {
|
||||||
|
Synth.api.pointee.setTranspose.unsafelyUnwrapped(pointer, halfSteps)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var frequencyModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(Synth.api.pointee.getFrequencyModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retain(newValue)
|
||||||
|
Synth.api.pointee.setFrequencyModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var amplitudeModulator: SignalValue? {
|
||||||
|
get { SignalValue.wrap(Synth.api.pointee.getAmplitudeModulator.unsafelyUnwrapped(pointer)) }
|
||||||
|
set {
|
||||||
|
retain(newValue)
|
||||||
|
Synth.api.pointee.setAmplitudeModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of parameters the synth's generator supports.
|
||||||
|
public var parameterCount: Int {
|
||||||
|
Int(Synth.api.pointee.getParameterCount.unsafelyUnwrapped(pointer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a generator parameter. Returns `false` if the parameter is
|
||||||
|
/// invalid.
|
||||||
|
@discardableResult
|
||||||
|
public func setParameter(_ parameter: Int, value: Float) -> Bool {
|
||||||
|
Synth.api.pointee.setParameter.unsafelyUnwrapped(pointer, Int32(parameter), value) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setParameterModulator(_ parameter: Int, _ modulator: SignalValue?) {
|
||||||
|
retain(modulator)
|
||||||
|
Synth.api.pointee.setParameterModulator.unsafelyUnwrapped(pointer, Int32(parameter),
|
||||||
|
modulator?.pointer)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func parameterModulator(_ parameter: Int) -> SignalValue? {
|
||||||
|
SignalValue.wrap(Synth.api.pointee.getParameterModulator.unsafelyUnwrapped(pointer, Int32(parameter)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func retain(_ modulator: SignalValue?) {
|
||||||
|
if let modulator, !retainedModulators.contains(where: { $0 === modulator }) {
|
||||||
|
retainedModulators.append(modulator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Playing
|
||||||
|
|
||||||
|
/// Plays a note at `frequency` Hz. `length` is in seconds; `nil`
|
||||||
|
/// plays until `noteOff()`. `when` is the audio-clock time to start,
|
||||||
|
/// or 0 for immediately.
|
||||||
|
public func playNote(frequency: Float, velocity: Float = 1,
|
||||||
|
length: Float? = nil, when: UInt32 = 0) {
|
||||||
|
Synth.api.pointee.playNote.unsafelyUnwrapped(pointer, frequency, velocity, length ?? -1, when)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plays a MIDI note, where 60 is middle C.
|
||||||
|
public func playMIDINote(_ note: MIDINote, velocity: Float = 1,
|
||||||
|
length: Float? = nil, when: UInt32 = 0) {
|
||||||
|
Synth.api.pointee.playMIDINote.unsafelyUnwrapped(pointer, note, velocity, length ?? -1, when)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases the playing note at time `when`, or immediately if 0.
|
||||||
|
public func noteOff(when: UInt32 = 0) {
|
||||||
|
Synth.api.pointee.noteOff.unsafelyUnwrapped(pointer, when)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops the synth immediately, without playing the release phase.
|
||||||
|
public func stop() {
|
||||||
|
Synth.api.pointee.stop.unsafelyUnwrapped(pointer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sound.Synth {
|
||||||
|
/// The synth's waveform.
|
||||||
|
public enum Waveform: UInt32, Sendable {
|
||||||
|
case square = 0
|
||||||
|
case triangle = 1
|
||||||
|
case sine = 2
|
||||||
|
case noise = 3
|
||||||
|
case sawtooth = 4
|
||||||
|
case poPhase = 5
|
||||||
|
case poDigital = 6
|
||||||
|
case poVosim = 7
|
||||||
|
|
||||||
|
var cValue: SoundWaveform { SoundWaveform(SoundWaveform.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
extension Sound.Synth {
|
||||||
|
/// Custom generator callbacks. Samples are in signed Q8.24 format.
|
||||||
|
public struct Generator {
|
||||||
|
/// Renders up to 256 sample frames into `left` (and `right` for
|
||||||
|
/// stereo generators). `rate` is the per-frame phase step in
|
||||||
|
/// Q0.32 format and `drate` its per-frame change. Returns the
|
||||||
|
/// number of frames rendered.
|
||||||
|
public var render: (_ left: UnsafeMutableBufferPointer<Int32>,
|
||||||
|
_ right: UnsafeMutableBufferPointer<Int32>?,
|
||||||
|
_ rate: UInt32, _ drate: Int32) -> Int
|
||||||
|
/// Called when a note starts. `length` is -1 for indefinite notes.
|
||||||
|
public var noteOn: ((_ note: Sound.MIDINote, _ velocity: Float, _ length: Float) -> Void)?
|
||||||
|
/// Called when a note is released (`stop == false`) or stopped
|
||||||
|
/// (`stop == true`).
|
||||||
|
public var release: ((_ stop: Bool) -> Void)?
|
||||||
|
/// Sets a generator parameter. Returns `true` if the parameter is
|
||||||
|
/// valid.
|
||||||
|
public var setParameter: ((_ parameter: Int, _ value: Float) -> Bool)?
|
||||||
|
|
||||||
|
public init(render: @escaping (_ left: UnsafeMutableBufferPointer<Int32>,
|
||||||
|
_ right: UnsafeMutableBufferPointer<Int32>?,
|
||||||
|
_ rate: UInt32, _ drate: Int32) -> Int,
|
||||||
|
noteOn: ((_ note: Sound.MIDINote, _ velocity: Float, _ length: Float) -> Void)? = nil,
|
||||||
|
release: ((_ stop: Bool) -> Void)? = nil,
|
||||||
|
setParameter: ((_ parameter: Int, _ value: Float) -> Bool)? = nil) {
|
||||||
|
self.render = render
|
||||||
|
self.noteOn = noteOn
|
||||||
|
self.release = release
|
||||||
|
self.setParameter = setParameter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
//
|
|
||||||
// SoundEffect.swift
|
|
||||||
// SoundEffect wrappers: filters, bitcrusher, ring modulator, delay line,
|
|
||||||
// and overdrive.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
|
||||||
|
|
||||||
private var effectAPI: UnsafePointer<playdate_sound_effect> { Playdate.effectAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
extension Sound {
|
|
||||||
/// An effect that processes a channel's audio: the base class of the
|
|
||||||
/// built-in effects. Wraps `SoundEffect`.
|
|
||||||
public class Effect {
|
|
||||||
/// Processes up to `AUDIO_FRAMES_PER_CYCLE` sample frames in signed
|
|
||||||
/// Q8.24 format. `bufferActive` is `false` when the input buffer is
|
|
||||||
/// silent. Returns `true` if the effect produced output.
|
|
||||||
public typealias Processor = (_ left: UnsafeMutableBufferPointer<Int32>,
|
|
||||||
_ right: UnsafeMutableBufferPointer<Int32>?,
|
|
||||||
_ bufferActive: Bool) -> Bool
|
|
||||||
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let isOwned: Bool
|
|
||||||
private var retainedMixModulator: SignalValue?
|
|
||||||
private var processorBox: Unmanaged<ProcessorBox>?
|
|
||||||
|
|
||||||
final class ProcessorBox {
|
|
||||||
let processor: Processor
|
|
||||||
init(_ processor: @escaping Processor) { self.processor = processor }
|
|
||||||
}
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
self.pointer = pointer
|
|
||||||
self.isOwned = isOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an effect that processes audio with a Swift callback.
|
|
||||||
public init(processor: @escaping Processor) {
|
|
||||||
let box = Unmanaged.passRetained(ProcessorBox(processor))
|
|
||||||
processorBox = box
|
|
||||||
pointer = effectAPI.pointee.newEffect.unsafelyUnwrapped({ effect, left, right, nsamples, bufactive in
|
|
||||||
guard let effect, let left,
|
|
||||||
let userdata = effectAPI.pointee.getUserdata.unsafelyUnwrapped(effect) else { return 0 }
|
|
||||||
let box = Unmanaged<ProcessorBox>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
let leftBuffer = UnsafeMutableBufferPointer(start: left, count: Int(nsamples))
|
|
||||||
let rightBuffer = right.map { UnsafeMutableBufferPointer(start: $0, count: Int(nsamples)) }
|
|
||||||
return box.processor(leftBuffer, rightBuffer, bufactive != 0) ? 1 : 0
|
|
||||||
}, box.toOpaque()).unsafelyUnwrapped
|
|
||||||
isOwned = true
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
effectAPI.pointee.freeEffect.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
processorBox?.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The wet/dry mix: 1 is fully processed, 0 fully dry.
|
|
||||||
public func setMix(_ level: Float) {
|
|
||||||
effectAPI.pointee.setMix.unsafelyUnwrapped(pointer, level)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var mixModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(effectAPI.pointee.getMixModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedMixModulator = newValue
|
|
||||||
effectAPI.pointee.setMixModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Two-pole filter
|
|
||||||
|
|
||||||
/// A two-pole IIR filter. Wraps `TwoPoleFilter`.
|
|
||||||
public final class TwoPoleFilter: Effect {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_effect_twopolefilter> { Playdate.twoPoleFilterAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
public enum Kind: UInt32, Sendable {
|
|
||||||
case lowPass = 0
|
|
||||||
case highPass = 1
|
|
||||||
case bandPass = 2
|
|
||||||
case notch = 3
|
|
||||||
case peq = 4
|
|
||||||
case lowShelf = 5
|
|
||||||
case highShelf = 6
|
|
||||||
|
|
||||||
var cValue: TwoPoleFilterType { TwoPoleFilterType(TwoPoleFilterType.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var retainedFrequencyModulator: SignalValue?
|
|
||||||
private var retainedResonanceModulator: SignalValue?
|
|
||||||
|
|
||||||
public init(kind: Kind = .lowPass) {
|
|
||||||
super.init(pointer: TwoPoleFilter.api.pointee.newFilter.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
setKind(kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
TwoPoleFilter.api.pointee.freeFilter.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setKind(_ kind: Kind) {
|
|
||||||
TwoPoleFilter.api.pointee.setType.unsafelyUnwrapped(pointer, kind.cValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The center/corner frequency, in Hz.
|
|
||||||
public func setFrequency(_ frequency: Float) {
|
|
||||||
TwoPoleFilter.api.pointee.setFrequency.unsafelyUnwrapped(pointer, frequency)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var frequencyModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(TwoPoleFilter.api.pointee.getFrequencyModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedFrequencyModulator = newValue
|
|
||||||
TwoPoleFilter.api.pointee.setFrequencyModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The gain, used by PEQ and shelf filters.
|
|
||||||
public func setGain(_ gain: Float) {
|
|
||||||
TwoPoleFilter.api.pointee.setGain.unsafelyUnwrapped(pointer, gain)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setResonance(_ resonance: Float) {
|
|
||||||
TwoPoleFilter.api.pointee.setResonance.unsafelyUnwrapped(pointer, resonance)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var resonanceModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(TwoPoleFilter.api.pointee.getResonanceModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedResonanceModulator = newValue
|
|
||||||
TwoPoleFilter.api.pointee.setResonanceModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - One-pole filter
|
|
||||||
|
|
||||||
/// A one-pole low/high-pass filter. Wraps `OnePoleFilter`.
|
|
||||||
public final class OnePoleFilter: Effect {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_effect_onepolefilter> { Playdate.onePoleFilterAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
private var retainedParameterModulator: SignalValue?
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
super.init(pointer: OnePoleFilter.api.pointee.newFilter.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
OnePoleFilter.api.pointee.freeFilter.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The filter's cutoff: -1 to 1, where values above 0 are low-pass
|
|
||||||
/// and values below 0 high-pass.
|
|
||||||
public func setParameter(_ parameter: Float) {
|
|
||||||
OnePoleFilter.api.pointee.setParameter.unsafelyUnwrapped(pointer, parameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var parameterModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(OnePoleFilter.api.pointee.getParameterModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedParameterModulator = newValue
|
|
||||||
OnePoleFilter.api.pointee.setParameterModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Bit crusher
|
|
||||||
|
|
||||||
/// A bit-crushing and downsampling effect. Wraps `BitCrusher`.
|
|
||||||
public final class BitCrusher: Effect {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_effect_bitcrusher> { Playdate.bitCrusherAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
private var retainedModulators: [SignalValue] = []
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
super.init(pointer: BitCrusher.api.pointee.newBitCrusher.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
BitCrusher.api.pointee.freeBitCrusher.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When `true`, `setDepth` values map exponentially to bit depth.
|
|
||||||
public func setExponential(_ flag: Bool) {
|
|
||||||
BitCrusher.api.pointee.setExponential.unsafelyUnwrapped(pointer, flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The amount of crushing, 0 (none) to 1 (quantized to 1 bit).
|
|
||||||
public func setDepth(_ depth: Float) {
|
|
||||||
BitCrusher.api.pointee.setDepth.unsafelyUnwrapped(pointer, depth)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var depthModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(BitCrusher.api.pointee.getDepthModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retain(newValue)
|
|
||||||
BitCrusher.api.pointee.setDepthModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The amount of downsampling, 0 (none) to 1 (every sample repeated).
|
|
||||||
public func setDownsampling(_ downsampling: Float) {
|
|
||||||
BitCrusher.api.pointee.setDownsampling.unsafelyUnwrapped(pointer, downsampling)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var downsamplingModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(BitCrusher.api.pointee.getDownsamplingModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retain(newValue)
|
|
||||||
BitCrusher.api.pointee.setDownsamplingModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func retain(_ modulator: SignalValue?) {
|
|
||||||
if let modulator { retainedModulators.append(modulator) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Ring modulator
|
|
||||||
|
|
||||||
/// A ring modulator effect. Wraps `RingModulator`.
|
|
||||||
public final class RingModulator: Effect {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_effect_ringmodulator> { Playdate.ringModulatorAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
private var retainedFrequencyModulator: SignalValue?
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
super.init(pointer: RingModulator.api.pointee.newRingmod.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
RingModulator.api.pointee.freeRingmod.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The modulation frequency, in Hz.
|
|
||||||
public func setFrequency(_ frequency: Float) {
|
|
||||||
RingModulator.api.pointee.setFrequency.unsafelyUnwrapped(pointer, frequency)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var frequencyModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(RingModulator.api.pointee.getFrequencyModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedFrequencyModulator = newValue
|
|
||||||
RingModulator.api.pointee.setFrequencyModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Delay line
|
|
||||||
|
|
||||||
/// A tap into a delay line; produces audio and can be added to a channel
|
|
||||||
/// as a source. Wraps `DelayLineTap`.
|
|
||||||
public final class DelayLineTap: Source {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_effect_delayline> { Playdate.delayLineAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The delay line is retained so the tap stays valid.
|
|
||||||
private let delayLine: DelayLine
|
|
||||||
private var retainedDelayModulator: SignalValue?
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer, delayLine: DelayLine) {
|
|
||||||
self.delayLine = delayLine
|
|
||||||
super.init(pointer: pointer, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
DelayLineTap.api.pointee.freeTap.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The tap's position in the delay line, in frames.
|
|
||||||
public func setDelay(frames: Int) {
|
|
||||||
DelayLineTap.api.pointee.setTapDelay.unsafelyUnwrapped(pointer, Int32(frames))
|
|
||||||
}
|
|
||||||
|
|
||||||
public var delayModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(DelayLineTap.api.pointee.getTapDelayModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedDelayModulator = newValue
|
|
||||||
DelayLineTap.api.pointee.setTapDelayModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For stereo delay lines: swaps the left and right channels.
|
|
||||||
public func setChannelsFlipped(_ flipped: Bool) {
|
|
||||||
DelayLineTap.api.pointee.setTapChannelsFlipped.unsafelyUnwrapped(pointer, flipped ? 1 : 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A delay line effect. Wraps `DelayLine`.
|
|
||||||
public final class DelayLine: Effect {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_effect_delayline> { Playdate.delayLineAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// Creates a delay line holding `length` frames.
|
|
||||||
public init(length: Int, stereo: Bool = false) {
|
|
||||||
super.init(pointer: DelayLine.api.pointee.newDelayLine.unsafelyUnwrapped(
|
|
||||||
Int32(length), stereo ? 1 : 0).unsafelyUnwrapped, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
DelayLine.api.pointee.freeDelayLine.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes the delay length. Cannot be larger than the line's
|
|
||||||
/// original length.
|
|
||||||
public func setLength(frames: Int) {
|
|
||||||
DelayLine.api.pointee.setLength.unsafelyUnwrapped(pointer, Int32(frames))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The feedback level, 0...1.
|
|
||||||
public func setFeedback(_ feedback: Float) {
|
|
||||||
DelayLine.api.pointee.setFeedback.unsafelyUnwrapped(pointer, feedback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a tap `delay` frames behind the write head. The tap can be
|
|
||||||
/// added to a channel as a sound source.
|
|
||||||
public func addTap(delay: Int) -> DelayLineTap? {
|
|
||||||
guard let tap = DelayLine.api.pointee.addTap.unsafelyUnwrapped(pointer, Int32(delay)) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return DelayLineTap(pointer: tap, delayLine: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Overdrive
|
|
||||||
|
|
||||||
/// An overdrive/distortion effect. Wraps `Overdrive`.
|
|
||||||
public final class Overdrive: Effect {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_effect_overdrive> { Playdate.overdriveAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
private var retainedModulators: [SignalValue] = []
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
super.init(pointer: Overdrive.api.pointee.newOverdrive.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
Overdrive.api.pointee.freeOverdrive.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The input gain applied before clipping.
|
|
||||||
public func setGain(_ gain: Float) {
|
|
||||||
Overdrive.api.pointee.setGain.unsafelyUnwrapped(pointer, gain)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The level where the amplified input clips.
|
|
||||||
public func setLimit(_ limit: Float) {
|
|
||||||
Overdrive.api.pointee.setLimit.unsafelyUnwrapped(pointer, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var limitModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(Overdrive.api.pointee.getLimitModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retain(newValue)
|
|
||||||
Overdrive.api.pointee.setLimitModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A DC offset applied to the input, making the clipping asymmetric.
|
|
||||||
public func setOffset(_ offset: Float) {
|
|
||||||
Overdrive.api.pointee.setOffset.unsafelyUnwrapped(pointer, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var offsetModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(Overdrive.api.pointee.getOffsetModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retain(newValue)
|
|
||||||
Overdrive.api.pointee.setOffsetModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func retain(_ modulator: SignalValue?) {
|
|
||||||
if let modulator { retainedModulators.append(modulator) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
//
|
|
||||||
// SoundSignal.swift
|
|
||||||
// Signal wrappers: PDSynthSignalValue, PDSynthSignal, PDSynthLFO,
|
|
||||||
// PDSynthEnvelope, and ControlSignal.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
|
||||||
|
|
||||||
extension Sound {
|
|
||||||
/// A value that can modulate a parameter. The base class of `Signal`,
|
|
||||||
/// `LFO`, `Envelope`, and `ControlSignal`. Wraps `PDSynthSignalValue`.
|
|
||||||
public class SignalValue {
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let isOwned: Bool
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
self.pointer = pointer
|
|
||||||
self.isOwned = isOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wraps a signal value pointer returned by the OS (not owned).
|
|
||||||
static func wrap(_ pointer: OpaquePointer?) -> SignalValue? {
|
|
||||||
guard let pointer else { return nil }
|
|
||||||
return SignalValue(pointer: pointer, isOwned: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A signal object; also provides custom signals driven by Swift
|
|
||||||
/// callbacks. Wraps `PDSynthSignal`.
|
|
||||||
public final class Signal: SignalValue {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_signal> { Playdate.signalAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// Custom signal callbacks.
|
|
||||||
public struct Callbacks {
|
|
||||||
/// Returns the signal's value at the end of the current cycle.
|
|
||||||
/// `ioFrames` is the number of frames until the cycle ends and
|
|
||||||
/// may be lowered to interpolate toward `interpolationValue`.
|
|
||||||
public var step: (_ ioFrames: UnsafeMutablePointer<Int32>?,
|
|
||||||
_ interpolationValue: UnsafeMutablePointer<Float>?) -> Float
|
|
||||||
/// Called on note-on events. `length` is -1 for indefinite notes.
|
|
||||||
public var noteOn: ((_ note: MIDINote, _ velocity: Float, _ length: Float) -> Void)?
|
|
||||||
/// Called on note-off events. `stopped` is `false` when the note
|
|
||||||
/// is released and `true` when it actually stops playing;
|
|
||||||
/// `offset` is the frame offset within the current cycle.
|
|
||||||
public var noteOff: ((_ stopped: Bool, _ offset: Int) -> Void)?
|
|
||||||
|
|
||||||
public init(step: @escaping (_ ioFrames: UnsafeMutablePointer<Int32>?,
|
|
||||||
_ interpolationValue: UnsafeMutablePointer<Float>?) -> Float,
|
|
||||||
noteOn: ((_ note: MIDINote, _ velocity: Float, _ length: Float) -> Void)? = nil,
|
|
||||||
noteOff: ((_ stopped: Bool, _ offset: Int) -> Void)? = nil) {
|
|
||||||
self.step = step
|
|
||||||
self.noteOn = noteOn
|
|
||||||
self.noteOff = noteOff
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class Box {
|
|
||||||
let callbacks: Callbacks
|
|
||||||
init(_ callbacks: Callbacks) { self.callbacks = callbacks }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a signal driven by the given callbacks.
|
|
||||||
public init(callbacks: Callbacks) {
|
|
||||||
let box = Unmanaged.passRetained(Box(callbacks))
|
|
||||||
let pointer = Signal.api.pointee.newSignal.unsafelyUnwrapped(
|
|
||||||
{ userdata, ioFrames, interpolationValue in
|
|
||||||
guard let userdata else { return 0 }
|
|
||||||
let box = Unmanaged<Box>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
return box.callbacks.step(ioFrames, interpolationValue)
|
|
||||||
},
|
|
||||||
{ userdata, note, velocity, length in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let box = Unmanaged<Box>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
box.callbacks.noteOn?(note, velocity, length)
|
|
||||||
},
|
|
||||||
{ userdata, stopped, offset in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let box = Unmanaged<Box>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
box.callbacks.noteOff?(stopped != 0, Int(offset))
|
|
||||||
},
|
|
||||||
{ userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
Unmanaged<Box>.fromOpaque(userdata).release()
|
|
||||||
},
|
|
||||||
box.toOpaque())
|
|
||||||
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a plain signal object wrapping an existing signal value,
|
|
||||||
/// so it can be scaled and offset.
|
|
||||||
public init(value: SignalValue) {
|
|
||||||
let pointer = Signal.api.pointee.newSignalForValue.unsafelyUnwrapped(value.pointer)
|
|
||||||
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
super.init(pointer: pointer, isOwned: isOwned)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
Signal.api.pointee.freeSignal.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The signal's current value.
|
|
||||||
public var value: Float {
|
|
||||||
Signal.api.pointee.getValue.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scales the signal's output.
|
|
||||||
public func setValueScale(_ scale: Float) {
|
|
||||||
Signal.api.pointee.setValueScale.unsafelyUnwrapped(pointer, scale)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Offsets the signal's output.
|
|
||||||
public func setValueOffset(_ offset: Float) {
|
|
||||||
Signal.api.pointee.setValueOffset.unsafelyUnwrapped(pointer, offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - LFO
|
|
||||||
|
|
||||||
/// A low-frequency oscillator signal. Wraps `PDSynthLFO`.
|
|
||||||
public final class LFO: SignalValue {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_lfo> { Playdate.lfoAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The oscillator's waveform.
|
|
||||||
public enum Shape: UInt32, Sendable {
|
|
||||||
case square = 0
|
|
||||||
case triangle = 1
|
|
||||||
case sine = 2
|
|
||||||
case sampleAndHold = 3
|
|
||||||
case sawtoothUp = 4
|
|
||||||
case sawtoothDown = 5
|
|
||||||
case arpeggiator = 6
|
|
||||||
case function = 7
|
|
||||||
|
|
||||||
var cValue: LFOType { LFOType(LFOType.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var function: ((LFO) -> Float)?
|
|
||||||
|
|
||||||
public init(shape: Shape = .sine) {
|
|
||||||
let pointer = LFO.api.pointee.newLFO.unsafelyUnwrapped(shape.cValue)
|
|
||||||
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
LFO.api.pointee.freeLFO.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setShape(_ shape: Shape) {
|
|
||||||
LFO.api.pointee.setType.unsafelyUnwrapped(pointer, shape.cValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The LFO rate, in cycles per second.
|
|
||||||
public func setRate(_ rate: Float) {
|
|
||||||
LFO.api.pointee.setRate.unsafelyUnwrapped(pointer, rate)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current phase, 0...1.
|
|
||||||
public func setPhase(_ phase: Float) {
|
|
||||||
LFO.api.pointee.setPhase.unsafelyUnwrapped(pointer, phase)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The phase the LFO starts at when a note starts, 0...1.
|
|
||||||
public func setStartPhase(_ phase: Float) {
|
|
||||||
LFO.api.pointee.setStartPhase.unsafelyUnwrapped(pointer, phase)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The center value of the LFO output.
|
|
||||||
public func setCenter(_ center: Float) {
|
|
||||||
LFO.api.pointee.setCenter.unsafelyUnwrapped(pointer, center)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The amplitude of the LFO around its center.
|
|
||||||
public func setDepth(_ depth: Float) {
|
|
||||||
LFO.api.pointee.setDepth.unsafelyUnwrapped(pointer, depth)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For `.arpeggiator` LFOs: the sequence of values (in half-steps)
|
|
||||||
/// to step through.
|
|
||||||
public func setArpeggiation(_ steps: [Float]) {
|
|
||||||
var steps = steps
|
|
||||||
steps.withUnsafeMutableBufferPointer { buffer in
|
|
||||||
LFO.api.pointee.setArpeggiation.unsafelyUnwrapped(pointer, Int32(buffer.count),
|
|
||||||
buffer.baseAddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For `.function` LFOs: the Swift function providing the value. If
|
|
||||||
/// `interpolate` is `true`, values are interpolated between calls.
|
|
||||||
public func setFunction(interpolate: Bool = false, _ function: @escaping (LFO) -> Float) {
|
|
||||||
self.function = function
|
|
||||||
LFO.api.pointee.setFunction.unsafelyUnwrapped(pointer, { _, userdata in
|
|
||||||
guard let userdata else { return 0 }
|
|
||||||
let lfo = Unmanaged<LFO>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
return lfo.function?(lfo) ?? 0
|
|
||||||
}, Unmanaged.passUnretained(self).toOpaque(), interpolate ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits `holdoff` seconds after a note starts, then ramps the LFO
|
|
||||||
/// depth up over `rampTime` seconds.
|
|
||||||
public func setDelay(holdoff: Float, rampTime: Float) {
|
|
||||||
LFO.api.pointee.setDelay.unsafelyUnwrapped(pointer, holdoff, rampTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether the LFO phase restarts on every new note.
|
|
||||||
public func setRetrigger(_ flag: Bool) {
|
|
||||||
LFO.api.pointee.setRetrigger.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When `true`, the LFO runs globally instead of per-note.
|
|
||||||
public func setGlobal(_ global: Bool) {
|
|
||||||
LFO.api.pointee.setGlobal.unsafelyUnwrapped(pointer, global ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Seeds the random number generator used by `.sampleAndHold` LFOs.
|
|
||||||
public func setRandomSeed(_ seed: UInt16) {
|
|
||||||
LFO.api.pointee.setRandomSeed.unsafelyUnwrapped(pointer, seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var value: Float {
|
|
||||||
LFO.api.pointee.getValue.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Envelope
|
|
||||||
|
|
||||||
/// An ADSR envelope signal. Wraps `PDSynthEnvelope`.
|
|
||||||
public final class Envelope: SignalValue {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_envelope> { Playdate.envelopeAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// Creates an envelope with the given attack and decay times
|
|
||||||
/// (seconds), sustain level (0...1), and release time (seconds).
|
|
||||||
public init(attack: Float = 0, decay: Float = 0, sustain: Float = 1, release: Float = 0) {
|
|
||||||
let pointer = Envelope.api.pointee.newEnvelope.unsafelyUnwrapped(attack, decay, sustain, release)
|
|
||||||
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
super.init(pointer: pointer, isOwned: isOwned)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
Envelope.api.pointee.freeEnvelope.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setAttack(_ attack: Float) {
|
|
||||||
Envelope.api.pointee.setAttack.unsafelyUnwrapped(pointer, attack)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setDecay(_ decay: Float) {
|
|
||||||
Envelope.api.pointee.setDecay.unsafelyUnwrapped(pointer, decay)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setSustain(_ sustain: Float) {
|
|
||||||
Envelope.api.pointee.setSustain.unsafelyUnwrapped(pointer, sustain)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setRelease(_ release: Float) {
|
|
||||||
Envelope.api.pointee.setRelease.unsafelyUnwrapped(pointer, release)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When `true`, a new note while a note is playing does not restart
|
|
||||||
/// the envelope.
|
|
||||||
public func setLegato(_ flag: Bool) {
|
|
||||||
Envelope.api.pointee.setLegato.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When `true`, a new note restarts the envelope from zero instead of
|
|
||||||
/// its current value.
|
|
||||||
public func setRetrigger(_ flag: Bool) {
|
|
||||||
Envelope.api.pointee.setRetrigger.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bends the envelope's segments: 0 is linear, 1 is maximum curvature.
|
|
||||||
public func setCurvature(_ amount: Float) {
|
|
||||||
Envelope.api.pointee.setCurvature.unsafelyUnwrapped(pointer, amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How much note velocity scales the envelope's output.
|
|
||||||
public func setVelocitySensitivity(_ sensitivity: Float) {
|
|
||||||
Envelope.api.pointee.setVelocitySensitivity.unsafelyUnwrapped(pointer, sensitivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scales the envelope's rate by note: notes above `start` play the
|
|
||||||
/// envelope faster (up to `scaling` at `end` and beyond).
|
|
||||||
public func setRateScaling(_ scaling: Float, start: MIDINote, end: MIDINote) {
|
|
||||||
Envelope.api.pointee.setRateScaling.unsafelyUnwrapped(pointer, scaling, start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var value: Float {
|
|
||||||
Envelope.api.pointee.getValue.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - ControlSignal
|
|
||||||
|
|
||||||
/// A signal whose values are set on a sequence timeline. Wraps
|
|
||||||
/// `ControlSignal`.
|
|
||||||
public final class ControlSignal: SignalValue {
|
|
||||||
private static var api: UnsafePointer<playdate_control_signal> { Playdate.controlSignalAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
let pointer = ControlSignal.api.pointee.newSignal.unsafelyUnwrapped()
|
|
||||||
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
super.init(pointer: pointer, isOwned: isOwned)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
ControlSignal.api.pointee.freeSignal.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func clearEvents() {
|
|
||||||
ControlSignal.api.pointee.clearEvents.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a value at `step` in the signal's timeline. If `interpolate`
|
|
||||||
/// is `true`, the value ramps from the previous event.
|
|
||||||
public func addEvent(step: Int, value: Float, interpolate: Bool = false) {
|
|
||||||
ControlSignal.api.pointee.addEvent.unsafelyUnwrapped(pointer, Int32(step), value,
|
|
||||||
interpolate ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func removeEvent(step: Int) {
|
|
||||||
ControlSignal.api.pointee.removeEvent.unsafelyUnwrapped(pointer, Int32(step))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The MIDI controller number for signals loaded from a MIDI file.
|
|
||||||
public var midiControllerNumber: Int {
|
|
||||||
Int(ControlSignal.api.pointee.getMIDIControllerNumber.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
//
|
|
||||||
// SoundSource.swift
|
|
||||||
// SoundSource, FilePlayer, AudioSample, and SamplePlayer wrappers.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
|
||||||
|
|
||||||
extension Sound {
|
|
||||||
/// A source of audio: the base class of `FilePlayer`, `SamplePlayer`,
|
|
||||||
/// `Synth`, `DelayLineTap`, and `CallbackSource`. Wraps `SoundSource`.
|
|
||||||
public class Source {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_source> { Playdate.sourceAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The underlying C object. Set once, immediately after creation.
|
|
||||||
var pointer: OpaquePointer!
|
|
||||||
let isOwned: Bool
|
|
||||||
var finishCallback: ((Source) -> Void)?
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer?, isOwned: Bool) {
|
|
||||||
self.pointer = pointer
|
|
||||||
self.isOwned = isOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the playback volume for the left and right channels, 0...1.
|
|
||||||
public func setVolume(left: Float, right: Float) {
|
|
||||||
Source.api.pointee.setVolume.unsafelyUnwrapped(pointer, left, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the playback volume of both channels.
|
|
||||||
public func setVolume(_ volume: Float) {
|
|
||||||
setVolume(left: volume, right: volume)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The playback volume of the left and right channels.
|
|
||||||
public var volume: (left: Float, right: Float) {
|
|
||||||
var left: Float = 0, right: Float = 0
|
|
||||||
Source.api.pointee.getVolume.unsafelyUnwrapped(pointer, &left, &right)
|
|
||||||
return (left, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isPlaying: Bool {
|
|
||||||
Source.api.pointee.isPlaying.unsafelyUnwrapped(pointer) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a function called when the source finishes playing.
|
|
||||||
public func setFinishCallback(_ callback: ((Source) -> Void)?) {
|
|
||||||
finishCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
Source.api.pointee.setFinishCallback.unsafelyUnwrapped(pointer, { _, userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let source = Unmanaged<Source>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
source.finishCallback?(source)
|
|
||||||
}, Unmanaged.passUnretained(self).toOpaque())
|
|
||||||
} else {
|
|
||||||
Source.api.pointee.setFinishCallback.unsafelyUnwrapped(pointer, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A source that produces audio by calling back into Swift.
|
|
||||||
public final class CallbackSource: Source {
|
|
||||||
/// Fills the sample buffers and returns `true` if output was
|
|
||||||
/// produced. `right` is non-nil only for stereo sources.
|
|
||||||
public typealias Callback = (_ left: UnsafeMutableBufferPointer<Int16>,
|
|
||||||
_ right: UnsafeMutableBufferPointer<Int16>?) -> Bool
|
|
||||||
|
|
||||||
let callback: Callback
|
|
||||||
|
|
||||||
/// Every callback source is kept alive here while the C side may
|
|
||||||
/// still invoke its trampoline: from creation until it is removed
|
|
||||||
/// with `Sound.removeSource`/`Channel.removeSource`, or until its
|
|
||||||
/// owning channel is freed.
|
|
||||||
nonisolated(unsafe) static var live: [CallbackSource] = []
|
|
||||||
|
|
||||||
/// Releases the registration added by `adopt(pointer:)`.
|
|
||||||
static func release(_ source: Source) {
|
|
||||||
live.removeAll { $0 === source }
|
|
||||||
}
|
|
||||||
|
|
||||||
init(callback: @escaping Callback) {
|
|
||||||
self.callback = callback
|
|
||||||
super.init(pointer: nil, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
var contextPointer: UnsafeMutableRawPointer {
|
|
||||||
Unmanaged.passUnretained(self).toOpaque()
|
|
||||||
}
|
|
||||||
|
|
||||||
static let trampoline: @convention(c) (UnsafeMutableRawPointer?, UnsafeMutablePointer<Int16>?,
|
|
||||||
UnsafeMutablePointer<Int16>?, Int32) -> Int32 = { context, left, right, length in
|
|
||||||
guard let context, let left else { return 0 }
|
|
||||||
let source = Unmanaged<CallbackSource>.fromOpaque(context).takeUnretainedValue()
|
|
||||||
let leftBuffer = UnsafeMutableBufferPointer(start: left, count: Int(length))
|
|
||||||
let rightBuffer = right.map { UnsafeMutableBufferPointer(start: $0, count: Int(length)) }
|
|
||||||
return source.callback(leftBuffer, rightBuffer) ? 1 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attaches the C object created for this source.
|
|
||||||
func adopt(pointer: OpaquePointer) {
|
|
||||||
self.pointer = pointer
|
|
||||||
CallbackSource.live.append(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - FilePlayer
|
|
||||||
|
|
||||||
/// Streams audio from a file. Wraps `FilePlayer`.
|
|
||||||
public final class FilePlayer: Source {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_fileplayer> { Playdate.filePlayerAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
var loopCallback: ((FilePlayer) -> Void)?
|
|
||||||
var fadeCallback: ((FilePlayer) -> Void)?
|
|
||||||
var mp3DataSource: ((UnsafeMutableBufferPointer<UInt8>) -> Int)?
|
|
||||||
private var retainedRateModulator: SignalValue?
|
|
||||||
|
|
||||||
override init(pointer: OpaquePointer?, isOwned: Bool) {
|
|
||||||
super.init(pointer: pointer, isOwned: isOwned)
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init() {
|
|
||||||
self.init(pointer: FilePlayer.api.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a player and loads the audio file at `path`.
|
|
||||||
public convenience init(path: String) throws(PlaydateError) {
|
|
||||||
self.init()
|
|
||||||
try load(path: path)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
FilePlayer.api.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepares the player to stream the file at `path`.
|
|
||||||
public func load(path: String) throws(PlaydateError) {
|
|
||||||
let loaded = path.withPlaydateCString {
|
|
||||||
FilePlayer.api.pointee.loadIntoPlayer.unsafelyUnwrapped(pointer, $0) != 0
|
|
||||||
}
|
|
||||||
if !loaded {
|
|
||||||
throw PlaydateError(message: "unable to load audio file: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the length of the stream buffer, in seconds. Default 0.25.
|
|
||||||
public func setBufferLength(_ seconds: Float) {
|
|
||||||
FilePlayer.api.pointee.setBufferLength.unsafelyUnwrapped(pointer, seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts playback, looping `repeat` times; 0 loops endlessly.
|
|
||||||
@discardableResult
|
|
||||||
public func play(repeat repeatCount: Int = 1) -> Bool {
|
|
||||||
FilePlayer.api.pointee.play.unsafelyUnwrapped(pointer, Int32(repeatCount)) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
public func pause() {
|
|
||||||
FilePlayer.api.pointee.pause.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func stop() {
|
|
||||||
FilePlayer.api.pointee.stop.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The file's length in seconds.
|
|
||||||
public var length: Float {
|
|
||||||
FilePlayer.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The playback position in seconds.
|
|
||||||
public var offset: Float {
|
|
||||||
get { FilePlayer.api.pointee.getOffset.unsafelyUnwrapped(pointer) }
|
|
||||||
set { FilePlayer.api.pointee.setOffset.unsafelyUnwrapped(pointer, newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The playback rate; 1 is normal speed, negative values are not
|
|
||||||
/// supported.
|
|
||||||
public var rate: Float {
|
|
||||||
get { FilePlayer.api.pointee.getRate.unsafelyUnwrapped(pointer) }
|
|
||||||
set { FilePlayer.api.pointee.setRate.unsafelyUnwrapped(pointer, newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loops playback between `start` and `end` (seconds) while playing
|
|
||||||
/// with `repeat` 0. An `end` of 0 means the end of the file.
|
|
||||||
public func setLoopRange(start: Float, end: Float) {
|
|
||||||
FilePlayer.api.pointee.setLoopRange.unsafelyUnwrapped(pointer, start, end)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether playback underran because the file could not be read fast
|
|
||||||
/// enough.
|
|
||||||
public var didUnderrun: Bool {
|
|
||||||
FilePlayer.api.pointee.didUnderrun.unsafelyUnwrapped(pointer) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops playback (instead of looping the buffer) on underrun.
|
|
||||||
public func setStopOnUnderrun(_ flag: Bool) {
|
|
||||||
FilePlayer.api.pointee.setStopOnUnderrun.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a function called every time playback loops.
|
|
||||||
public func setLoopCallback(_ callback: ((FilePlayer) -> Void)?) {
|
|
||||||
loopCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
FilePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, { _, userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let player = Unmanaged<FilePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
player.loopCallback?(player)
|
|
||||||
}, Unmanaged.passUnretained(self).toOpaque())
|
|
||||||
} else {
|
|
||||||
FilePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fades the volume to the given levels over `length` sample frames,
|
|
||||||
/// then calls `completion`.
|
|
||||||
public func fadeVolume(left: Float, right: Float, length: Int32,
|
|
||||||
completion: ((FilePlayer) -> Void)? = nil) {
|
|
||||||
fadeCallback = completion
|
|
||||||
if completion != nil {
|
|
||||||
FilePlayer.api.pointee.fadeVolume.unsafelyUnwrapped(pointer, left, right, length, { _, userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let player = Unmanaged<FilePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
player.fadeCallback?(player)
|
|
||||||
}, Unmanaged.passUnretained(self).toOpaque())
|
|
||||||
} else {
|
|
||||||
FilePlayer.api.pointee.fadeVolume.unsafelyUnwrapped(pointer, left, right, length, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Streams MP3 data from a callback instead of a file. The callback
|
|
||||||
/// fills the buffer and returns the number of bytes written; return 0
|
|
||||||
/// to signal the end of the stream.
|
|
||||||
public func setMP3StreamSource(bufferLength: Float,
|
|
||||||
_ dataSource: @escaping (UnsafeMutableBufferPointer<UInt8>) -> Int) {
|
|
||||||
mp3DataSource = dataSource
|
|
||||||
FilePlayer.api.pointee.setMP3StreamSource.unsafelyUnwrapped(pointer, { data, bytes, userdata in
|
|
||||||
guard let userdata, let data else { return 0 }
|
|
||||||
let player = Unmanaged<FilePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
let buffer = UnsafeMutableBufferPointer(start: data, count: Int(bytes))
|
|
||||||
return Int32(player.mp3DataSource?(buffer) ?? 0)
|
|
||||||
}, Unmanaged.passUnretained(self).toOpaque(), bufferLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modulates the playback rate.
|
|
||||||
public var rateModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(FilePlayer.api.pointee.getRateModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedRateModulator = newValue
|
|
||||||
FilePlayer.api.pointee.setRateModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - AudioSample
|
|
||||||
|
|
||||||
/// Audio data loaded into memory. Wraps `AudioSample`.
|
|
||||||
public final class AudioSample {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_sample> { Playdate.sampleAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let isOwned: Bool
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
self.pointer = pointer
|
|
||||||
self.isOwned = isOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocates a sample buffer with room for `byteCount` bytes.
|
|
||||||
public convenience init(byteCount: Int) {
|
|
||||||
self.init(pointer: AudioSample.api.pointee.newSampleBuffer.unsafelyUnwrapped(
|
|
||||||
Int32(byteCount)).unsafelyUnwrapped, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the wav or aiff file at `path`.
|
|
||||||
public convenience init(path: String) throws(PlaydateError) {
|
|
||||||
let pointer = path.withPlaydateCString { AudioSample.api.pointee.load.unsafelyUnwrapped($0) }
|
|
||||||
guard let pointer else {
|
|
||||||
throw PlaydateError(message: "unable to load sample: \(path)")
|
|
||||||
}
|
|
||||||
self.init(pointer: pointer, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a sample referencing existing sample data. If
|
|
||||||
/// `freeWhenDone` is `true`, the OS frees `data` when the sample is
|
|
||||||
/// freed; otherwise the caller must keep `data` valid for the
|
|
||||||
/// sample's lifetime.
|
|
||||||
public convenience init?(data: UnsafeMutablePointer<UInt8>, format: Format,
|
|
||||||
sampleRate: UInt32, byteCount: Int, freeWhenDone: Bool) {
|
|
||||||
guard let pointer = AudioSample.api.pointee.newSampleFromData.unsafelyUnwrapped(
|
|
||||||
data, format.cValue, sampleRate, Int32(byteCount), freeWhenDone ? 1 : 0) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.init(pointer: pointer, isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
AudioSample.api.pointee.freeSample.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads the file at `path` into this sample's buffer.
|
|
||||||
public func load(path: String) throws(PlaydateError) {
|
|
||||||
let loaded = path.withPlaydateCString {
|
|
||||||
AudioSample.api.pointee.loadIntoSample.unsafelyUnwrapped(pointer, $0) != 0
|
|
||||||
}
|
|
||||||
if !loaded {
|
|
||||||
throw PlaydateError(message: "unable to load sample: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sample's raw data, format, and rate.
|
|
||||||
public var data: (data: UnsafeMutablePointer<UInt8>?, format: Format,
|
|
||||||
sampleRate: UInt32, byteLength: UInt32) {
|
|
||||||
var data: UnsafeMutablePointer<UInt8>?
|
|
||||||
var format = kSound16bitMono
|
|
||||||
var sampleRate: UInt32 = 0, byteLength: UInt32 = 0
|
|
||||||
AudioSample.api.pointee.getData.unsafelyUnwrapped(pointer, &data, &format, &sampleRate, &byteLength)
|
|
||||||
return (data, Format(format), sampleRate, byteLength)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sample's length in seconds.
|
|
||||||
public var length: Float {
|
|
||||||
AudioSample.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decompresses an ADPCM sample to 16-bit PCM so it can be used in a
|
|
||||||
/// synth. Returns `false` if there is not enough memory.
|
|
||||||
@discardableResult
|
|
||||||
public func decompress() -> Bool {
|
|
||||||
AudioSample.api.pointee.decompress.unsafelyUnwrapped(pointer) != 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - SamplePlayer
|
|
||||||
|
|
||||||
/// Plays an `AudioSample` from memory. Wraps `SamplePlayer`.
|
|
||||||
public final class SamplePlayer: Source {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_sampleplayer> { Playdate.samplePlayerAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
var loopCallback: ((SamplePlayer) -> Void)?
|
|
||||||
private var retainedSample: AudioSample?
|
|
||||||
private var retainedRateModulator: SignalValue?
|
|
||||||
|
|
||||||
override init(pointer: OpaquePointer?, isOwned: Bool) {
|
|
||||||
super.init(pointer: pointer, isOwned: isOwned)
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init() {
|
|
||||||
self.init(pointer: SamplePlayer.api.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a player for the sample at `path`.
|
|
||||||
public convenience init(path: String) throws(PlaydateError) {
|
|
||||||
self.init()
|
|
||||||
sample = try AudioSample(path: path)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
SamplePlayer.api.pointee.freePlayer.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sample to play.
|
|
||||||
public var sample: AudioSample? {
|
|
||||||
get { retainedSample }
|
|
||||||
set {
|
|
||||||
retainedSample = newValue
|
|
||||||
SamplePlayer.api.pointee.setSample.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts playback at `rate`, looping `repeat` times; 0 loops
|
|
||||||
/// endlessly, -1 loops ping-pong.
|
|
||||||
@discardableResult
|
|
||||||
public func play(repeat repeatCount: Int = 1, rate: Float = 1) -> Bool {
|
|
||||||
SamplePlayer.api.pointee.play.unsafelyUnwrapped(pointer, Int32(repeatCount), rate) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
public func stop() {
|
|
||||||
SamplePlayer.api.pointee.stop.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setPaused(_ paused: Bool) {
|
|
||||||
SamplePlayer.api.pointee.setPaused.unsafelyUnwrapped(pointer, paused ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sample's length in seconds.
|
|
||||||
public var length: Float {
|
|
||||||
SamplePlayer.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The playback position in seconds.
|
|
||||||
public var offset: Float {
|
|
||||||
get { SamplePlayer.api.pointee.getOffset.unsafelyUnwrapped(pointer) }
|
|
||||||
set { SamplePlayer.api.pointee.setOffset.unsafelyUnwrapped(pointer, newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The playback rate; 1 is normal speed, negative plays backward.
|
|
||||||
public var rate: Float {
|
|
||||||
get { SamplePlayer.api.pointee.getRate.unsafelyUnwrapped(pointer) }
|
|
||||||
set { SamplePlayer.api.pointee.setRate.unsafelyUnwrapped(pointer, newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restricts playback to the given range of sample frames.
|
|
||||||
public func setPlayRange(start: Int, end: Int) {
|
|
||||||
SamplePlayer.api.pointee.setPlayRange.unsafelyUnwrapped(pointer, Int32(start), Int32(end))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a function called every time playback loops.
|
|
||||||
public func setLoopCallback(_ callback: ((SamplePlayer) -> Void)?) {
|
|
||||||
loopCallback = callback
|
|
||||||
if callback != nil {
|
|
||||||
SamplePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, { _, userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let player = Unmanaged<SamplePlayer>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
player.loopCallback?(player)
|
|
||||||
}, Unmanaged.passUnretained(self).toOpaque())
|
|
||||||
} else {
|
|
||||||
SamplePlayer.api.pointee.setLoopCallback.unsafelyUnwrapped(pointer, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modulates the playback rate.
|
|
||||||
public var rateModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(SamplePlayer.api.pointee.getRateModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retainedRateModulator = newValue
|
|
||||||
SamplePlayer.api.pointee.setRateModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,614 +0,0 @@
|
|||||||
//
|
|
||||||
// SoundSynth.swift
|
|
||||||
// Synth, Instrument, SequenceTrack, and Sequence wrappers.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
|
||||||
|
|
||||||
extension Sound {
|
|
||||||
/// A synthesizer voice. Wraps `PDSynth`.
|
|
||||||
public final class Synth: Source {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_synth> { Playdate.synthAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
/// The synth's waveform.
|
|
||||||
public enum Waveform: UInt32, Sendable {
|
|
||||||
case square = 0
|
|
||||||
case triangle = 1
|
|
||||||
case sine = 2
|
|
||||||
case noise = 3
|
|
||||||
case sawtooth = 4
|
|
||||||
case poPhase = 5
|
|
||||||
case poDigital = 6
|
|
||||||
case poVosim = 7
|
|
||||||
|
|
||||||
var cValue: SoundWaveform { SoundWaveform(SoundWaveform.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Custom generator callbacks. Samples are in signed Q8.24 format.
|
|
||||||
public struct Generator {
|
|
||||||
/// Renders up to 256 sample frames into `left` (and `right` for
|
|
||||||
/// stereo generators). `rate` is the per-frame phase step in
|
|
||||||
/// Q0.32 format and `drate` its per-frame change. Returns the
|
|
||||||
/// number of frames rendered.
|
|
||||||
public var render: (_ left: UnsafeMutableBufferPointer<Int32>,
|
|
||||||
_ right: UnsafeMutableBufferPointer<Int32>?,
|
|
||||||
_ rate: UInt32, _ drate: Int32) -> Int
|
|
||||||
/// Called when a note starts. `length` is -1 for indefinite notes.
|
|
||||||
public var noteOn: ((_ note: MIDINote, _ velocity: Float, _ length: Float) -> Void)?
|
|
||||||
/// Called when a note is released (`stop == false`) or stopped
|
|
||||||
/// (`stop == true`).
|
|
||||||
public var release: ((_ stop: Bool) -> Void)?
|
|
||||||
/// Sets a generator parameter. Returns `true` if the parameter is
|
|
||||||
/// valid.
|
|
||||||
public var setParameter: ((_ parameter: Int, _ value: Float) -> Bool)?
|
|
||||||
|
|
||||||
public init(render: @escaping (_ left: UnsafeMutableBufferPointer<Int32>,
|
|
||||||
_ right: UnsafeMutableBufferPointer<Int32>?,
|
|
||||||
_ rate: UInt32, _ drate: Int32) -> Int,
|
|
||||||
noteOn: ((_ note: MIDINote, _ velocity: Float, _ length: Float) -> Void)? = nil,
|
|
||||||
release: ((_ stop: Bool) -> Void)? = nil,
|
|
||||||
setParameter: ((_ parameter: Int, _ value: Float) -> Bool)? = nil) {
|
|
||||||
self.render = render
|
|
||||||
self.noteOn = noteOn
|
|
||||||
self.release = release
|
|
||||||
self.setParameter = setParameter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class GeneratorBox {
|
|
||||||
let generator: Generator
|
|
||||||
let stereo: Bool
|
|
||||||
init(_ generator: Generator, stereo: Bool) {
|
|
||||||
self.generator = generator
|
|
||||||
self.stereo = stereo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var retainedSample: AudioSample?
|
|
||||||
private var retainedModulators: [SignalValue] = []
|
|
||||||
|
|
||||||
override init(pointer: OpaquePointer?, isOwned: Bool) {
|
|
||||||
super.init(pointer: pointer, isOwned: isOwned)
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init() {
|
|
||||||
self.init(pointer: Synth.api.pointee.newSynth.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init(waveform: Waveform) {
|
|
||||||
self.init()
|
|
||||||
setWaveform(waveform)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
Synth.api.pointee.freeSynth.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Copies the synth (and its generator, if any).
|
|
||||||
public func copy() -> Synth {
|
|
||||||
Synth(pointer: Synth.api.pointee.copy.unsafelyUnwrapped(pointer).unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Sound generation
|
|
||||||
|
|
||||||
public func setWaveform(_ waveform: Waveform) {
|
|
||||||
Synth.api.pointee.setWaveform.unsafelyUnwrapped(pointer, waveform.cValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plays a sample instead of a waveform. A nonzero sustain range
|
|
||||||
/// loops that part of the sample while the note is held.
|
|
||||||
public func setSample(_ sample: AudioSample, sustainStart: UInt32 = 0, sustainEnd: UInt32 = 0) {
|
|
||||||
retainedSample = sample
|
|
||||||
Synth.api.pointee.setSample.unsafelyUnwrapped(pointer, sample.pointer, sustainStart, sustainEnd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Uses a wavetable for the synth. `log2size` is the base-2 log of
|
|
||||||
/// each waveform's size (e.g. 8 for 256 samples).
|
|
||||||
public func setWavetable(_ sample: AudioSample, log2size: Int,
|
|
||||||
columns: Int, rows: Int) throws(PlaydateError) {
|
|
||||||
retainedSample = sample
|
|
||||||
guard Synth.api.pointee.setWavetable.unsafelyUnwrapped(
|
|
||||||
pointer, sample.pointer, Int32(log2size), Int32(columns), Int32(rows)) != 0 else {
|
|
||||||
throw PlaydateError(message: "invalid wavetable dimensions")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides audio via custom Swift callbacks.
|
|
||||||
public func setGenerator(stereo: Bool, _ generator: Generator) {
|
|
||||||
let box = Unmanaged.passRetained(GeneratorBox(generator, stereo: stereo))
|
|
||||||
Synth.api.pointee.setGenerator.unsafelyUnwrapped(
|
|
||||||
pointer, stereo ? 1 : 0,
|
|
||||||
{ userdata, left, right, nsamples, rate, drate in
|
|
||||||
guard let userdata, let left else { return 0 }
|
|
||||||
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
let leftBuffer = UnsafeMutableBufferPointer(start: left, count: Int(nsamples))
|
|
||||||
let rightBuffer = right.map { UnsafeMutableBufferPointer(start: $0, count: Int(nsamples)) }
|
|
||||||
return Int32(box.generator.render(leftBuffer, rightBuffer, rate, drate))
|
|
||||||
},
|
|
||||||
{ userdata, note, velocity, length in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
box.generator.noteOn?(note, velocity, length)
|
|
||||||
},
|
|
||||||
{ userdata, stop in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
box.generator.release?(stop != 0)
|
|
||||||
},
|
|
||||||
{ userdata, parameter, value in
|
|
||||||
guard let userdata else { return 0 }
|
|
||||||
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
return box.generator.setParameter?(Int(parameter), value) == true ? 1 : 0
|
|
||||||
},
|
|
||||||
{ userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
Unmanaged<GeneratorBox>.fromOpaque(userdata).release()
|
|
||||||
},
|
|
||||||
{ userdata in
|
|
||||||
guard let userdata else { return nil }
|
|
||||||
let box = Unmanaged<GeneratorBox>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
return Unmanaged.passRetained(GeneratorBox(box.generator, stereo: box.stereo)).toOpaque()
|
|
||||||
},
|
|
||||||
box.toOpaque())
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Envelope
|
|
||||||
|
|
||||||
public func setAttackTime(_ attack: Float) {
|
|
||||||
Synth.api.pointee.setAttackTime.unsafelyUnwrapped(pointer, attack)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setDecayTime(_ decay: Float) {
|
|
||||||
Synth.api.pointee.setDecayTime.unsafelyUnwrapped(pointer, decay)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setSustainLevel(_ sustain: Float) {
|
|
||||||
Synth.api.pointee.setSustainLevel.unsafelyUnwrapped(pointer, sustain)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setReleaseTime(_ release: Float) {
|
|
||||||
Synth.api.pointee.setReleaseTime.unsafelyUnwrapped(pointer, release)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The synth's amplitude envelope. Owned by the synth.
|
|
||||||
public var envelope: Envelope? {
|
|
||||||
guard let envelope = Synth.api.pointee.getEnvelope.unsafelyUnwrapped(pointer) else { return nil }
|
|
||||||
return Envelope(pointer: envelope, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the synth's envelope so it plays at constant volume.
|
|
||||||
public func clearEnvelope() {
|
|
||||||
Synth.api.pointee.clearEnvelope.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Modulation
|
|
||||||
|
|
||||||
/// Transposes played notes by `halfSteps` (fractional values allowed).
|
|
||||||
public func setTranspose(_ halfSteps: Float) {
|
|
||||||
Synth.api.pointee.setTranspose.unsafelyUnwrapped(pointer, halfSteps)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var frequencyModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(Synth.api.pointee.getFrequencyModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retain(newValue)
|
|
||||||
Synth.api.pointee.setFrequencyModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var amplitudeModulator: SignalValue? {
|
|
||||||
get { SignalValue.wrap(Synth.api.pointee.getAmplitudeModulator.unsafelyUnwrapped(pointer)) }
|
|
||||||
set {
|
|
||||||
retain(newValue)
|
|
||||||
Synth.api.pointee.setAmplitudeModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of parameters the synth's generator supports.
|
|
||||||
public var parameterCount: Int {
|
|
||||||
Int(Synth.api.pointee.getParameterCount.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a generator parameter. Returns `false` if the parameter is
|
|
||||||
/// invalid.
|
|
||||||
@discardableResult
|
|
||||||
public func setParameter(_ parameter: Int, value: Float) -> Bool {
|
|
||||||
Synth.api.pointee.setParameter.unsafelyUnwrapped(pointer, Int32(parameter), value) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setParameterModulator(_ parameter: Int, _ modulator: SignalValue?) {
|
|
||||||
retain(modulator)
|
|
||||||
Synth.api.pointee.setParameterModulator.unsafelyUnwrapped(pointer, Int32(parameter),
|
|
||||||
modulator?.pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func parameterModulator(_ parameter: Int) -> SignalValue? {
|
|
||||||
SignalValue.wrap(Synth.api.pointee.getParameterModulator.unsafelyUnwrapped(pointer, Int32(parameter)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func retain(_ modulator: SignalValue?) {
|
|
||||||
if let modulator, !retainedModulators.contains(where: { $0 === modulator }) {
|
|
||||||
retainedModulators.append(modulator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Playing
|
|
||||||
|
|
||||||
/// Plays a note at `frequency` Hz. `length` is in seconds; `nil`
|
|
||||||
/// plays until `noteOff()`. `when` is the audio-clock time to start,
|
|
||||||
/// or 0 for immediately.
|
|
||||||
public func playNote(frequency: Float, velocity: Float = 1,
|
|
||||||
length: Float? = nil, when: UInt32 = 0) {
|
|
||||||
Synth.api.pointee.playNote.unsafelyUnwrapped(pointer, frequency, velocity, length ?? -1, when)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plays a MIDI note, where 60 is middle C.
|
|
||||||
public func playMIDINote(_ note: MIDINote, velocity: Float = 1,
|
|
||||||
length: Float? = nil, when: UInt32 = 0) {
|
|
||||||
Synth.api.pointee.playMIDINote.unsafelyUnwrapped(pointer, note, velocity, length ?? -1, when)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Releases the playing note at time `when`, or immediately if 0.
|
|
||||||
public func noteOff(when: UInt32 = 0) {
|
|
||||||
Synth.api.pointee.noteOff.unsafelyUnwrapped(pointer, when)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops the synth immediately, without playing the release phase.
|
|
||||||
public func stop() {
|
|
||||||
Synth.api.pointee.stop.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Instrument
|
|
||||||
|
|
||||||
/// A bank of synth voices for playing a sequence track. Wraps
|
|
||||||
/// `PDSynthInstrument`.
|
|
||||||
public final class Instrument {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_instrument> { Playdate.instrumentAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let isOwned: Bool
|
|
||||||
private var retainedVoices: [Synth] = []
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
self.pointer = pointer
|
|
||||||
self.isOwned = isOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init() {
|
|
||||||
self.init(pointer: Instrument.api.pointee.newInstrument.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
Instrument.api.pointee.freeInstrument.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a voice to the instrument, handling notes in
|
|
||||||
/// `rangeStart...rangeEnd` (0...127 handles all notes), transposed by
|
|
||||||
/// `transpose` half-steps.
|
|
||||||
@discardableResult
|
|
||||||
public func addVoice(_ synth: Synth, rangeStart: MIDINote = 0, rangeEnd: MIDINote = 127,
|
|
||||||
transpose: Float = 0) -> Bool {
|
|
||||||
let added = Instrument.api.pointee.addVoice.unsafelyUnwrapped(
|
|
||||||
pointer, synth.pointer, rangeStart, rangeEnd, transpose) != 0
|
|
||||||
if added, !retainedVoices.contains(where: { $0 === synth }) {
|
|
||||||
retainedVoices.append(synth)
|
|
||||||
}
|
|
||||||
return added
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plays a note at `frequency` Hz on an available voice. Returns the
|
|
||||||
/// synth used, if any.
|
|
||||||
@discardableResult
|
|
||||||
public func playNote(frequency: Float, velocity: Float = 1,
|
|
||||||
length: Float? = nil, when: UInt32 = 0) -> Synth? {
|
|
||||||
let synth = Instrument.api.pointee.playNote.unsafelyUnwrapped(
|
|
||||||
pointer, frequency, velocity, length ?? -1, when)
|
|
||||||
return voice(for: synth)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plays a MIDI note on an available voice. Returns the synth used.
|
|
||||||
@discardableResult
|
|
||||||
public func playMIDINote(_ note: MIDINote, velocity: Float = 1,
|
|
||||||
length: Float? = nil, when: UInt32 = 0) -> Synth? {
|
|
||||||
let synth = Instrument.api.pointee.playMIDINote.unsafelyUnwrapped(
|
|
||||||
pointer, note, velocity, length ?? -1, when)
|
|
||||||
return voice(for: synth)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func voice(for pointer: OpaquePointer?) -> Synth? {
|
|
||||||
guard let pointer else { return nil }
|
|
||||||
if let voice = retainedVoices.first(where: { $0.pointer == pointer }) {
|
|
||||||
return voice
|
|
||||||
}
|
|
||||||
return Synth(pointer: pointer, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bends played notes by `bend` × the pitch bend range.
|
|
||||||
public func setPitchBend(_ bend: Float) {
|
|
||||||
Instrument.api.pointee.setPitchBend.unsafelyUnwrapped(pointer, bend)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setPitchBendRange(halfSteps: Float) {
|
|
||||||
Instrument.api.pointee.setPitchBendRange.unsafelyUnwrapped(pointer, halfSteps)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setTranspose(halfSteps: Float) {
|
|
||||||
Instrument.api.pointee.setTranspose.unsafelyUnwrapped(pointer, halfSteps)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Releases the voice playing `note` at time `when` (0 = now).
|
|
||||||
public func noteOff(_ note: MIDINote, when: UInt32 = 0) {
|
|
||||||
Instrument.api.pointee.noteOff.unsafelyUnwrapped(pointer, note, when)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func allNotesOff(when: UInt32 = 0) {
|
|
||||||
Instrument.api.pointee.allNotesOff.unsafelyUnwrapped(pointer, when)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setVolume(left: Float, right: Float) {
|
|
||||||
Instrument.api.pointee.setVolume.unsafelyUnwrapped(pointer, left, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var volume: (left: Float, right: Float) {
|
|
||||||
var left: Float = 0, right: Float = 0
|
|
||||||
Instrument.api.pointee.getVolume.unsafelyUnwrapped(pointer, &left, &right)
|
|
||||||
return (left, right)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var activeVoiceCount: Int {
|
|
||||||
Int(Instrument.api.pointee.activeVoiceCount.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - SequenceTrack
|
|
||||||
|
|
||||||
/// A track of notes played by an instrument. Wraps `SequenceTrack`.
|
|
||||||
public final class SequenceTrack {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_track> { Playdate.trackAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
let isOwned: Bool
|
|
||||||
private var retainedInstrument: Instrument?
|
|
||||||
|
|
||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
|
||||||
self.pointer = pointer
|
|
||||||
self.isOwned = isOwned
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init() {
|
|
||||||
self.init(pointer: SequenceTrack.api.pointee.newTrack.unsafelyUnwrapped().unsafelyUnwrapped,
|
|
||||||
isOwned: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if isOwned {
|
|
||||||
SequenceTrack.api.pointee.freeTrack.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The instrument that plays this track's notes.
|
|
||||||
public var instrument: Instrument? {
|
|
||||||
get {
|
|
||||||
if let retainedInstrument { return retainedInstrument }
|
|
||||||
guard let instrument = SequenceTrack.api.pointee.getInstrument.unsafelyUnwrapped(pointer) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Instrument(pointer: instrument, isOwned: false)
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
retainedInstrument = newValue
|
|
||||||
SequenceTrack.api.pointee.setInstrument.unsafelyUnwrapped(pointer, newValue?.pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a note starting at `step`, lasting `length` steps.
|
|
||||||
public func addNote(step: UInt32, length: UInt32, note: MIDINote, velocity: Float = 1) {
|
|
||||||
SequenceTrack.api.pointee.addNoteEvent.unsafelyUnwrapped(pointer, step, length, note, velocity)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func removeNote(step: UInt32, note: MIDINote) {
|
|
||||||
SequenceTrack.api.pointee.removeNoteEvent.unsafelyUnwrapped(pointer, step, note)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func clearNotes() {
|
|
||||||
SequenceTrack.api.pointee.clearNotes.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The track's length in steps, including the tail of the last note.
|
|
||||||
public var length: UInt32 {
|
|
||||||
SequenceTrack.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The index of the first note at or after `step`.
|
|
||||||
public func indexForStep(_ step: UInt32) -> Int {
|
|
||||||
Int(SequenceTrack.api.pointee.getIndexForStep.unsafelyUnwrapped(pointer, step))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The note at `index`, or `nil` if the index is out of range.
|
|
||||||
public func note(at index: Int) -> (step: UInt32, length: UInt32,
|
|
||||||
note: MIDINote, velocity: Float)? {
|
|
||||||
var step: UInt32 = 0, length: UInt32 = 0
|
|
||||||
var note: MIDINote = 0
|
|
||||||
var velocity: Float = 0
|
|
||||||
guard SequenceTrack.api.pointee.getNoteAtIndex.unsafelyUnwrapped(
|
|
||||||
pointer, Int32(index), &step, &length, ¬e, &velocity) != 0 else { return nil }
|
|
||||||
return (step, length, note, velocity)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The number of control signals on the track.
|
|
||||||
public var controlSignalCount: Int {
|
|
||||||
Int(SequenceTrack.api.pointee.getControlSignalCount.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The control signal at `index`. Owned by the track.
|
|
||||||
public func controlSignal(at index: Int) -> ControlSignal? {
|
|
||||||
guard let signal = SequenceTrack.api.pointee.getControlSignal.unsafelyUnwrapped(
|
|
||||||
pointer, Int32(index)) else { return nil }
|
|
||||||
return ControlSignal(pointer: signal, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The control signal for MIDI controller `controller`, optionally
|
|
||||||
/// creating it. Owned by the track.
|
|
||||||
public func signalForController(_ controller: Int, create: Bool = false) -> ControlSignal? {
|
|
||||||
guard let signal = SequenceTrack.api.pointee.getSignalForController.unsafelyUnwrapped(
|
|
||||||
pointer, Int32(controller), create ? 1 : 0) else { return nil }
|
|
||||||
return ControlSignal(pointer: signal, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func clearControlEvents() {
|
|
||||||
SequenceTrack.api.pointee.clearControlEvents.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The maximum number of simultaneous notes in the track.
|
|
||||||
public var polyphony: Int {
|
|
||||||
Int(SequenceTrack.api.pointee.getPolyphony.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
public var activeVoiceCount: Int {
|
|
||||||
Int(SequenceTrack.api.pointee.activeVoiceCount.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setMuted(_ muted: Bool) {
|
|
||||||
SequenceTrack.api.pointee.setMuted.unsafelyUnwrapped(pointer, muted ? 1 : 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Sequence
|
|
||||||
|
|
||||||
/// A collection of tracks with tempo and loop control, playable from a
|
|
||||||
/// MIDI file. Wraps `SoundSequence`.
|
|
||||||
public final class Sequence {
|
|
||||||
private static var api: UnsafePointer<playdate_sound_sequence> { Playdate.sequenceAPI.unsafelyUnwrapped }
|
|
||||||
|
|
||||||
let pointer: OpaquePointer
|
|
||||||
private var retainedTracks: [SequenceTrack] = []
|
|
||||||
var finishCallback: ((Sequence) -> Void)?
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
pointer = Sequence.api.pointee.newSequence.unsafelyUnwrapped().unsafelyUnwrapped
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a sequence and loads the MIDI file at `path`.
|
|
||||||
public convenience init(midiFilePath: String) throws(PlaydateError) {
|
|
||||||
self.init()
|
|
||||||
try loadMIDIFile(path: midiFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
Sequence.api.pointee.freeSequence.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func loadMIDIFile(path: String) throws(PlaydateError) {
|
|
||||||
let loaded = path.withPlaydateCString {
|
|
||||||
Sequence.api.pointee.loadMIDIFile.unsafelyUnwrapped(pointer, $0) != 0
|
|
||||||
}
|
|
||||||
if !loaded {
|
|
||||||
throw PlaydateError(message: "unable to load MIDI file: \(path)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts playback. `completion` is called when the sequence finishes.
|
|
||||||
public func play(completion: ((Sequence) -> Void)? = nil) {
|
|
||||||
finishCallback = completion
|
|
||||||
if completion != nil {
|
|
||||||
Sequence.api.pointee.play.unsafelyUnwrapped(pointer, { _, userdata in
|
|
||||||
guard let userdata else { return }
|
|
||||||
let sequence = Unmanaged<Sequence>.fromOpaque(userdata).takeUnretainedValue()
|
|
||||||
sequence.finishCallback?(sequence)
|
|
||||||
}, Unmanaged.passUnretained(self).toOpaque())
|
|
||||||
} else {
|
|
||||||
Sequence.api.pointee.play.unsafelyUnwrapped(pointer, nil, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public func stop() {
|
|
||||||
Sequence.api.pointee.stop.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isPlaying: Bool {
|
|
||||||
Sequence.api.pointee.isPlaying.unsafelyUnwrapped(pointer) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The playback position, in samples.
|
|
||||||
public var time: UInt32 {
|
|
||||||
get { Sequence.api.pointee.getTime.unsafelyUnwrapped(pointer) }
|
|
||||||
set { Sequence.api.pointee.setTime.unsafelyUnwrapped(pointer, newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The tempo, in steps per second.
|
|
||||||
public var tempo: Float {
|
|
||||||
get { Sequence.api.pointee.getTempo.unsafelyUnwrapped(pointer) }
|
|
||||||
set { Sequence.api.pointee.setTempo.unsafelyUnwrapped(pointer, newValue) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sequence's length in steps, including the tail of the last note.
|
|
||||||
public var length: UInt32 {
|
|
||||||
Sequence.api.pointee.getLength.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loops the range `loopStart..<loopEnd` (steps) `loops` times while
|
|
||||||
/// playing; 0 loops endlessly.
|
|
||||||
public func setLoops(start: Int, end: Int, count: Int = 0) {
|
|
||||||
Sequence.api.pointee.setLoops.unsafelyUnwrapped(pointer, Int32(start), Int32(end), Int32(count))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The current step, and the time offset (in samples) into that step.
|
|
||||||
public var currentStep: (step: Int, timeOffset: Int) {
|
|
||||||
var timeOffset: Int32 = 0
|
|
||||||
let step = Sequence.api.pointee.getCurrentStep.unsafelyUnwrapped(pointer, &timeOffset)
|
|
||||||
return (Int(step), Int(timeOffset))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves playback to the given step. If `playNotes` is `true`, notes
|
|
||||||
/// at the position (that started before it) are played.
|
|
||||||
public func setCurrentStep(_ step: Int, timeOffset: Int = 0, playNotes: Bool = false) {
|
|
||||||
Sequence.api.pointee.setCurrentStep.unsafelyUnwrapped(pointer, Int32(step),
|
|
||||||
Int32(timeOffset), playNotes ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Tracks
|
|
||||||
|
|
||||||
public var trackCount: Int {
|
|
||||||
Int(Sequence.api.pointee.getTrackCount.unsafelyUnwrapped(pointer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a new track to the sequence. The track is owned by the
|
|
||||||
/// sequence.
|
|
||||||
@discardableResult
|
|
||||||
public func addTrack() -> SequenceTrack {
|
|
||||||
let track = SequenceTrack(
|
|
||||||
pointer: Sequence.api.pointee.addTrack.unsafelyUnwrapped(pointer).unsafelyUnwrapped,
|
|
||||||
isOwned: false)
|
|
||||||
retainedTracks.append(track)
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The track at `index`. Owned by the sequence.
|
|
||||||
public func track(at index: Int) -> SequenceTrack? {
|
|
||||||
guard let track = Sequence.api.pointee.getTrackAtIndex.unsafelyUnwrapped(
|
|
||||||
pointer, UInt32(index)) else { return nil }
|
|
||||||
return SequenceTrack(pointer: track, isOwned: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Installs `track` at `index`.
|
|
||||||
public func setTrack(_ track: SequenceTrack, at index: Int) {
|
|
||||||
if !retainedTracks.contains(where: { $0 === track }) {
|
|
||||||
retainedTracks.append(track)
|
|
||||||
}
|
|
||||||
Sequence.api.pointee.setTrackAtIndex.unsafelyUnwrapped(pointer, track.pointer, UInt32(index))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Releases every playing note in the sequence.
|
|
||||||
public func allNotesOff() {
|
|
||||||
Sequence.api.pointee.allNotesOff.unsafelyUnwrapped(pointer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,7 @@
|
|||||||
//
|
|
||||||
// Sprite.swift
|
|
||||||
// Wraps `playdate->sprite` (pd_api_sprite.h).
|
|
||||||
//
|
|
||||||
// The binding stores a back-reference to each `Sprite` wrapper in the
|
|
||||||
// underlying `LCDSprite`'s userdata slot, so callbacks and queries can
|
|
||||||
// recover the wrapper. Do not mix these wrappers with C code that sets its
|
|
||||||
// own sprite userdata; use `Sprite.userdata` for per-sprite storage instead.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
private var spriteAPI: UnsafePointer<playdate_sprite> { Playdate.spriteAPI.unsafelyUnwrapped }
|
private var spriteAPI: UnsafePointer<playdate_sprite> { Playdate.spriteAPI.unsafelyUnwrapped }
|
||||||
|
|
||||||
/// A floating-point rectangle mirroring `PDRect`.
|
|
||||||
public struct Rect: Sendable {
|
|
||||||
public var x: Float
|
|
||||||
public var y: Float
|
|
||||||
public var width: Float
|
|
||||||
public var height: Float
|
|
||||||
|
|
||||||
public init(x: Float, y: Float, width: Float, height: Float) {
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ rect: PDRect) {
|
|
||||||
self.init(x: rect.x, y: rect.y, width: rect.width, height: rect.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
var cValue: PDRect { PDRect(x: x, y: y, width: width, height: height) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A sprite: a drawable object with position, z-order, and collision
|
/// A sprite: a drawable object with position, z-order, and collision
|
||||||
/// support. Wraps `LCDSprite`. Static members wrap the global sprite
|
/// support. Wraps `LCDSprite`. Static members wrap the global sprite
|
||||||
/// system functions.
|
/// system functions.
|
||||||
@@ -102,79 +71,6 @@ public final class Sprite {
|
|||||||
return copy
|
return copy
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Types
|
|
||||||
|
|
||||||
/// How a sprite reacts when a collision occurs.
|
|
||||||
public enum CollisionResponse: UInt32, Sendable {
|
|
||||||
case slide = 0
|
|
||||||
case freeze = 1
|
|
||||||
case overlap = 2
|
|
||||||
case bounce = 3
|
|
||||||
|
|
||||||
init(_ response: SpriteCollisionResponseType) {
|
|
||||||
self = CollisionResponse(rawValue: UInt32(response.rawValue)) ?? .freeze
|
|
||||||
}
|
|
||||||
var cValue: SpriteCollisionResponseType { SpriteCollisionResponseType(SpriteCollisionResponseType.RawValue(rawValue)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about a single collision, mirroring `SpriteCollisionInfo`.
|
|
||||||
public struct CollisionInfo {
|
|
||||||
/// The sprite being moved.
|
|
||||||
public let sprite: Sprite
|
|
||||||
/// The sprite it collided with.
|
|
||||||
public let other: Sprite
|
|
||||||
/// The collision response used.
|
|
||||||
public let response: CollisionResponse
|
|
||||||
/// `true` if the sprites were overlapping when the collision
|
|
||||||
/// started; `false` if the sprite tunneled through.
|
|
||||||
public let overlaps: Bool
|
|
||||||
/// How far along the movement (0...1) the collision occurred.
|
|
||||||
public let ti: Float
|
|
||||||
/// The difference between the requested and actual positions.
|
|
||||||
public let move: (x: Float, y: Float)
|
|
||||||
/// The collision normal (each component -1, 0, or 1).
|
|
||||||
public let normal: (x: Int, y: Int)
|
|
||||||
/// Where the sprite started touching `other`.
|
|
||||||
public let touch: (x: Float, y: Float)
|
|
||||||
/// The sprite's rect at the moment of the touch.
|
|
||||||
public let spriteRect: Rect
|
|
||||||
/// `other`'s rect at the moment of the touch.
|
|
||||||
public let otherRect: Rect
|
|
||||||
|
|
||||||
init(_ info: SpriteCollisionInfo) {
|
|
||||||
sprite = Sprite.wrapper(for: info.sprite)
|
|
||||||
other = Sprite.wrapper(for: info.other)
|
|
||||||
response = CollisionResponse(info.responseType)
|
|
||||||
overlaps = info.overlaps != 0
|
|
||||||
ti = info.ti
|
|
||||||
move = (info.move.x, info.move.y)
|
|
||||||
normal = (Int(info.normal.x), Int(info.normal.y))
|
|
||||||
touch = (info.touch.x, info.touch.y)
|
|
||||||
spriteRect = Rect(info.spriteRect)
|
|
||||||
otherRect = Rect(info.otherRect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information about a sprite intersected by a line segment,
|
|
||||||
/// mirroring `SpriteQueryInfo`.
|
|
||||||
public struct QueryInfo {
|
|
||||||
public let sprite: Sprite
|
|
||||||
/// How far along the segment (0...1) the segment enters the sprite.
|
|
||||||
public let ti1: Float
|
|
||||||
/// How far along the segment (0...1) the segment exits the sprite.
|
|
||||||
public let ti2: Float
|
|
||||||
public let entryPoint: (x: Float, y: Float)
|
|
||||||
public let exitPoint: (x: Float, y: Float)
|
|
||||||
|
|
||||||
init(_ info: SpriteQueryInfo) {
|
|
||||||
sprite = Sprite.wrapper(for: info.sprite)
|
|
||||||
ti1 = info.ti1
|
|
||||||
ti2 = info.ti2
|
|
||||||
entryPoint = (info.entryPoint.x, info.entryPoint.y)
|
|
||||||
exitPoint = (info.exitPoint.x, info.exitPoint.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Display list
|
// MARK: - Display list
|
||||||
|
|
||||||
/// Sprites currently added to the display list, kept alive here.
|
/// Sprites currently added to the display list, kept alive here.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sprite {
|
||||||
|
/// How a sprite reacts when a collision occurs.
|
||||||
|
public enum CollisionResponse: UInt32, Sendable {
|
||||||
|
case slide = 0
|
||||||
|
case freeze = 1
|
||||||
|
case overlap = 2
|
||||||
|
case bounce = 3
|
||||||
|
|
||||||
|
init(_ response: SpriteCollisionResponseType) {
|
||||||
|
self = CollisionResponse(rawValue: UInt32(response.rawValue)) ?? .freeze
|
||||||
|
}
|
||||||
|
var cValue: SpriteCollisionResponseType { SpriteCollisionResponseType(SpriteCollisionResponseType.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sprite {
|
||||||
|
/// Information about a single collision, mirroring `SpriteCollisionInfo`.
|
||||||
|
public struct CollisionInfo {
|
||||||
|
/// The sprite being moved.
|
||||||
|
public let sprite: Sprite
|
||||||
|
/// The sprite it collided with.
|
||||||
|
public let other: Sprite
|
||||||
|
/// The collision response used.
|
||||||
|
public let response: CollisionResponse
|
||||||
|
/// `true` if the sprites were overlapping when the collision
|
||||||
|
/// started; `false` if the sprite tunneled through.
|
||||||
|
public let overlaps: Bool
|
||||||
|
/// How far along the movement (0...1) the collision occurred.
|
||||||
|
public let ti: Float
|
||||||
|
/// The difference between the requested and actual positions.
|
||||||
|
public let move: (x: Float, y: Float)
|
||||||
|
/// The collision normal (each component -1, 0, or 1).
|
||||||
|
public let normal: (x: Int, y: Int)
|
||||||
|
/// Where the sprite started touching `other`.
|
||||||
|
public let touch: (x: Float, y: Float)
|
||||||
|
/// The sprite's rect at the moment of the touch.
|
||||||
|
public let spriteRect: Rect
|
||||||
|
/// `other`'s rect at the moment of the touch.
|
||||||
|
public let otherRect: Rect
|
||||||
|
|
||||||
|
init(_ info: SpriteCollisionInfo) {
|
||||||
|
sprite = Sprite.wrapper(for: info.sprite)
|
||||||
|
other = Sprite.wrapper(for: info.other)
|
||||||
|
response = CollisionResponse(info.responseType)
|
||||||
|
overlaps = info.overlaps != 0
|
||||||
|
ti = info.ti
|
||||||
|
move = (info.move.x, info.move.y)
|
||||||
|
normal = (Int(info.normal.x), Int(info.normal.y))
|
||||||
|
touch = (info.touch.x, info.touch.y)
|
||||||
|
spriteRect = Rect(info.spriteRect)
|
||||||
|
otherRect = Rect(info.otherRect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension Sprite {
|
||||||
|
/// Information about a sprite intersected by a line segment,
|
||||||
|
/// mirroring `SpriteQueryInfo`.
|
||||||
|
public struct QueryInfo {
|
||||||
|
public let sprite: Sprite
|
||||||
|
/// How far along the segment (0...1) the segment enters the sprite.
|
||||||
|
public let ti1: Float
|
||||||
|
/// How far along the segment (0...1) the segment exits the sprite.
|
||||||
|
public let ti2: Float
|
||||||
|
public let entryPoint: (x: Float, y: Float)
|
||||||
|
public let exitPoint: (x: Float, y: Float)
|
||||||
|
|
||||||
|
init(_ info: SpriteQueryInfo) {
|
||||||
|
sprite = Sprite.wrapper(for: info.sprite)
|
||||||
|
ti1 = info.ti1
|
||||||
|
ti2 = info.ti2
|
||||||
|
entryPoint = (info.entryPoint.x, info.entryPoint.y)
|
||||||
|
exitPoint = (info.exitPoint.x, info.exitPoint.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
/// A floating-point rectangle mirroring `PDRect`.
|
||||||
|
public struct Rect: Sendable {
|
||||||
|
public var x: Float
|
||||||
|
public var y: Float
|
||||||
|
public var width: Float
|
||||||
|
public var height: Float
|
||||||
|
|
||||||
|
public init(x: Float, y: Float, width: Float, height: Float) {
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ rect: PDRect) {
|
||||||
|
self.init(x: rect.x, y: rect.y, width: rect.width, height: rect.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cValue: PDRect { PDRect(x: x, y: y, width: width, height: height) }
|
||||||
|
}
|
||||||
@@ -1,12 +1,3 @@
|
|||||||
//
|
|
||||||
// Support.swift
|
|
||||||
// Internal helpers shared by the wrappers.
|
|
||||||
//
|
|
||||||
// C-string conversions are implemented manually (rather than with
|
|
||||||
// `String(cString:)` / `withCString`) so the module stays within the
|
|
||||||
// Embedded Swift subset used for device builds.
|
|
||||||
//
|
|
||||||
|
|
||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension System {
|
||||||
|
/// An item added to the system menu. Keep no more than three items at once.
|
||||||
|
public final class MenuItem {
|
||||||
|
let pointer: OpaquePointer
|
||||||
|
var onSelect: (MenuItem) -> Void
|
||||||
|
/// Retains C strings passed to the OS for option titles.
|
||||||
|
private var retainedOptionTitles: [UnsafeMutablePointer<CChar>] = []
|
||||||
|
|
||||||
|
init?(pointer: OpaquePointer?,
|
||||||
|
retainedOptionTitles: [UnsafeMutablePointer<CChar>] = [],
|
||||||
|
onSelect: @escaping (MenuItem) -> Void) {
|
||||||
|
guard let pointer else {
|
||||||
|
for title in retainedOptionTitles { title.deallocate() }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.pointer = pointer
|
||||||
|
self.retainedOptionTitles = retainedOptionTitles
|
||||||
|
self.onSelect = onSelect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The menu item's title.
|
||||||
|
public var title: String {
|
||||||
|
get {
|
||||||
|
String(playdateCString: Playdate.systemAPI.pointee.getMenuItemTitle.unsafelyUnwrapped(pointer)) ?? ""
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
newValue.withPlaydateCString {
|
||||||
|
Playdate.systemAPI.pointee.setMenuItemTitle.unsafelyUnwrapped(pointer, $0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For checkmark items this is 0 or 1; for option items it is the
|
||||||
|
/// index of the selected option.
|
||||||
|
public var value: Int {
|
||||||
|
get { Int(Playdate.systemAPI.pointee.getMenuItemValue.unsafelyUnwrapped(pointer)) }
|
||||||
|
set { Playdate.systemAPI.pointee.setMenuItemValue.unsafelyUnwrapped(pointer, Int32(newValue)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience view of `value` for checkmark items.
|
||||||
|
public var isChecked: Bool {
|
||||||
|
get { value != 0 }
|
||||||
|
set { value = newValue ? 1 : 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
func deallocateRetainedTitles() {
|
||||||
|
for title in retainedOptionTitles { title.deallocate() }
|
||||||
|
retainedOptionTitles = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
internal import CPlaydate
|
||||||
|
|
||||||
|
extension System {
|
||||||
|
/// The system language.
|
||||||
|
public enum Language: UInt32, Sendable {
|
||||||
|
case english = 0
|
||||||
|
case japanese = 1
|
||||||
|
/// Only meaningful as an argument to `localizedText(forKey:language:)`.
|
||||||
|
case system = 2
|
||||||
|
|
||||||
|
init(_ language: PDLanguage) {
|
||||||
|
self = Language(rawValue: UInt32(language.rawValue)) ?? .english
|
||||||
|
}
|
||||||
|
var cValue: PDLanguage { PDLanguage(PDLanguage.RawValue(rawValue)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user