Restructured the source code in the Playdate bindings target.
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
internal import CPlaydate
|
||||
|
||||
extension Sound {
|
||||
/// A mixer channel holding sources and effects. Wraps `SoundChannel`.
|
||||
public final class Channel {
|
||||
private static var api: UnsafePointer<playdate_sound_channel> { Playdate.channelAPI.unsafelyUnwrapped }
|
||||
|
||||
let pointer: OpaquePointer
|
||||
let isOwned: Bool
|
||||
private var retainedSources: [Source] = []
|
||||
private var retainedEffects: [Effect] = []
|
||||
private var retainedModulators: [SignalValue] = []
|
||||
|
||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||
self.pointer = pointer
|
||||
self.isOwned = isOwned
|
||||
}
|
||||
|
||||
/// Creates a new channel. Add it to the sound engine with `add()`.
|
||||
public convenience init() {
|
||||
self.init(pointer: Channel.api.pointee.newChannel.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||
isOwned: true)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if isOwned {
|
||||
Channel.api.pointee.freeChannel.unsafelyUnwrapped(pointer)
|
||||
// The freed channel no longer pulls its callback sources, so
|
||||
// their trampoline registrations can be released too.
|
||||
for source in retainedSources where source is CallbackSource {
|
||||
CallbackSource.release(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default channel, which sources are added to unless otherwise
|
||||
/// specified. A single shared wrapper, so resources retained through
|
||||
/// it (sources, effects, modulators) stay alive.
|
||||
public static var `default`: Channel { defaultChannel }
|
||||
|
||||
nonisolated(unsafe) private static let defaultChannel =
|
||||
Channel(pointer: snd.pointee.getDefaultChannel.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||
isOwned: false)
|
||||
|
||||
nonisolated(unsafe) private static var addedChannels: [Channel] = []
|
||||
|
||||
/// Adds the channel to the sound engine.
|
||||
@discardableResult
|
||||
public func add() -> Bool {
|
||||
let added = snd.pointee.addChannel.unsafelyUnwrapped(pointer) != 0
|
||||
if added, !Channel.addedChannels.contains(where: { $0 === self }) {
|
||||
Channel.addedChannels.append(self)
|
||||
}
|
||||
return added
|
||||
}
|
||||
|
||||
/// Removes the channel from the sound engine.
|
||||
@discardableResult
|
||||
public func remove() -> Bool {
|
||||
let removed = snd.pointee.removeChannel.unsafelyUnwrapped(pointer) != 0
|
||||
Channel.addedChannels.removeAll { $0 === self }
|
||||
return removed
|
||||
}
|
||||
|
||||
/// Adds a source to the channel. A source can only be on one channel.
|
||||
@discardableResult
|
||||
public func addSource(_ source: Source) -> Bool {
|
||||
let added = Channel.api.pointee.addSource.unsafelyUnwrapped(pointer, source.pointer) != 0
|
||||
if added, !retainedSources.contains(where: { $0 === source }) {
|
||||
retainedSources.append(source)
|
||||
}
|
||||
return added
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func removeSource(_ source: Source) -> Bool {
|
||||
let removed = Channel.api.pointee.removeSource.unsafelyUnwrapped(pointer, source.pointer) != 0
|
||||
// Only drop the retentions if the source was actually on this
|
||||
// channel; otherwise another channel may still be pulling it.
|
||||
if removed {
|
||||
retainedSources.removeAll { $0 === source }
|
||||
CallbackSource.release(source)
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
/// Adds a callback-based source to the channel. The callback fills
|
||||
/// the sample buffers and returns `true` if it produced output.
|
||||
public func addCallbackSource(stereo: Bool,
|
||||
_ callback: @escaping CallbackSource.Callback) -> CallbackSource {
|
||||
let source = CallbackSource(callback: callback)
|
||||
let pointer = Channel.api.pointee.addCallbackSource.unsafelyUnwrapped(
|
||||
self.pointer, CallbackSource.trampoline, source.contextPointer, stereo ? 1 : 0)
|
||||
source.adopt(pointer: pointer.unsafelyUnwrapped)
|
||||
retainedSources.append(source)
|
||||
return source
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func addEffect(_ effect: Effect) -> Bool {
|
||||
let added = Channel.api.pointee.addEffect.unsafelyUnwrapped(pointer, effect.pointer) != 0
|
||||
if added, !retainedEffects.contains(where: { $0 === effect }) {
|
||||
retainedEffects.append(effect)
|
||||
}
|
||||
return added
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func removeEffect(_ effect: Effect) -> Bool {
|
||||
let removed = Channel.api.pointee.removeEffect.unsafelyUnwrapped(pointer, effect.pointer) != 0
|
||||
retainedEffects.removeAll { $0 === effect }
|
||||
return removed
|
||||
}
|
||||
|
||||
/// The channel's volume, 0...1.
|
||||
public var volume: Float {
|
||||
get { Channel.api.pointee.getVolume.unsafelyUnwrapped(pointer) }
|
||||
set { Channel.api.pointee.setVolume.unsafelyUnwrapped(pointer, newValue) }
|
||||
}
|
||||
|
||||
/// Modulates the channel's volume.
|
||||
public func setVolumeModulator(_ modulator: SignalValue?) {
|
||||
retain(modulator)
|
||||
Channel.api.pointee.setVolumeModulator.unsafelyUnwrapped(pointer, modulator?.pointer)
|
||||
}
|
||||
|
||||
public var volumeModulator: SignalValue? {
|
||||
SignalValue.wrap(Channel.api.pointee.getVolumeModulator.unsafelyUnwrapped(pointer))
|
||||
}
|
||||
|
||||
/// The channel's stereo pan: -1 (left) to 1 (right).
|
||||
public func setPan(_ pan: Float) {
|
||||
Channel.api.pointee.setPan.unsafelyUnwrapped(pointer, pan)
|
||||
}
|
||||
|
||||
/// Modulates the channel's pan. The signal's range 0...1 maps to
|
||||
/// left...right.
|
||||
public func setPanModulator(_ modulator: SignalValue?) {
|
||||
retain(modulator)
|
||||
Channel.api.pointee.setPanModulator.unsafelyUnwrapped(pointer, modulator?.pointer)
|
||||
}
|
||||
|
||||
public var panModulator: SignalValue? {
|
||||
SignalValue.wrap(Channel.api.pointee.getPanModulator.unsafelyUnwrapped(pointer))
|
||||
}
|
||||
|
||||
/// A signal following the channel's dry (unprocessed) level.
|
||||
public var dryLevelSignal: SignalValue? {
|
||||
SignalValue.wrap(Channel.api.pointee.getDryLevelSignal.unsafelyUnwrapped(pointer))
|
||||
}
|
||||
|
||||
/// A signal following the channel's wet (processed) level.
|
||||
public var wetLevelSignal: SignalValue? {
|
||||
SignalValue.wrap(Channel.api.pointee.getWetLevelSignal.unsafelyUnwrapped(pointer))
|
||||
}
|
||||
|
||||
/// The channel's output as a source, for feeding into another channel.
|
||||
/// The same wrapper is returned on every access, so callbacks
|
||||
/// registered on it stay valid for the channel's lifetime.
|
||||
public var outputAsSource: Source? {
|
||||
guard let source = Channel.api.pointee.getOutputAsSource.unsafelyUnwrapped(pointer) else {
|
||||
return nil
|
||||
}
|
||||
if let cached = cachedOutputSource, cached.pointer == source {
|
||||
return cached
|
||||
}
|
||||
let wrapper = Source(pointer: source, isOwned: false)
|
||||
cachedOutputSource = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
private var cachedOutputSource: Source?
|
||||
|
||||
private func retain(_ modulator: SignalValue?) {
|
||||
if let modulator, !retainedModulators.contains(where: { $0 === modulator }) {
|
||||
retainedModulators.append(modulator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user