From 4cb0f8f5006669c94dff7e9d42fab32edd3e8cb6 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Fri, 18 Sep 2026 12:06:21 +0200 Subject: [PATCH] Adopted `Span` and `MutableSpan` for buffers across the library to adapt to Swift 6.4. --- Sources/PlaydateKit/File/Classes/Handle.swift | 22 +++++------ .../PlaydateKit/Graphics/Classes/Bitmap.swift | 38 ++++++++++++++++--- .../PlaydateKit/Graphics/Classes/Font.swift | 6 ++- Sources/PlaydateKit/Graphics/Graphics.swift | 22 ++++++++--- .../Graphics/Structures/Bitmap.Data.swift | 16 +++----- Sources/PlaydateKit/JSON/JSON.swift | 4 +- .../Network/Classes/HTTPConnection.swift | 7 ++-- .../Network/Classes/TCPConnection.swift | 22 +++++------ .../Effect/Aliases/Effect.Processor.swift | 9 +++-- .../Sound/Effect/Classes/Effect.swift | 6 +-- Sources/PlaydateKit/Sound/Sound.swift | 8 ++-- .../Source/Aliases/SoundCallbackSource.swift | 6 +-- .../Sound/Source/Classes/CallbackSource.swift | 6 +-- .../Sound/Source/Classes/FilePlayer.swift | 8 ++-- .../Sound/Synth/Classes/Synth.swift | 6 +-- .../Synth/Structures/Synth.Generator.swift | 10 ++--- Sources/PlaydateKit/System/System.swift | 8 +++- Tests/PlaydateKit/WrapperTests.swift | 6 ++- 18 files changed, 127 insertions(+), 83 deletions(-) diff --git a/Sources/PlaydateKit/File/Classes/Handle.swift b/Sources/PlaydateKit/File/Classes/Handle.swift index fc30ecf..357d5c1 100644 --- a/Sources/PlaydateKit/File/Classes/Handle.swift +++ b/Sources/PlaydateKit/File/Classes/Handle.swift @@ -31,9 +31,10 @@ extension File { /// 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)) + public func read(into buffer: inout MutableSpan) throws(PlaydateError) -> Int { + let result = buffer.withUnsafeMutableBufferPointer { buffer in + fileAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count)) + } if result < 0 { throw lastFileError() } return Int(result) } @@ -49,11 +50,12 @@ extension File { return bytes } - /// Writes the buffer to the file. Returns the number of bytes written. + /// Writes the bytes 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)) + public func write(_ bytes: Span) throws(PlaydateError) -> Int { + let result = bytes.withUnsafeBufferPointer { buffer in + fileAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count)) + } if result < 0 { throw lastFileError() } return Int(result) } @@ -61,11 +63,9 @@ extension File { /// 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)) + try bytes.withUnsafeBufferPointer { buffer throws(PlaydateError) in + try write(buffer.span) } - if result < 0 { throw lastFileError() } - return Int(result) } /// Writes the string's UTF-8 to the file. Returns the bytes written. diff --git a/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift b/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift index 9ef15d0..bb2a821 100644 --- a/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift +++ b/Sources/PlaydateKit/Graphics/Classes/Bitmap.swift @@ -39,14 +39,42 @@ extension Graphics { // MARK: Properties - /// The bitmap's dimensions, row stride, and raw storage. + /// The bitmap's dimensions and row stride. public var data: Data { + let raw = rawData() + return Data(width: raw.width, height: raw.height, rowBytes: raw.rowBytes, + hasMask: raw.mask != nil) + } + + /// Calls `body` with the bitmap's pixel data: one bit per pixel, + /// `height` rows of `rowBytes` bytes each. + public func withPixelData( + _ body: (inout MutableSpan) throws(Failure) -> Result + ) throws(Failure) -> Result { + let raw = rawData() + var span = UnsafeMutableBufferPointer( + start: raw.data, count: raw.data == nil ? 0 : raw.height * raw.rowBytes).mutableSpan + return try body(&span) + } + + /// Calls `body` with the bitmap's mask data, laid out like the pixel + /// data. Returns `nil` if the bitmap has no mask. + public func withMaskData( + _ body: (inout MutableSpan) throws(Failure) -> Result + ) throws(Failure) -> Result? { + let raw = rawData() + guard let mask = raw.mask else { return nil } + var span = UnsafeMutableBufferPointer(start: mask, count: raw.height * raw.rowBytes).mutableSpan + return try body(&span) + } + + private func rawData() -> (width: Int, height: Int, rowBytes: Int, + mask: UnsafeMutablePointer?, data: UnsafeMutablePointer?) { var width: Int32 = 0, height: Int32 = 0, rowBytes: Int32 = 0 var mask: UnsafeMutablePointer? var data: UnsafeMutablePointer? gfx.pointee.getBitmapData.unsafelyUnwrapped(pointer, &width, &height, &rowBytes, &mask, &data) - return Data(width: Int(width), height: Int(height), rowBytes: Int(rowBytes), - mask: mask, data: data) + return (Int(width), Int(height), Int(rowBytes), mask, data) } /// Cached dimensions, so `width`/`height` don't pay a full @@ -56,8 +84,8 @@ extension Graphics { private var size: (width: Int, height: Int) { if let cachedSize { return cachedSize } - let data = self.data - let size = (data.width, data.height) + let raw = rawData() + let size = (raw.width, raw.height) cachedSize = size return size } diff --git a/Sources/PlaydateKit/Graphics/Classes/Font.swift b/Sources/PlaydateKit/Graphics/Classes/Font.swift index 7ffa149..afcd9bd 100644 --- a/Sources/PlaydateKit/Graphics/Classes/Font.swift +++ b/Sources/PlaydateKit/Graphics/Classes/Font.swift @@ -23,9 +23,11 @@ extension Graphics { /// Creates a font from the contents of a .pft file already in memory. /// The bytes are copied and retained for the font's lifetime. - public convenience init?(data: UnsafeRawBufferPointer, wide: Bool = false) { + public convenience init?(data: Span, wide: Bool = false) { let copy = UnsafeMutableRawPointer.allocate(byteCount: data.count, alignment: 4) - copy.copyMemory(from: data.baseAddress.unsafelyUnwrapped, byteCount: data.count) + data.withUnsafeBytes { bytes in + copy.copyMemory(from: bytes.baseAddress.unsafelyUnwrapped, byteCount: bytes.count) + } let fontData = OpaquePointer(copy) guard let pointer = gfx.pointee.makeFontFromData.unsafelyUnwrapped( fontData, wide ? 1 : 0, Int32(data.count)) else { diff --git a/Sources/PlaydateKit/Graphics/Graphics.swift b/Sources/PlaydateKit/Graphics/Graphics.swift index 1d7afdc..a6545ba 100644 --- a/Sources/PlaydateKit/Graphics/Graphics.swift +++ b/Sources/PlaydateKit/Graphics/Graphics.swift @@ -279,15 +279,25 @@ extension Graphics { // MARK: - Framebuffer - /// The current working framebuffer. Rows are `rowSize` bytes. + /// Calls `body` with the current working framebuffer: `rows` rows of + /// `rowSize` bytes each. Returns `nil` if there is no framebuffer. /// Call `markUpdatedRows(from:to:)` after writing directly. - public static var frame: UnsafeMutablePointer? { - gfx.pointee.getFrame.unsafelyUnwrapped() + public static func withFrame( + _ body: (inout MutableSpan) throws(Failure) -> Result + ) throws(Failure) -> Result? { + guard let frame = gfx.pointee.getFrame.unsafelyUnwrapped() else { return nil } + var span = UnsafeMutableBufferPointer(start: frame, count: rows * rowSize).mutableSpan + return try body(&span) } - /// The framebuffer currently shown on the display. Rows are `rowSize` bytes. - public static var displayFrame: UnsafeMutablePointer? { - gfx.pointee.getDisplayFrame.unsafelyUnwrapped() + /// Calls `body` with the framebuffer currently shown on the display: + /// `rows` rows of `rowSize` bytes each. Returns `nil` if there is no + /// framebuffer. + public static func withDisplayFrame( + _ body: (Span) throws(Failure) -> Result + ) throws(Failure) -> Result? { + guard let frame = gfx.pointee.getDisplayFrame.unsafelyUnwrapped() else { return nil } + return try body(UnsafeBufferPointer(start: frame, count: rows * rowSize).span) } /// A bitmap view of the display framebuffer. Simulator only; `nil` on device. diff --git a/Sources/PlaydateKit/Graphics/Structures/Bitmap.Data.swift b/Sources/PlaydateKit/Graphics/Structures/Bitmap.Data.swift index 7efc480..e517a96 100644 --- a/Sources/PlaydateKit/Graphics/Structures/Bitmap.Data.swift +++ b/Sources/PlaydateKit/Graphics/Structures/Bitmap.Data.swift @@ -1,18 +1,14 @@ extension Graphics.Bitmap { - /// The bitmap's dimensions, row stride, and raw pixel/mask storage. - /// The pointers are owned by the bitmap. - public struct Data { + /// The bitmap's dimensions and row stride. Access the pixels themselves + /// with `withPixelData(_:)` and `withMaskData(_:)`. + public struct Data: Sendable { /// The bitmap's width, in pixels. public let width: Int /// The bitmap's height, in pixels. public let height: Int - /// The stride of one row of pixel data, in bytes. + /// The stride of one row of pixel (and mask) data, in bytes. public let rowBytes: Int - /// The bitmap's mask data, or `nil` if it has no mask. One bit per - /// pixel; rows are `rowBytes` wide. - public let mask: UnsafeMutablePointer? - /// The bitmap's pixel data. One bit per pixel; rows are `rowBytes` - /// wide. - public let data: UnsafeMutablePointer? + /// Whether the bitmap has a mask. + public let hasMask: Bool } } diff --git a/Sources/PlaydateKit/JSON/JSON.swift b/Sources/PlaydateKit/JSON/JSON.swift index 71f8751..93241a0 100644 --- a/Sources/PlaydateKit/JSON/JSON.swift +++ b/Sources/PlaydateKit/JSON/JSON.swift @@ -126,9 +126,9 @@ extension JSON { reader.read = { userdata, buffer, size in guard let userdata, let buffer else { return -1 } let file = Unmanaged.fromOpaque(userdata).takeUnretainedValue() - let destination = UnsafeMutableRawBufferPointer(start: buffer, count: Int(size)) + var destination = UnsafeMutableBufferPointer(start: buffer, count: Int(size)).mutableSpan do { - let count = try file.read(into: destination) + let count = try file.read(into: &destination) return count > 0 ? Int32(count) : -1 } catch { return -1 diff --git a/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift b/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift index 15f81be..ed967d9 100644 --- a/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift +++ b/Sources/PlaydateKit/Network/Classes/HTTPConnection.swift @@ -152,9 +152,10 @@ extension Network { /// 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)) + public func read(into buffer: inout MutableSpan) throws(NetError) -> Int { + let result = buffer.withUnsafeMutableBufferPointer { buffer in + httpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, UInt32(buffer.count)) + } if result < 0 { throw NetError(rawValue: result) ?? .unknown } diff --git a/Sources/PlaydateKit/Network/Classes/TCPConnection.swift b/Sources/PlaydateKit/Network/Classes/TCPConnection.swift index ab59116..8814154 100644 --- a/Sources/PlaydateKit/Network/Classes/TCPConnection.swift +++ b/Sources/PlaydateKit/Network/Classes/TCPConnection.swift @@ -112,8 +112,10 @@ extension Network { /// 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) + public func read(into buffer: inout MutableSpan) throws(NetError) -> Int { + let result = buffer.withUnsafeMutableBufferPointer { buffer in + tcpAPI.pointee.read.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count) + } if result < 0 { throw NetError(rawValue: result) ?? .unknown } @@ -133,11 +135,13 @@ extension Network { return bytes } - /// Writes the buffer to the connection. Returns the number of bytes + /// Writes the bytes 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) + public func write(_ bytes: Span) throws(NetError) -> Int { + let result = bytes.withUnsafeBufferPointer { buffer in + tcpAPI.pointee.write.unsafelyUnwrapped(pointer, buffer.baseAddress, buffer.count) + } if result < 0 { throw NetError(rawValue: result) ?? .unknown } @@ -148,13 +152,9 @@ extension Network { /// 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) + try bytes.withUnsafeBufferPointer { buffer throws(NetError) in + try write(buffer.span) } - if result < 0 { - throw NetError(rawValue: result) ?? .unknown - } - return Int(result) } } } diff --git a/Sources/PlaydateKit/Sound/Effect/Aliases/Effect.Processor.swift b/Sources/PlaydateKit/Sound/Effect/Aliases/Effect.Processor.swift index 3d7cc8a..d0ebe56 100644 --- a/Sources/PlaydateKit/Sound/Effect/Aliases/Effect.Processor.swift +++ b/Sources/PlaydateKit/Sound/Effect/Aliases/Effect.Processor.swift @@ -1,8 +1,9 @@ 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, - _ right: UnsafeMutableBufferPointer?, + /// Q8.24 format. `right` is empty when the channel is mono. + /// `bufferActive` is `false` when the input buffer is silent. Returns + /// `true` if the effect produced output. + public typealias Processor = (_ left: inout MutableSpan, + _ right: inout MutableSpan, _ bufferActive: Bool) -> Bool } diff --git a/Sources/PlaydateKit/Sound/Effect/Classes/Effect.swift b/Sources/PlaydateKit/Sound/Effect/Classes/Effect.swift index 7d1ec25..66d21bd 100644 --- a/Sources/PlaydateKit/Sound/Effect/Classes/Effect.swift +++ b/Sources/PlaydateKit/Sound/Effect/Classes/Effect.swift @@ -30,9 +30,9 @@ extension Sound { guard let effect, let left, let userdata = effectAPI.pointee.getUserdata.unsafelyUnwrapped(effect) else { return 0 } let box = Unmanaged.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 + var leftSpan = UnsafeMutableBufferPointer(start: left, count: Int(nsamples)).mutableSpan + var rightSpan = UnsafeMutableBufferPointer(start: right, count: right == nil ? 0 : Int(nsamples)).mutableSpan + return box.processor(&leftSpan, &rightSpan, bufactive != 0) ? 1 : 0 }, box.toOpaque()).unsafelyUnwrapped isOwned = true } diff --git a/Sources/PlaydateKit/Sound/Sound.swift b/Sources/PlaydateKit/Sound/Sound.swift index e69ff0a..afce940 100644 --- a/Sources/PlaydateKit/Sound/Sound.swift +++ b/Sources/PlaydateKit/Sound/Sound.swift @@ -51,14 +51,14 @@ extension Sound { /// 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. + /// The span contains mono 16-bit samples. @discardableResult public static func setMicCallback(source: MicSource = .autodetect, - _ callback: ((UnsafeMutableBufferPointer) -> Bool)?) -> Bool { + _ callback: ((Span) -> Bool)?) -> Bool { micCallback = callback if callback != nil { return snd.pointee.setMicCallback.unsafelyUnwrapped({ _, buffer, length in - let samples = UnsafeMutableBufferPointer(start: buffer, count: Int(length)) + let samples = UnsafeBufferPointer(start: buffer, count: Int(length)).span return Sound.micCallback?(samples) == true ? 1 : 0 }, nil, CPlaydate.MicSource(CPlaydate.MicSource.RawValue(source.rawValue))) != 0 } else { @@ -66,7 +66,7 @@ extension Sound { } } - nonisolated(unsafe) private static var micCallback: ((UnsafeMutableBufferPointer) -> Bool)? + nonisolated(unsafe) private static var micCallback: ((Span) -> Bool)? /// Asks the user for permission to record from the microphone. `purpose` /// is shown in the permission prompt. The completion receives whether diff --git a/Sources/PlaydateKit/Sound/Source/Aliases/SoundCallbackSource.swift b/Sources/PlaydateKit/Sound/Source/Aliases/SoundCallbackSource.swift index a899ebb..8419891 100644 --- a/Sources/PlaydateKit/Sound/Source/Aliases/SoundCallbackSource.swift +++ b/Sources/PlaydateKit/Sound/Source/Aliases/SoundCallbackSource.swift @@ -1,6 +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, - _ right: UnsafeMutableBufferPointer?) -> Bool + /// produced. `right` is empty for mono sources. + public typealias Callback = (_ left: inout MutableSpan, + _ right: inout MutableSpan) -> Bool } diff --git a/Sources/PlaydateKit/Sound/Source/Classes/CallbackSource.swift b/Sources/PlaydateKit/Sound/Source/Classes/CallbackSource.swift index 0d16202..dc7a5da 100644 --- a/Sources/PlaydateKit/Sound/Source/Classes/CallbackSource.swift +++ b/Sources/PlaydateKit/Sound/Source/Classes/CallbackSource.swift @@ -27,9 +27,9 @@ extension Sound { UnsafeMutablePointer?, Int32) -> Int32 = { context, left, right, length in guard let context, let left else { return 0 } let source = Unmanaged.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 + var leftSpan = UnsafeMutableBufferPointer(start: left, count: Int(length)).mutableSpan + var rightSpan = UnsafeMutableBufferPointer(start: right, count: right == nil ? 0 : Int(length)).mutableSpan + return source.callback(&leftSpan, &rightSpan) ? 1 : 0 } /// Attaches the C object created for this source. diff --git a/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift b/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift index 42ce46f..814693d 100644 --- a/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift +++ b/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.swift @@ -7,7 +7,7 @@ extension Sound { var loopCallback: ((FilePlayer) -> Void)? var fadeCallback: ((FilePlayer) -> Void)? - var mp3DataSource: ((UnsafeMutableBufferPointer) -> Int)? + var mp3DataSource: ((inout MutableSpan) -> Int)? private var retainedRateModulator: SignalValue? override init(pointer: OpaquePointer?, isOwned: Bool) { @@ -131,13 +131,13 @@ extension Sound { /// 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) -> Int) { + _ dataSource: @escaping (inout MutableSpan) -> Int) { mp3DataSource = dataSource FilePlayer.api.pointee.setMP3StreamSource.unsafelyUnwrapped(pointer, { data, bytes, userdata in guard let userdata, let data else { return 0 } let player = Unmanaged.fromOpaque(userdata).takeUnretainedValue() - let buffer = UnsafeMutableBufferPointer(start: data, count: Int(bytes)) - return Int32(player.mp3DataSource?(buffer) ?? 0) + var buffer = UnsafeMutableBufferPointer(start: data, count: Int(bytes)).mutableSpan + return Int32(player.mp3DataSource?(&buffer) ?? 0) }, Unmanaged.passUnretained(self).toOpaque(), bufferLength) } diff --git a/Sources/PlaydateKit/Sound/Synth/Classes/Synth.swift b/Sources/PlaydateKit/Sound/Synth/Classes/Synth.swift index 5b3b6d8..b87557e 100644 --- a/Sources/PlaydateKit/Sound/Synth/Classes/Synth.swift +++ b/Sources/PlaydateKit/Sound/Synth/Classes/Synth.swift @@ -75,9 +75,9 @@ extension Sound { { userdata, left, right, nsamples, rate, drate in guard let userdata, let left else { return 0 } let box = Unmanaged.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)) + var leftSpan = UnsafeMutableBufferPointer(start: left, count: Int(nsamples)).mutableSpan + var rightSpan = UnsafeMutableBufferPointer(start: right, count: right == nil ? 0 : Int(nsamples)).mutableSpan + return Int32(box.generator.render(&leftSpan, &rightSpan, rate, drate)) }, { userdata, note, velocity, length in guard let userdata else { return } diff --git a/Sources/PlaydateKit/Sound/Synth/Structures/Synth.Generator.swift b/Sources/PlaydateKit/Sound/Synth/Structures/Synth.Generator.swift index 492579d..9dbaf6a 100644 --- a/Sources/PlaydateKit/Sound/Synth/Structures/Synth.Generator.swift +++ b/Sources/PlaydateKit/Sound/Synth/Structures/Synth.Generator.swift @@ -2,11 +2,11 @@ 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 + /// stereo generators; it is empty for mono ones). `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, - _ right: UnsafeMutableBufferPointer?, + public var render: (_ left: inout MutableSpan, + _ right: inout MutableSpan, _ 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)? @@ -17,8 +17,8 @@ extension Sound.Synth { /// valid. public var setParameter: ((_ parameter: Int, _ value: Float) -> Bool)? - public init(render: @escaping (_ left: UnsafeMutableBufferPointer, - _ right: UnsafeMutableBufferPointer?, + public init(render: @escaping (_ left: inout MutableSpan, + _ right: inout MutableSpan, _ rate: UInt32, _ drate: Int32) -> Int, noteOn: ((_ note: Sound.MIDINote, _ velocity: Float, _ length: Float) -> Void)? = nil, release: ((_ stop: Bool) -> Void)? = nil, diff --git a/Sources/PlaydateKit/System/System.swift b/Sources/PlaydateKit/System/System.swift index c343841..178eb7c 100644 --- a/Sources/PlaydateKit/System/System.swift +++ b/Sources/PlaydateKit/System/System.swift @@ -307,8 +307,12 @@ extension System { /// Sends data over the mirror connection. Returns `false` if mirroring is /// not active or the send fails. @discardableResult - public static func sendMirrorData(command: UInt8, data: UnsafeMutableRawBufferPointer) -> Bool { - api.pointee.sendMirrorData.unsafelyUnwrapped(command, data.baseAddress, Int32(data.count)) + public static func sendMirrorData(command: UInt8, data: Span) -> Bool { + data.withUnsafeBufferPointer { buffer in + // The C API takes a non-const pointer but only reads the data. + api.pointee.sendMirrorData.unsafelyUnwrapped( + command, UnsafeMutableRawPointer(mutating: buffer.baseAddress), Int32(buffer.count)) + } } /// OS, language, and pdx version information. diff --git a/Tests/PlaydateKit/WrapperTests.swift b/Tests/PlaydateKit/WrapperTests.swift index 793772a..ac2bcc7 100644 --- a/Tests/PlaydateKit/WrapperTests.swift +++ b/Tests/PlaydateKit/WrapperTests.swift @@ -146,7 +146,8 @@ struct WrapperTests { var produced = 0 let source = Sound.addSource(stereo: false) { left, right in produced += left.count - #expect(right == nil) + let isMono = right.isEmpty // #expect cannot capture a span + #expect(isMono) return true } #expect(Sound.CallbackSource.live.count == baseline + 1) @@ -280,7 +281,8 @@ struct WrapperTests { let effect = Sound.Effect(processor: { left, right, _ in _ = token processed += left.count - #expect(right == nil) + let isMono = right.isEmpty // #expect cannot capture a span + #expect(isMono) return true })