Files
playdate-kit/Sources/PlaydateKit/Sound/Synth/Classes/Synth.swift
T
javier c5037dd716 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>
2026-09-18 13:15:08 +00:00

220 lines
9.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
internal import CPlaydate
extension Sound {
/// A synthesizer voice. Wraps `PDSynth`. Keeps samples and generators set on it alive.
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)
}
}
/// An independently owned copy, including any generator.
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 `sample` (uncompressed PCM, not ADPCM). Frames `sustainStart..<sustainEnd`
/// loop while held; `sustainEnd` 0 with nonzero `sustainStart` means the sample's end.
public func setSample(_ sample: AudioSample, sustainStart: UInt32 = 0, sustainEnd: UInt32 = 0) {
retainedSample = sample
Synth.api.pointee.setSample.unsafelyUnwrapped(pointer, sample.pointer, sustainStart, sustainEnd)
}
/// Plays `sample` (16-bit mono, uncompressed) as `columns` × `rows` cells of
/// 2^`log2size` samples; parameters 14 select the position.
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")
}
}
/// `copy()` shares `generator`.
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()
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 }
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
/// In seconds.
public func setAttackTime(_ attack: Float) {
Synth.api.pointee.setAttackTime.unsafelyUnwrapped(pointer, attack)
}
/// In seconds.
public func setDecayTime(_ decay: Float) {
Synth.api.pointee.setDecayTime.unsafelyUnwrapped(pointer, decay)
}
/// 0...1.
public func setSustainLevel(_ sustain: Float) {
Synth.api.pointee.setSustainLevel.unsafelyUnwrapped(pointer, sustain)
}
/// In seconds.
public func setReleaseTime(_ release: Float) {
Synth.api.pointee.setReleaseTime.unsafelyUnwrapped(pointer, release)
}
/// The amplitude envelope; owned by the synth, valid only while it is alive.
public var envelope: Envelope? {
guard let envelope = Synth.api.pointee.getEnvelope.unsafelyUnwrapped(pointer) else { return nil }
return Envelope(pointer: envelope, isOwned: false)
}
public func clearEnvelope() {
Synth.api.pointee.clearEnvelope.unsafelyUnwrapped(pointer)
}
// MARK: Pitch, modulation, and parameters
/// Fractional half-steps allowed.
public func setTranspose(_ halfSteps: Float) {
Synth.api.pointee.setTranspose.unsafelyUnwrapped(pointer, halfSteps)
}
/// 1 is an octave up, -1 an octave down.
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 generator supports.
public var parameterCount: Int {
Int(Synth.api.pointee.getParameterCount.unsafelyUnwrapped(pointer))
}
/// `parameter` is 1-based. Returns `false` if it is invalid.
@discardableResult
public func setParameter(_ parameter: Int, value: Float) -> Bool {
Synth.api.pointee.setParameter.unsafelyUnwrapped(pointer, Int32(parameter), value) != 0
}
/// `parameter` is 1-based.
public func setParameterModulator(_ parameter: Int, _ modulator: SignalValue?) {
retain(modulator)
Synth.api.pointee.setParameterModulator.unsafelyUnwrapped(pointer, Int32(parameter),
modulator?.pointer)
}
/// `parameter` is 1-based.
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
/// `frequency` in Hz; `length` in seconds, `nil` until `noteOff(when:)`;
/// `when` is an audio-clock time, 0 for now.
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)
}
/// 60 is C4; fractional notes allowed. Other arguments as in `playNote`.
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 note at audio-clock time `when`, or now if 0.
public func noteOff(when: UInt32 = 0) {
Synth.api.pointee.noteOff.unsafelyUnwrapped(pointer, when)
}
/// Stops immediately, skipping the release phase.
public func stop() {
Synth.api.pointee.stop.unsafelyUnwrapped(pointer)
}
}
}