Files
playdate-kit/Sources/PlaydateKit/Sound/Source/Classes/FilePlayer.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

148 lines
6.4 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 {
/// 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: ((inout MutableSpan<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)
}
public convenience init(path: String) throws(PlaydateError) {
self.init()
try load(path: path)
}
deinit {
if isOwned {
FilePlayer.api.pointee.freePlayer.unsafelyUnwrapped(pointer)
}
}
public func load(path: String) throws(PlaydateError) {
let loaded = path.withCString {
FilePlayer.api.pointee.loadIntoPlayer.unsafelyUnwrapped(pointer, $0) != 0
}
if !loaded {
throw PlaydateError(message: "unable to load audio file: \(path)")
}
}
/// Stream buffer length, in seconds; default 0.25.
public func setBufferLength(_ seconds: Float) {
FilePlayer.api.pointee.setBufferLength.unsafelyUnwrapped(pointer, seconds)
}
/// Plays `repeat` times (0 loops forever); `false` if buffer allocation failed.
@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)
}
/// Length in seconds.
public var length: Float {
FilePlayer.api.pointee.getLength.unsafelyUnwrapped(pointer)
}
/// Playback position, in seconds.
public var offset: Float {
get { FilePlayer.api.pointee.getOffset.unsafelyUnwrapped(pointer) }
set { FilePlayer.api.pointee.setOffset.unsafelyUnwrapped(pointer, newValue) }
}
/// Playback rate; 1 is normal. Negative (reverse) is unsupported.
public var rate: Float {
get { FilePlayer.api.pointee.getRate.unsafelyUnwrapped(pointer) }
set { FilePlayer.api.pointee.setRate.unsafelyUnwrapped(pointer, newValue) }
}
/// Loop region, in seconds; `end` 0 means end of file. Loops only if played
/// with `repeat` 0 or ≥ 2.
public func setLoopRange(start: Float, end: Float) {
FilePlayer.api.pointee.setLoopRange.unsafelyUnwrapped(pointer, start, end)
}
/// Whether playback underran because the file couldn't be read fast enough.
public var didUnderrun: Bool {
FilePlayer.api.pointee.didUnderrun.unsafelyUnwrapped(pointer) != 0
}
/// If `true`, an underrun stops playback and calls the finish callback; by
/// default playback resumes after a stutter once data arrives.
public func setStopOnUnderrun(_ flag: Bool) {
FilePlayer.api.pointee.setStopOnUnderrun.unsafelyUnwrapped(pointer, flag ? 1 : 0)
}
/// Called each time playback loops; `nil` removes it.
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 to `left`/`right` (01) 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 from `dataSource`, buffering `bufferLength` seconds. `dataSource`
/// fills the span and returns bytes written; 0 ends the stream.
public func setMP3StreamSource(bufferLength: Float,
_ dataSource: @escaping (inout MutableSpan<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()
var buffer = UnsafeMutableBufferPointer(start: data, count: Int(bytes)).mutableSpan
return Int32(player.mp3DataSource?(&buffer) ?? 0)
}, Unmanaged.passUnretained(self).toOpaque(), bufferLength)
}
/// A signal added to `rate`; `nil` clears it. The player retains it.
public var rateModulator: SignalValue? {
get { SignalValue.wrap(FilePlayer.api.pointee.getRateModulator.unsafelyUnwrapped(pointer)) }
set {
retainedRateModulator = newValue
FilePlayer.api.pointee.setRateModulator.unsafelyUnwrapped(pointer, newValue?.pointer)
}
}
}
}