2026-07-25 12:39:32 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:55:53 +02:00
|
|
|
|
/// The range of `setPitchBend(_:)`, in half-steps.
|
2026-07-25 12:39:32 +02:00
|
|
|
|
public func setPitchBendRange(halfSteps: Float) {
|
|
|
|
|
|
Instrument.api.pointee.setPitchBendRange.unsafelyUnwrapped(pointer, halfSteps)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:55:53 +02:00
|
|
|
|
/// Transposes played notes by `halfSteps` (fractional values
|
|
|
|
|
|
/// allowed).
|
2026-07-25 12:39:32 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:55:53 +02:00
|
|
|
|
/// Releases every playing voice at time `when` (0 = now).
|
2026-07-25 12:39:32 +02:00
|
|
|
|
public func allNotesOff(when: UInt32 = 0) {
|
|
|
|
|
|
Instrument.api.pointee.allNotesOff.unsafelyUnwrapped(pointer, when)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:55:53 +02:00
|
|
|
|
/// Sets the volume of the left and right channels, 0...1.
|
2026-07-25 12:39:32 +02:00
|
|
|
|
public func setVolume(left: Float, right: Float) {
|
|
|
|
|
|
Instrument.api.pointee.setVolume.unsafelyUnwrapped(pointer, left, right)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:55:53 +02:00
|
|
|
|
/// The volume of the left and right channels.
|
2026-07-25 12:39:32 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 12:55:53 +02:00
|
|
|
|
/// The number of voices currently playing.
|
2026-07-25 12:39:32 +02:00
|
|
|
|
public var activeVoiceCount: Int {
|
|
|
|
|
|
Int(Instrument.api.pointee.activeVoiceCount.unsafelyUnwrapped(pointer))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|