diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/play-date.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/play-date.xcscheme deleted file mode 100644 index 4f0451d..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/play-date.xcscheme +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/PlayDate/Sound.swift b/Sources/PlayDate/Sound.swift index 87dff3c..fcb7b08 100644 --- a/Sources/PlayDate/Sound.swift +++ b/Sources/PlayDate/Sound.swift @@ -78,7 +78,9 @@ extension Sound { /// Removes a source from its channel. @discardableResult public static func removeSource(_ source: Source) -> Bool { - snd.removeSource.unsafelyUnwrapped(source.pointer) != 0 + let removed = snd.removeSource.unsafelyUnwrapped(source.pointer) != 0 + CallbackSource.release(source) + return removed } /// Sets a callback that records microphone input. Return `false` from the @@ -196,6 +198,11 @@ extension Sound { deinit { if isOwned { Channel.api.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) + } } } @@ -240,6 +247,7 @@ extension Sound { public func removeSource(_ source: Source) -> Bool { let removed = Channel.api.removeSource.unsafelyUnwrapped(pointer, source.pointer) != 0 retainedSources.removeAll { $0 === source } + CallbackSource.release(source) return removed } diff --git a/Sources/PlayDate/SoundSource.swift b/Sources/PlayDate/SoundSource.swift index ebbda1b..848f761 100644 --- a/Sources/PlayDate/SoundSource.swift +++ b/Sources/PlayDate/SoundSource.swift @@ -66,10 +66,17 @@ extension Sound { let callback: Callback - /// Sources created through the top-level `Sound.addSource` are kept - /// alive here until removed with `Sound.removeSource`. + /// 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)