Swift 6.4 migration and exhancements (#1)

This PR contains the work done to update the library and the attached example project to use the Swift 6.4 computer as a minimum supported version and also, to use the latest features introduced in it.

Reviewed-on: #1
Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-09-18 13:15:08 +00:00
committed by javier
parent d0a561b91f
commit c5037dd716
105 changed files with 1390 additions and 1397 deletions
@@ -1,8 +1,7 @@
internal import CPlaydate
extension Sound {
/// A signal whose values are set on a sequence timeline. Wraps
/// `ControlSignal`.
/// Values set at sequence steps, for automating parameters. Wraps `ControlSignal`.
public final class ControlSignal: SignalValue {
private static var api: UnsafePointer<playdate_control_signal> { Playdate.controlSignalAPI.unsafelyUnwrapped }
@@ -21,24 +20,21 @@ extension Sound {
}
}
/// Removes all events from the signal's timeline.
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.
/// If `interpolate`, ramps to `value` 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)
}
/// Removes the event at `step`, if any.
public func removeEvent(step: Int) {
ControlSignal.api.pointee.removeEvent.unsafelyUnwrapped(pointer, Int32(step))
}
/// The MIDI controller number for signals loaded from a MIDI file.
/// For signals created by `Sequence.loadMIDIFile(path:)`.
public var midiControllerNumber: Int {
Int(ControlSignal.api.pointee.getMIDIControllerNumber.unsafelyUnwrapped(pointer))
}
@@ -5,8 +5,7 @@ extension Sound {
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).
/// `attack`, `decay`, and `release` are in seconds; `sustain` is 0...1.
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)
@@ -22,55 +21,51 @@ extension Sound {
}
}
/// The attack time, in seconds.
/// In seconds.
public func setAttack(_ attack: Float) {
Envelope.api.pointee.setAttack.unsafelyUnwrapped(pointer, attack)
}
/// The decay time, in seconds.
/// In seconds.
public func setDecay(_ decay: Float) {
Envelope.api.pointee.setDecay.unsafelyUnwrapped(pointer, decay)
}
/// The sustain level, 0...1.
/// 0...1.
public func setSustain(_ sustain: Float) {
Envelope.api.pointee.setSustain.unsafelyUnwrapped(pointer, sustain)
}
/// The release time, in seconds.
/// In seconds.
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.
/// If `true`, retriggering before release stays in sustain instead of re-attacking.
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.
/// If `true`, each note starts from 0 instead of the 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.
/// Segment shape, 0 (linear) to 1 (exponential).
public func setCurvature(_ amount: Float) {
Envelope.api.pointee.setCurvature.unsafelyUnwrapped(pointer, amount)
}
/// How much note velocity scales the envelope's output.
/// 1 (default) scales output by velocity; 0 ignores it.
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).
/// Rate scale by note: 1 below `start`, `scaling` above `end`, interpolated between.
public func setRateScaling(_ scaling: Float, start: MIDINote, end: MIDINote) {
Envelope.api.pointee.setRateScaling.unsafelyUnwrapped(pointer, scaling, start, end)
}
/// The envelope's current value.
public var value: Float {
Envelope.api.pointee.getValue.unsafelyUnwrapped(pointer)
}
@@ -22,33 +22,32 @@ extension Sound {
LFO.api.pointee.setType.unsafelyUnwrapped(pointer, shape.cValue)
}
/// The LFO rate, in cycles per second.
/// In cycles per second.
public func setRate(_ rate: Float) {
LFO.api.pointee.setRate.unsafelyUnwrapped(pointer, rate)
}
/// The current phase, 0...1.
/// 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.
/// 0...1; used when the LFO is retriggered.
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.
/// The output's amplitude 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.
/// Switches to `.arpeggiator` over `steps`, in half-steps from the center note
/// (e.g. `[0, 4, 7, 12]` for a major chord).
public func setArpeggiation(_ steps: [Float]) {
var steps = steps
steps.withUnsafeMutableBufferPointer { buffer in
@@ -57,8 +56,7 @@ extension Sound {
}
}
/// For `.function` LFOs: the Swift function providing the value. If
/// `interpolate` is `true`, values are interpolated between calls.
/// For `.function` LFOs; `interpolate` smooths between calls. Keeps `function` alive.
public func setFunction(interpolate: Bool = false, _ function: @escaping (LFO) -> Float) {
self.function = function
LFO.api.pointee.setFunction.unsafelyUnwrapped(pointer, { _, userdata in
@@ -68,28 +66,27 @@ extension Sound {
}, Unmanaged.passUnretained(self).toOpaque(), interpolate ? 1 : 0)
}
/// Waits `holdoff` seconds after a note starts, then ramps the LFO
/// depth up over `rampTime` seconds.
/// Holds at center `holdoff` seconds after a note starts, then ramps linearly to
/// full depth 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.
/// If `true`, notes on a synth using the LFO reset its phase to the start phase.
public func setRetrigger(_ flag: Bool) {
LFO.api.pointee.setRetrigger.unsafelyUnwrapped(pointer, flag ? 1 : 0)
}
/// When `true`, the LFO runs globally instead of per-note.
/// If `true`, updates continuously, even when not in use.
public func setGlobal(_ global: Bool) {
LFO.api.pointee.setGlobal.unsafelyUnwrapped(pointer, global ? 1 : 0)
}
/// Seeds the random number generator used by `.sampleAndHold` LFOs.
/// Seeds the random generator, for reproducible `.sampleAndHold` output.
public func setRandomSeed(_ seed: UInt16) {
LFO.api.pointee.setRandomSeed.unsafelyUnwrapped(pointer, seed)
}
/// The LFO's current value.
public var value: Float {
LFO.api.pointee.getValue.unsafelyUnwrapped(pointer)
}
@@ -1,8 +1,8 @@
internal import CPlaydate
extension Sound {
/// A signal object; also provides custom signals driven by Swift
/// callbacks. Wraps `PDSynthSignal`.
/// A scaled, offset signal: custom (Swift callbacks) or tracking another value.
/// Wraps `PDSynthSignal`.
public final class Signal: SignalValue {
private static var api: UnsafePointer<playdate_sound_signal> { Playdate.signalAPI.unsafelyUnwrapped }
@@ -11,7 +11,7 @@ extension Sound {
init(_ callbacks: Callbacks) { self.callbacks = callbacks }
}
/// Creates a signal driven by the given callbacks.
/// `callbacks` stay alive until the C signal is freed.
public init(callbacks: Callbacks) {
let box = Unmanaged.passRetained(Box(callbacks))
let pointer = Signal.api.pointee.newSignal.unsafelyUnwrapped(
@@ -38,8 +38,7 @@ extension Sound {
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
}
/// Creates a plain signal object wrapping an existing signal value,
/// so it can be scaled and offset.
/// Tracks `value` so it can be scaled and offset; does not keep `value` alive.
public init(value: SignalValue) {
let pointer = Signal.api.pointee.newSignalForValue.unsafelyUnwrapped(value.pointer)
super.init(pointer: pointer.unsafelyUnwrapped, isOwned: true)
@@ -55,17 +54,15 @@ extension Sound {
}
}
/// The signal's current value.
public var value: Float {
Signal.api.pointee.getValue.unsafelyUnwrapped(pointer)
}
/// Scales the signal's output.
/// Applied before the offset.
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)
}
@@ -1,6 +1,7 @@
extension Sound {
/// A value that can modulate a parameter. The base class of `Signal`,
/// `LFO`, `Envelope`, and `ControlSignal`. Wraps `PDSynthSignalValue`.
/// A value that can modulate a parameter. Wraps `PDSynthSignalValue`; base of
/// `Signal`, `LFO`, `Envelope`, and `ControlSignal`. What it modulates keeps it
/// alive; assigning `nil` to a modulator property clears it.
public class SignalValue {
let pointer: OpaquePointer
let isOwned: Bool
@@ -10,7 +11,7 @@ extension Sound {
self.isOwned = isOwned
}
/// Wraps a signal value pointer returned by the OS (not owned).
/// Wraps a C API pointer without taking ownership.
static func wrap(_ pointer: OpaquePointer?) -> SignalValue? {
guard let pointer else { return nil }
return SignalValue(pointer: pointer, isOwned: false)
@@ -1,15 +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
/// Random values, held for each cycle.
case sampleAndHold = 3
case sawtoothUp = 4
case sawtoothDown = 5
/// Steps through the values set by `setArpeggiation(_:)`.
case arpeggiator = 6
/// Values come from the function set by `setFunction(interpolate:_:)`.
case function = 7
var cValue: LFOType { LFOType(LFOType.RawValue(rawValue)) }
@@ -1,16 +1,14 @@
extension Sound.Signal {
/// Custom signal callbacks.
/// Custom signal callbacks, run on the audio render thread; return quickly.
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`.
/// Returns the value at the end of the cycle; `ioFrames` holds its frames left. For
/// a mid-cycle value, write it to `interpolationValue`, set `ioFrames` to its offset.
public var step: (_ ioFrames: UnsafeMutablePointer<Int32>?,
_ interpolationValue: UnsafeMutablePointer<Float>?) -> Float
/// Called on note-on events. `length` is -1 for indefinite notes.
/// `length` is in seconds, or -1 if indefinite.
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.
/// `stopped` is `false` on release, `true` on stop; `offset` is the frame offset
/// into the cycle.
public var noteOff: ((_ stopped: Bool, _ offset: Int) -> Void)?
public init(step: @escaping (_ ioFrames: UnsafeMutablePointer<Int32>?,